diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 29fc42f289..0a7f9afef0 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1796,8 +1796,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } } else { let groupId = self.groupId + let filterPredicate = (self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.data).flatMap(chatListFilterPredicate) signal = self.context.account.postbox.transaction { transaction -> Void in - let filterPredicate = (self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.data).flatMap(chatListFilterPredicate) markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: filterPredicate) if let filterPredicate = filterPredicate { for additionalGroupId in filterPredicate.includeAdditionalPeerGroupIds { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index a05cf1d705..e88efbf722 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -341,7 +341,7 @@ private final class ChatListContainerItemNode: ASDisplayNode { if strongSelf.emptyShimmerEffectNode == nil { let emptyShimmerEffectNode = ChatListShimmerNode() strongSelf.emptyShimmerEffectNode = emptyShimmerEffectNode - strongSelf.addSubnode(emptyShimmerEffectNode) + strongSelf.insertSubnode(emptyShimmerEffectNode, belowSubnode: strongSelf.listNode) if let (size, insets, _) = strongSelf.validLayout, let offset = strongSelf.floatingHeaderOffset { strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset, transition: .immediate) } diff --git a/submodules/ChatListUI/Sources/ChatListEmptyNode.swift b/submodules/ChatListUI/Sources/ChatListEmptyNode.swift index 35624cda4e..46a9794511 100644 --- a/submodules/ChatListUI/Sources/ChatListEmptyNode.swift +++ b/submodules/ChatListUI/Sources/ChatListEmptyNode.swift @@ -119,7 +119,7 @@ final class ChatListEmptyNode: ASDisplayNode { let string = NSMutableAttributedString(string: self.isFilter ? strings.ChatList_EmptyChatListFilterTitle : strings.ChatList_EmptyChatList, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor) let descriptionString: NSAttributedString if self.isFilter { - descriptionString = NSAttributedString(string: strings.ChatList_EmptyChatListFilterText, font: Font.medium(14.0), textColor: theme.list.itemSecondaryTextColor) + descriptionString = NSAttributedString(string: strings.ChatList_EmptyChatListFilterText, font: Font.regular(14.0), textColor: theme.list.itemSecondaryTextColor) } else { descriptionString = NSAttributedString() } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index f6dfd5d0d6..9769ed2ffd 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -283,6 +283,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, if view.laterIndex == nil && savedMessagesPeer == nil { pinnedIndexOffset += UInt16(view.additionalItemEntries.count) } + var filterAfterHole = false loop: for entry in view.entries { switch entry { case let .MessageEntry(index, message, combinedReadState, isRemovedFromTotalUnreadCount, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact): @@ -301,8 +302,9 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, result.append(.PeerEntry(index: offsetPinnedIndex(index, offset: pinnedIndexOffset), presentationData: state.presentationData, message: updatedMessage, readState: updatedCombinedReadState, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: embeddedState, peer: peer, presence: peerPresence, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, selected: state.selectedPeerIds.contains(index.messageIndex.id.peerId), inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: false, hasFailedMessages: hasFailed, isContact: isContact)) case let .HoleEntry(hole): if hole.index.timestamp == Int32.max - 1 { - return ([.HeaderEntry], true) + //return ([.HeaderEntry], true) } + filterAfterHole = true result.append(.HoleEntry(hole, theme: state.presentationData.theme)) } } @@ -351,11 +353,35 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, } } } + + var isLoading: Bool = false + + if filterAfterHole { + var seenHole = false + for i in (0 ..< result.count).reversed() { + if seenHole { + result.remove(at: i) + } else { + switch result[i] { + case .HeaderEntry: + break + case .ArchiveIntro, .AdditionalCategory, .GroupReferenceEntry: + break + case .PeerEntry: + break + case .HoleEntry: + isLoading = true + seenHole = true + result.remove(at: i) + } + } + } + } if result.count >= 1, case .HoleEntry = result[result.count - 1] { return ([.HeaderEntry], true) } else if result.count == 1, case .HoleEntry = result[0] { return ([.HeaderEntry], true) } - return (result, false) + return (result, isLoading) } diff --git a/submodules/Postbox/Sources/ChatListTable.swift b/submodules/Postbox/Sources/ChatListTable.swift index 6996f19a9b..e3e60119f7 100644 --- a/submodules/Postbox/Sources/ChatListTable.swift +++ b/submodules/Postbox/Sources/ChatListTable.swift @@ -262,7 +262,9 @@ final class ChatListTable: Table { let messageTagSummaryResult = resolveChatListMessageTagSummaryResultCalculation(postbox: postbox, peerId: peer.id, calculation: filterPredicate.messageTagSummary) - if filterPredicate.includes(peer: peer, groupId: groupId, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: messageTagSummaryResult) { + if filterPredicate.pinnedPeerIds.contains(peer.id) { + passFilter = true + } else if filterPredicate.includes(peer: peer, groupId: groupId, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: messageTagSummaryResult) { passFilter = true } else { passFilter = false diff --git a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift index 41f02904c0..6095198845 100644 --- a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift @@ -1488,7 +1488,7 @@ private func resolveMissingPeerChatInfos(network: Network, state: AccountMutable var updatedState = state switch result { - case let .peerDialogs(dialogs, messages, chats, users, state): + case let .peerDialogs(dialogs, messages, chats, users, _): updatedState.mergeChats(chats) updatedState.mergeUsers(users) @@ -1496,7 +1496,7 @@ private func resolveMissingPeerChatInfos(network: Network, state: AccountMutable for dialog in dialogs { switch dialog { - case let .dialog(_, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, notifySettings, pts, draft, folderId): + case let .dialog(_, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, notifySettings, pts, _, folderId): let peerId = peer.peerId updatedState.setNeedsHoleFromPreviousState(peerId: peerId, namespace: Namespaces.Message.Cloud) diff --git a/submodules/TelegramCore/Sources/ManagedChatListHoles.swift b/submodules/TelegramCore/Sources/ManagedChatListHoles.swift index 08b79d158c..bebb9ebefe 100644 --- a/submodules/TelegramCore/Sources/ManagedChatListHoles.swift +++ b/submodules/TelegramCore/Sources/ManagedChatListHoles.swift @@ -2,9 +2,12 @@ import Foundation import Postbox import SwiftSignalKit import SyncCore +import TelegramApi private final class ManagedChatListHolesState { private var currentHole: (ChatListHolesEntry, Disposable)? + private var currentPinnedIds: (Set, Disposable)? + private var processedPinnedIds: Set? func clearDisposables() -> [Disposable] { if let (_, disposable) = self.currentHole { @@ -15,9 +18,17 @@ private final class ManagedChatListHolesState { } } - func update(entries: [ChatListHolesEntry]) -> (removed: [Disposable], added: [ChatListHolesEntry: MetaDisposable]) { + func update(entries: [ChatListHolesEntry], pinnedIds: Set) -> (removed: [Disposable], added: [ChatListHolesEntry: MetaDisposable], addedPinnedIds: (Set, MetaDisposable)?) { var removed: [Disposable] = [] var added: [ChatListHolesEntry: MetaDisposable] = [:] + var addedPinnedIds: (Set, MetaDisposable)? + + if self.processedPinnedIds == nil && !pinnedIds.isEmpty { + self.processedPinnedIds = pinnedIds + let disposable = MetaDisposable() + self.currentPinnedIds = (pinnedIds, disposable) + addedPinnedIds = (pinnedIds, disposable) + } if let (entry, disposable) = self.currentHole { if !entries.contains(entry) { @@ -32,7 +43,7 @@ private final class ManagedChatListHolesState { added[entry] = disposable } - return (removed, added) + return (removed, added, addedPinnedIds) } } @@ -50,7 +61,13 @@ func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: Pee return lhs.hole.index > rhs.hole.index }) + var pinnedIds = Set() + if let preferencesView = combinedView.views[filtersKey] as? PreferencesView, let filtersState = preferencesView.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState, !filtersState.filters.isEmpty { + for filter in filtersState.filters { + pinnedIds.formUnion(filter.data.includePeers.pinnedPeers) + } + if let topRootHole = combinedView.views[topRootHoleKey] as? AllChatListHolesView, let hole = topRootHole.latestHole { let entry = ChatListHolesEntry(groupId: .root, hole: hole) if !entries.contains(entry) { @@ -67,8 +84,8 @@ func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: Pee } } - let (removed, added) = state.with { state in - return state.update(entries: entries) + let (removed, added, addedPinnedIds) = state.with { state in + return state.update(entries: entries, pinnedIds: pinnedIds) } for disposable in removed { @@ -78,6 +95,22 @@ func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: Pee for (entry, disposable) in added { disposable.set(fetchChatListHole(postbox: postbox, network: network, accountPeerId: accountPeerId, groupId: entry.groupId, hole: entry.hole).start()) } + + if let (ids, disposable) = addedPinnedIds { + let signal = postbox.transaction { transaction -> [Api.InputPeer] in + var peers: [Api.InputPeer] = [] + for id in ids { + if let inputPeer = transaction.getPeer(id).flatMap(apiInputPeer) { + peers.append(inputPeer) + } + } + return peers + } + |> mapToSignal { inputPeers -> Signal in + return loadAndStorePeerChatInfos(accountPeerId: accountPeerId, postbox: postbox, network: network, peers: inputPeers) + } + disposable.set(signal.start()) + } }) return ActionDisposable { @@ -90,3 +123,144 @@ func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: Pee } } } + +private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox, network: Network, peers: [Api.InputPeer]) -> Signal { + let signal = network.request(Api.functions.messages.getPeerDialogs(peers: peers.map(Api.InputDialogPeer.inputDialogPeer(peer:)))) + |> map(Optional.init) + + return signal + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .complete() + } + + return postbox.transaction { transaction -> Void in + var peers: [Peer] = [] + var peerPresences: [PeerId: PeerPresence] = [:] + var notificationSettings: [PeerId: PeerNotificationSettings] = [:] + var channelStates: [PeerId: ChannelState] = [:] + + switch result { + case let .peerDialogs(dialogs, messages, chats, users, _): + for chat in chats { + if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { + peers.append(groupOrChannel) + } + } + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + if let presence = TelegramUserPresence(apiUser: user) { + peerPresences[telegramUser.id] = presence + } + } + + var topMessageIds = Set() + + for dialog in dialogs { + switch dialog { + case let .dialog(_, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, notifySettings, pts, _, folderId): + let peerId = peer.peerId + + if topMessage != 0 { + topMessageIds.insert(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: topMessage)) + } + + var isExcludedFromChatList = false + for chat in chats { + if chat.peerId == peerId { + if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { + if let group = groupOrChannel as? TelegramGroup { + if group.flags.contains(.deactivated) { + isExcludedFromChatList = true + } else { + switch group.membership { + case .Member: + break + default: + isExcludedFromChatList = true + } + } + } else if let channel = groupOrChannel as? TelegramChannel { + switch channel.participationStatus { + case .member: + break + default: + isExcludedFromChatList = true + } + } + } + break + } + } + + if !isExcludedFromChatList { + let groupId = PeerGroupId(rawValue: folderId ?? 0) + let currentInclusion = transaction.getPeerChatListInclusion(peerId) + var currentPinningIndex: UInt16? + var currentMinTimestamp: Int32? + switch currentInclusion { + case let .ifHasMessagesOrOneOf(currentGroupId, pinningIndex, minTimestamp): + if currentGroupId == groupId { + currentPinningIndex = pinningIndex + } + currentMinTimestamp = minTimestamp + default: + break + } + transaction.updatePeerChatListInclusion(peerId, inclusion: .ifHasMessagesOrOneOf(groupId: groupId, pinningIndex: currentPinningIndex, minTimestamp: currentMinTimestamp)) + } + + notificationSettings[peer.peerId] = TelegramPeerNotificationSettings(apiSettings: notifySettings) + + transaction.resetIncomingReadStates([peerId: [Namespaces.Message.Cloud: .idBased(maxIncomingReadId: readInboxMaxId, maxOutgoingReadId: readOutboxMaxId, maxKnownId: topMessage, count: unreadCount, markedUnread: false)]]) + + transaction.replaceMessageTagSummary(peerId: peerId, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, count: unreadMentionsCount, maxId: topMessage) + + if let pts = pts { + let channelState = ChannelState(pts: pts, invalidatedPts: pts) + transaction.setPeerChatState(peerId, state: channelState) + channelStates[peer.peerId] = channelState + } + case .dialogFolder: + assertionFailure() + break + } + } + + var storeMessages: [StoreMessage] = [] + for message in messages { + if let storeMessage = StoreMessage(apiMessage: message) { + var updatedStoreMessage = storeMessage + if case let .Id(id) = storeMessage.id { + if let channelState = channelStates[id.peerId] { + var updatedAttributes = storeMessage.attributes + updatedAttributes.append(ChannelMessageStateVersionAttribute(pts: channelState.pts)) + updatedStoreMessage = updatedStoreMessage.withUpdatedAttributes(updatedAttributes) + } + } + storeMessages.append(updatedStoreMessage) + } + } + + for message in storeMessages { + if case let .Id(id) = message.id { + let _ = transaction.addMessages([message], location: topMessageIds.contains(id) ? .UpperHistoryBlock : .Random) + } + } + } + + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + + updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) + + transaction.updateCurrentPeerNotificationSettings(notificationSettings) + } + |> ignoreValues + } +}