import Foundation import Postbox import TelegramCore import SwiftSignalKit import Display import TelegramUIPreferences import AccountContext enum ChatListNodeLocation: Equatable { case initial(count: Int, filter: ChatListFilter?) case navigation(index: EngineChatList.Item.Index, filter: ChatListFilter?) case scroll(index: EngineChatList.Item.Index, sourceIndex: EngineChatList.Item.Index, scrollPosition: ListViewScrollPosition, animated: Bool, filter: ChatListFilter?) var filter: ChatListFilter? { switch self { case let .initial(_, filter): return filter case let .navigation(_, filter): return filter case let .scroll(_, _, _, _, filter): return filter } } } struct ChatListNodeViewUpdate { let list: EngineChatList let type: ViewUpdateType let scrollPosition: ChatListNodeViewScrollPosition? } public func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilterPredicate { var includePeers = Set(filter.includePeers.peers) var excludePeers = Set(filter.excludePeers) if !filter.includePeers.pinnedPeers.isEmpty { includePeers.subtract(filter.includePeers.pinnedPeers) excludePeers.subtract(filter.includePeers.pinnedPeers) } var includeAdditionalPeerGroupIds: [PeerGroupId] = [] if !filter.excludeArchived { includeAdditionalPeerGroupIds.append(Namespaces.PeerGroup.archive) } var messageTagSummary: ChatListMessageTagSummaryResultCalculation? if filter.excludeRead || filter.excludeMuted { messageTagSummary = ChatListMessageTagSummaryResultCalculation(addCount: ChatListMessageTagSummaryResultComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), subtractCount: ChatListMessageTagActionsSummaryResultComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud)) } return ChatListFilterPredicate(includePeerIds: includePeers, excludePeerIds: excludePeers, pinnedPeerIds: filter.includePeers.pinnedPeers, messageTagSummary: messageTagSummary, includeAdditionalPeerGroupIds: includeAdditionalPeerGroupIds, include: { peer, isMuted, isUnread, isContact, messageTagSummaryResult in if filter.excludeRead { var effectiveUnread = isUnread if let messageTagSummaryResult = messageTagSummaryResult, messageTagSummaryResult { effectiveUnread = true } if !effectiveUnread { return false } } if filter.excludeMuted { if isMuted { if let messageTagSummaryResult = messageTagSummaryResult, messageTagSummaryResult { } else { return false } } } if !filter.categories.contains(.contacts) && isContact { if let user = peer as? TelegramUser { if user.botInfo == nil { return false } } else if let _ = peer as? TelegramSecretChat { return false } } if !filter.categories.contains(.nonContacts) && !isContact { if let user = peer as? TelegramUser { if user.botInfo == nil { return false } } else if let _ = peer as? TelegramSecretChat { return false } } if !filter.categories.contains(.bots) { if let user = peer as? TelegramUser { if user.botInfo != nil { return false } } } if !filter.categories.contains(.groups) { if let _ = peer as? TelegramGroup { return false } else if let channel = peer as? TelegramChannel { if case .group = channel.info { return false } } } if !filter.categories.contains(.channels) { if let channel = peer as? TelegramChannel { if case .broadcast = channel.info { return false } } } return true }) } func chatListViewForLocation(chatListLocation: ChatListControllerLocation, location: ChatListNodeLocation, account: Account) -> Signal { switch chatListLocation { case let .chatList(groupId): let filterPredicate: ChatListFilterPredicate? if let filter = location.filter, case let .filter(_, _, _, data) = filter { filterPredicate = chatListFilterPredicate(filter: data) } else { filterPredicate = nil } switch location { case let .initial(count, _): let signal: Signal<(ChatListView, ViewUpdateType), NoError> signal = account.viewTracker.tailChatListView(groupId: groupId._asGroup(), filterPredicate: filterPredicate, count: count) return signal |> map { view, updateType -> ChatListNodeViewUpdate in return ChatListNodeViewUpdate(list: EngineChatList(view), type: updateType, scrollPosition: nil) } case let .navigation(index, _): guard case let .chatList(index) = index else { return .never() } var first = true return account.viewTracker.aroundChatListView(groupId: groupId._asGroup(), filterPredicate: filterPredicate, index: index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in let genericType: ViewUpdateType if first { first = false genericType = ViewUpdateType.UpdateVisible } else { genericType = updateType } return ChatListNodeViewUpdate(list: EngineChatList(view), type: genericType, scrollPosition: nil) } case let .scroll(index, sourceIndex, scrollPosition, animated, _): guard case let .chatList(index) = index else { return .never() } let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > .chatList(index) ? .Down : .Up let chatScrollPosition: ChatListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true return account.viewTracker.aroundChatListView(groupId: groupId._asGroup(), filterPredicate: filterPredicate, index: index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in let genericType: ViewUpdateType let scrollPosition: ChatListNodeViewScrollPosition? = first ? chatScrollPosition : nil if first { first = false genericType = ViewUpdateType.UpdateVisible } else { genericType = updateType } return ChatListNodeViewUpdate(list: EngineChatList(view), type: genericType, scrollPosition: scrollPosition) } } case let .forum(peerId): let viewKey: PostboxViewKey = .messageHistoryThreadIndex( id: peerId, summaryComponents: ChatListEntrySummaryComponents( components: [ ChatListEntryMessageTagSummaryKey( tag: .unseenPersonalMessage, actionType: PendingMessageActionType.consumeUnseenPersonalMessage ): ChatListEntrySummaryComponents.Component( tagSummary: ChatListEntryMessageTagSummaryComponent(namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(namespace: Namespaces.Message.Cloud) ), ChatListEntryMessageTagSummaryKey( tag: .unseenReaction, actionType: PendingMessageActionType.readReaction ): ChatListEntrySummaryComponents.Component( tagSummary: ChatListEntryMessageTagSummaryComponent(namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(namespace: Namespaces.Message.Cloud) ) ] ) ) var isFirst = false return account.postbox.combinedView(keys: [viewKey]) |> map { views -> ChatListNodeViewUpdate in guard let view = views.views[viewKey] as? MessageHistoryThreadIndexView else { preconditionFailure() } var items: [EngineChatList.Item] = [] for item in view.items { guard let peer = view.peer else { continue } guard let data = item.info.get(MessageHistoryThreadData.self) else { continue } let defaultPeerNotificationSettings: TelegramPeerNotificationSettings = (view.peerNotificationSettings as? TelegramPeerNotificationSettings) ?? .defaultSettings var hasUnseenMentions = false var isMuted = false switch data.notificationSettings.muteState { case .muted: isMuted = true case .unmuted: isMuted = false case .default: if case .default = data.notificationSettings.muteState { if case .muted = defaultPeerNotificationSettings.muteState { isMuted = true } } } if let info = item.tagSummaryInfo[ChatListEntryMessageTagSummaryKey( tag: .unseenPersonalMessage, actionType: PendingMessageActionType.consumeUnseenPersonalMessage )] { hasUnseenMentions = (info.tagSummaryCount ?? 0) > (info.actionsSummaryCount ?? 0) } var hasUnseenReactions = false if let info = item.tagSummaryInfo[ChatListEntryMessageTagSummaryKey( tag: .unseenReaction, actionType: PendingMessageActionType.readReaction )] { hasUnseenReactions = (info.tagSummaryCount ?? 0) != 0// > (info.actionsSummaryCount ?? 0) } let pinnedIndex: EngineChatList.Item.PinnedIndex if let index = item.pinnedIndex { pinnedIndex = .index(index) } else { pinnedIndex = .none } let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: data.maxOutgoingReadId, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))]), isMuted: false) var draft: EngineChatList.Draft? if let embeddedState = item.embeddedInterfaceState, let _ = embeddedState.overrideChatTimestamp { if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) { if let text = opaqueState.synchronizeableInputState?.text { draft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? []) } } } items.append(EngineChatList.Item( id: .forum(item.id), index: .forum(pinnedIndex: pinnedIndex, timestamp: item.index.timestamp, threadId: item.id, namespace: item.index.id.namespace, id: item.index.id.id), messages: item.topMessage.flatMap { [EngineMessage($0)] } ?? [], readCounters: readCounters, isMuted: isMuted, draft: draft, threadData: data, renderedPeer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, hasUnseenMentions: hasUnseenMentions, hasUnseenReactions: hasUnseenReactions, forumTopicData: nil, topForumTopicItems: [], hasFailed: false, isContact: false )) } let list = EngineChatList( items: items.reversed(), groupItems: [], additionalItems: [], hasEarlier: false, hasLater: false, isLoading: view.isLoading ) let type: ViewUpdateType if isFirst { type = .Initial } else { type = .Generic } isFirst = false return ChatListNodeViewUpdate(list: list, type: type, scrollPosition: nil) } } }