import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore import Postbox import TelegramPresentationData import TelegramUIPreferences import AccountContext import TelegramNotices import ContactsPeerItem import ContextUI public enum ChatListNodeMode { case chatList case peers(filter: ChatListNodePeersFilter) } struct ChatListNodeListViewTransition { let chatListView: ChatListNodeView let deleteItems: [ListViewDeleteItem] let insertItems: [ListViewInsertItem] let updateItems: [ListViewUpdateItem] let options: ListViewDeleteAndInsertOptions let scrollToItem: ListViewScrollToItem? let stationaryItemRange: (Int, Int)? } final class ChatListHighlightedLocation { let location: ChatLocation let progress: CGFloat init(location: ChatLocation, progress: CGFloat) { self.location = location self.progress = progress } func withUpdatedProgress(_ progress: CGFloat) -> ChatListHighlightedLocation { return ChatListHighlightedLocation(location: location, progress: progress) } } public final class ChatListNodeInteraction { let activateSearch: () -> Void let peerSelected: (Peer) -> Void let togglePeerSelected: (PeerId) -> Void let messageSelected: (Peer, Message, Bool) -> Void let groupSelected: (PeerGroupId) -> Void let addContact: (String) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let setItemPinned: (PinnedItemId, Bool) -> Void let setPeerMuted: (PeerId, Bool) -> Void let deletePeer: (PeerId) -> Void let updatePeerGrouping: (PeerId, Bool) -> Void let togglePeerMarkedUnread: (PeerId, Bool) -> Void let toggleArchivedFolderHiddenByDefault: () -> Void let activateChatPreview: (ChatListItem, ASDisplayNode, ContextGesture?) -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (PeerId) -> Void, messageSelected: @escaping (Peer, Message, Bool) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void) { self.activateSearch = activateSearch self.peerSelected = peerSelected self.togglePeerSelected = togglePeerSelected self.messageSelected = messageSelected self.groupSelected = groupSelected self.addContact = addContact self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.setItemPinned = setItemPinned self.setPeerMuted = setPeerMuted self.deletePeer = deletePeer self.updatePeerGrouping = updatePeerGrouping self.togglePeerMarkedUnread = togglePeerMarkedUnread self.toggleArchivedFolderHiddenByDefault = toggleArchivedFolderHiddenByDefault self.activateChatPreview = activateChatPreview } } public final class ChatListNodePeerInputActivities { public let activities: [PeerId: [(Peer, PeerInputActivity)]] public init(activities: [PeerId: [(Peer, PeerInputActivity)]]) { self.activities = activities } } public struct ChatListNodeState: Equatable { public var presentationData: ChatListPresentationData public var editing: Bool public var peerIdWithRevealedOptions: PeerId? public var selectedPeerIds: Set public var peerInputActivities: ChatListNodePeerInputActivities? public var pendingRemovalPeerIds: Set public var pendingClearHistoryPeerIds: Set public var archiveShouldBeTemporaryRevealed: Bool public init(presentationData: ChatListPresentationData, editing: Bool, peerIdWithRevealedOptions: PeerId?, selectedPeerIds: Set, peerInputActivities: ChatListNodePeerInputActivities?, pendingRemovalPeerIds: Set, pendingClearHistoryPeerIds: Set, archiveShouldBeTemporaryRevealed: Bool) { self.presentationData = presentationData self.editing = editing self.peerIdWithRevealedOptions = peerIdWithRevealedOptions self.selectedPeerIds = selectedPeerIds self.peerInputActivities = peerInputActivities self.pendingRemovalPeerIds = pendingRemovalPeerIds self.pendingClearHistoryPeerIds = pendingClearHistoryPeerIds self.archiveShouldBeTemporaryRevealed = archiveShouldBeTemporaryRevealed } public static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool { if lhs.presentationData !== rhs.presentationData { return false } if lhs.editing != rhs.editing { return false } if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions { return false } if lhs.selectedPeerIds != rhs.selectedPeerIds { return false } if lhs.peerInputActivities !== rhs.peerInputActivities { return false } if lhs.pendingRemovalPeerIds != rhs.pendingRemovalPeerIds { return false } if lhs.pendingClearHistoryPeerIds != rhs.pendingClearHistoryPeerIds { return false } if lhs.archiveShouldBeTemporaryRevealed != rhs.archiveShouldBeTemporaryRevealed { return false } return true } } private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, peerGroupId: PeerGroupId, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd): switch mode { case .chatList: return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd, ignoreUnreadBadge: false, displayAsMessage: false), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint) case let .peers(filter): let itemPeer = peer.chatMainPeer var chatPeer: Peer? if let peer = peer.peers[peer.peerId] { chatPeer = peer } var enabled = true if filter.contains(.onlyWriteable) { if let peer = peer.peers[peer.peerId] { if !canSendMessagesToPeer(peer) { enabled = false } } else { enabled = false } } if filter.contains(.onlyPrivateChats) { if let peer = peer.peers[peer.peerId] { if !(peer is TelegramUser || peer is TelegramSecretChat) { enabled = false } } else { enabled = false } } if filter.contains(.onlyGroups) { if let peer = peer.peers[peer.peerId] { if let _ = peer as? TelegramGroup { } else if let peer = peer as? TelegramChannel, case .group = peer.info { } else { enabled = false } } else { enabled = false } } if filter.contains(.onlyManageable) { if let peer = peer.peers[peer.peerId] { var canManage = false if let peer = peer as? TelegramGroup { switch peer.role { case .creator, .admin: canManage = true default: break } } if canManage { } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.inviteMembers) { } else { enabled = false } } else { enabled = false } } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, account: context.account, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } }), directionHint: entry.directionHint) } case let .HoleEntry(_, theme): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) case let .GroupReferenceEntry(index, presentationData, groupId, peers, message, editing, unreadState, revealed, hiddenByDefault): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, index: index, content: .groupReference(groupId: groupId, peers: peers, message: message, unreadState: unreadState, hiddenByDefault: hiddenByDefault), editing: editing, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: true, hiddenOffset: hiddenByDefault && !revealed, interaction: nodeInteraction), directionHint: entry.directionHint) case let .ArchiveIntro(presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) } } } private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, peerGroupId: PeerGroupId, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd): switch mode { case .chatList: return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd, ignoreUnreadBadge: false, displayAsMessage: false), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint) case let .peers(filter): let itemPeer = peer.chatMainPeer var chatPeer: Peer? if let peer = peer.peers[peer.peerId] { chatPeer = peer } var enabled = true if filter.contains(.onlyWriteable) { if let peer = peer.peers[peer.peerId] { if !canSendMessagesToPeer(peer) { enabled = false } } else { enabled = false } } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, account: context.account, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } }), directionHint: entry.directionHint) } case let .HoleEntry(_, theme): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) case let .GroupReferenceEntry(index, presentationData, groupId, peers, message, editing, unreadState, revealed, hiddenByDefault): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, index: index, content: .groupReference(groupId: groupId, peers: peers, message: message, unreadState: unreadState, hiddenByDefault: hiddenByDefault), editing: editing, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: true, hiddenOffset: hiddenByDefault && !revealed, interaction: nodeInteraction), directionHint: entry.directionHint) case let .ArchiveIntro(presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) } } } private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, peerGroupId: PeerGroupId, mode: ChatListNodeMode, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, peerGroupId: peerGroupId, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, peerGroupId: peerGroupId, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange) } private final class ChatListOpaqueTransactionState { let chatListView: ChatListNodeView init(chatListView: ChatListNodeView) { self.chatListView = chatListView } } public enum ChatListSelectionOption { case previous(unread: Bool) case next(unread: Bool) case peerId(PeerId) case index(Int) } public enum ChatListGlobalScrollOption { case none case top case unread } private struct ChatListVisibleUnreadCounts: Equatable { var raw: Int32 = 0 var filtered: Int32 = 0 } public enum ChatListNodeScrollPosition { case auto case autoUp case top } public enum ChatListNodeEmptyState: Equatable { case notEmpty(containsChats: Bool) case empty(isLoading: Bool) } public final class ChatListNode: ListView { private let controlsHistoryPreload: Bool private let context: AccountContext private let groupId: PeerGroupId private let mode: ChatListNodeMode private let _ready = ValuePromise() private var didSetReady = false public var ready: Signal { return _ready.get() } public var peerSelected: ((PeerId, Bool, Bool) -> Void)? public var groupSelected: ((PeerGroupId) -> Void)? public var addContact: ((String) -> Void)? public var activateSearch: (() -> Void)? public var deletePeerChat: ((PeerId) -> Void)? public var updatePeerGrouping: ((PeerId, Bool) -> Void)? public var presentAlert: ((String) -> Void)? public var toggleArchivedFolderHiddenByDefault: (() -> Void)? public var activateChatPreview: ((ChatListItem, ASDisplayNode, ContextGesture?) -> Void)? private var theme: PresentationTheme private let viewProcessingQueue = Queue() private var chatListView: ChatListNodeView? private var interaction: ChatListNodeInteraction? private var dequeuedInitialTransitionOnLayout = false private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)? private(set) var currentState: ChatListNodeState private let statePromise: ValuePromise public var state: Signal { return self.statePromise.get() } private var currentLocation: ChatListNodeLocation? private let chatListLocation = ValuePromise() private let chatListDisposable = MetaDisposable() private var activityStatusesDisposable: Disposable? private let scrollToTopOptionPromise = Promise(.none) public var scrollToTopOption: Signal { return self.scrollToTopOptionPromise.get() } private let scrolledAtTop = ValuePromise(true) private var scrolledAtTopValue: Bool = true { didSet { if self.scrolledAtTopValue != oldValue { self.scrolledAtTop.set(self.scrolledAtTopValue) } } } public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? public var contentScrollingEnded: ((ListView) -> Bool)? private let visibleUnreadCounts = ValuePromise(ChatListVisibleUnreadCounts()) private var visibleUnreadCountsValue = ChatListVisibleUnreadCounts() { didSet { if self.visibleUnreadCountsValue != oldValue { self.visibleUnreadCounts.set(self.visibleUnreadCountsValue) } } } public var isEmptyUpdated: ((ChatListNodeEmptyState) -> Void)? private var currentIsEmptyState: ChatListNodeEmptyState? public var addedVisibleChatsWithPeerIds: (([PeerId]) -> Void)? private let currentRemovingPeerId = Atomic(value: nil) public func setCurrentRemovingPeerId(_ peerId: PeerId?) { let _ = self.currentRemovingPeerId.swap(peerId) } private var hapticFeedback: HapticFeedback? public init(context: AccountContext, groupId: PeerGroupId, controlsHistoryPreload: Bool, mode: ChatListNodeMode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { self.context = context self.groupId = groupId self.controlsHistoryPreload = controlsHistoryPreload self.mode = mode self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: false, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) self.theme = theme super.init() self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor self.verticalScrollIndicatorFollowsOverscroll = true let nodeInteraction = ChatListNodeInteraction(activateSearch: { [weak self] in if let strongSelf = self, let activateSearch = strongSelf.activateSearch { activateSearch() } }, peerSelected: { [weak self] peer in if let strongSelf = self, let peerSelected = strongSelf.peerSelected { peerSelected(peer.id, true, false) } }, togglePeerSelected: { [weak self] peerId in self?.updateState { state in var state = state if state.selectedPeerIds.contains(peerId) { state.selectedPeerIds.remove(peerId) } else { if state.selectedPeerIds.count < 100 { state.selectedPeerIds.insert(peerId) } } return state } }, messageSelected: { [weak self] peer, message, isAd in if let strongSelf = self, let peerSelected = strongSelf.peerSelected { peerSelected(peer.id, true, isAd) } }, groupSelected: { [weak self] groupId in if let strongSelf = self, let groupSelected = strongSelf.groupSelected { groupSelected(groupId) } }, addContact: { _ in }, setPeerIdWithRevealedOptions: { [weak self] peerId, fromPeerId in if let strongSelf = self { strongSelf.updateState { state in if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) || (peerId == nil && fromPeerId == nil) { var state = state state.peerIdWithRevealedOptions = peerId return state } else { return state } } } }, setItemPinned: { [weak self] itemId, _ in let _ = (toggleItemPinned(postbox: context.account.postbox, groupId: groupId, itemId: itemId) |> deliverOnMainQueue).start(next: { result in if let strongSelf = self { switch result { case .done: break case let .limitExceeded(maxCount): strongSelf.presentAlert?(strongSelf.currentState.presentationData.strings.DialogList_PinLimitError("\(maxCount)").0) } } }) }, setPeerMuted: { [weak self] peerId, _ in let _ = (togglePeerMuted(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: { self?.updateState { state in var state = state state.peerIdWithRevealedOptions = nil return state } }) }, deletePeer: { [weak self] peerId in self?.deletePeerChat?(peerId) }, updatePeerGrouping: { [weak self] peerId, group in self?.updatePeerGrouping?(peerId, group) }, togglePeerMarkedUnread: { [weak self, weak context] peerId, animated in guard let context = context else { return } let _ = (togglePeerUnreadMarkInteractively(postbox: context.account.postbox, viewTracker: context.account.viewTracker, peerId: peerId) |> deliverOnMainQueue).start(completed: { self?.updateState { state in var state = state state.peerIdWithRevealedOptions = nil return state } }) }, toggleArchivedFolderHiddenByDefault: { [weak self] in self?.toggleArchivedFolderHiddenByDefault?() }, activateChatPreview: { [weak self] item, node, gesture in guard let strongSelf = self else { return } if let activateChatPreview = strongSelf.activateChatPreview { activateChatPreview(item, node, gesture) } else { gesture?.cancel() } }) let viewProcessingQueue = self.viewProcessingQueue let chatListViewUpdate = self.chatListLocation.get() |> distinctUntilChanged |> mapToSignal { location in return chatListViewForLocation(groupId: groupId, location: location, account: context.account) } let previousState = Atomic(value: self.currentState) let previousView = Atomic(value: nil) let previousHideArchivedFolderByDefault = Atomic(value: nil) let currentRemovingPeerId = self.currentRemovingPeerId let savedMessagesPeer: Signal if case let .peers(filter) = mode, filter.contains(.onlyWriteable) { savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> map(Optional.init) } else { savedMessagesPeer = .single(nil) } let hideArchivedFolderByDefault = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatArchiveSettings]) |> map { view -> Bool in let settings: ChatArchiveSettings = view.values[ApplicationSpecificPreferencesKeys.chatArchiveSettings] as? ChatArchiveSettings ?? .default return settings.isHiddenByDefault } |> distinctUntilChanged let displayArchiveIntro: Signal if Namespaces.PeerGroup.archive == groupId { displayArchiveIntro = context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.archiveIntroDismissedKey()) |> map { entry -> Bool in if let value = entry.value as? ApplicationSpecificVariantNotice { return !value.value } else { return true } } |> take(1) |> afterNext { value in Queue.mainQueue().async { if value { let _ = (context.sharedContext.accountManager.transaction { transaction -> Void in ApplicationSpecificNotice.setArchiveIntroDismissed(transaction: transaction, value: true) }).start() } } } } else { displayArchiveIntro = .single(false) } let currentPeerId: PeerId = context.account.peerId let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue, hideArchivedFolderByDefault, displayArchiveIntro, savedMessagesPeer, chatListViewUpdate, self.statePromise.get()) |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, savedMessagesPeer, update, state) -> Signal in let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) let (rawEntries, isLoading) = chatListNodeEntriesForView(update.view, state: state, savedMessagesPeer: savedMessagesPeer, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, mode: mode) let entries = rawEntries.filter { entry in switch entry { case let .PeerEntry(_, _, _, _, _, _, peer, _, _, _, _, _, _, _): switch mode { case .chatList: return true case let .peers(filter): guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false } guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false } guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser else { return false } if filter.contains(.onlyGroups) { var isGroup: Bool = false if let peer = peer.chatMainPeer as? TelegramChannel, case .group = peer.info { isGroup = true } else if peer.peerId.namespace == Namespaces.Peer.CloudGroup { isGroup = true } if !isGroup { return false } } if filter.contains(.onlyChannels) { if let peer = peer.chatMainPeer as? TelegramChannel, case .broadcast = peer.info { return true } else { return false } } if filter.contains(.onlyWriteable) && filter.contains(.excludeDisabled) { if let peer = peer.peers[peer.peerId] { if !canSendMessagesToPeer(peer) { return false } } else { return false } } return true } default: return true } } let processedView = ChatListNodeView(originalView: update.view, filteredEntries: entries, isLoading: isLoading) let previousView = previousView.swap(processedView) let previousState = previousState.swap(state) let reason: ChatListNodeViewTransitionReason var prepareOnMainQueue = false var previousWasEmptyOrSingleHole = false if let previous = previousView { if previous.filteredEntries.count == 1 { if case .HoleEntry = previous.filteredEntries[0] { previousWasEmptyOrSingleHole = true } } else if previous.filteredEntries.isEmpty && previous.isLoading { previousWasEmptyOrSingleHole = true } } else { previousWasEmptyOrSingleHole = true } var updatedScrollPosition = update.scrollPosition if previousWasEmptyOrSingleHole { reason = .initial if previousView == nil { prepareOnMainQueue = true } } else { if previousView?.originalView === update.view { reason = .interactiveChanges updatedScrollPosition = nil } else { switch update.type { case .InitialUnread, .Initial: reason = .initial prepareOnMainQueue = true case .Generic: reason = .interactiveChanges case .UpdateVisible: reason = .reload case .FillHole: reason = .reload } } } let removingPeerId = currentRemovingPeerId.with { $0 } var disableAnimations = state.presentationData.disableAnimations if previousState.editing != state.editing { disableAnimations = false } else { var previousPinnedChats: [PeerId] = [] var updatedPinnedChats: [PeerId] = [] var didIncludeRemovingPeerId = false var didIncludeHiddenByDefaultArchive = false if let previous = previousView { for entry in previous.filteredEntries { if case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _) = entry { if index.pinningIndex != nil { previousPinnedChats.append(index.messageIndex.id.peerId) } if index.messageIndex.id.peerId == removingPeerId { didIncludeRemovingPeerId = true } } else if case let .GroupReferenceEntry(entry) = entry { didIncludeHiddenByDefaultArchive = entry.hiddenByDefault } } } var doesIncludeRemovingPeerId = false var doesIncludeArchive = false var doesIncludeHiddenByDefaultArchive = false for entry in processedView.filteredEntries { if case let .PeerEntry(index, _, _, _, _, _, _, _, _ , _, _, _, _, _) = entry { if index.pinningIndex != nil { updatedPinnedChats.append(index.messageIndex.id.peerId) } if index.messageIndex.id.peerId == removingPeerId { doesIncludeRemovingPeerId = true } } else if case let .GroupReferenceEntry(entry) = entry { doesIncludeArchive = true doesIncludeHiddenByDefaultArchive = entry.hiddenByDefault } } if previousPinnedChats != updatedPinnedChats { disableAnimations = false } if previousState.selectedPeerIds != state.selectedPeerIds { disableAnimations = false } if doesIncludeRemovingPeerId != didIncludeRemovingPeerId { disableAnimations = false } if hideArchivedFolderByDefault && previousState.archiveShouldBeTemporaryRevealed != state.archiveShouldBeTemporaryRevealed && doesIncludeArchive { disableAnimations = false } if didIncludeHiddenByDefaultArchive != doesIncludeHiddenByDefaultArchive { disableAnimations = false } } if let _ = previousHideArchivedFolderByDefaultValue, previousHideArchivedFolderByDefaultValue != hideArchivedFolderByDefault { disableAnimations = false } var searchMode = false if case .peers = mode { searchMode = true } return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode) |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, peerGroupId: groupId, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } let appliedTransition = chatListNodeViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in if let strongSelf = self { return strongSelf.enqueueTransition(transition) } return .complete() } self.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in if let strongSelf = self, let chatListView = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView { let originalView = chatListView.originalView if let range = range.loadedRange { var location: ChatListNodeLocation? if range.firstIndex < 5 && originalView.laterIndex != nil { location = .navigation(index: originalView.entries[originalView.entries.count - 1].index) } else if range.firstIndex >= 5 && range.lastIndex >= originalView.entries.count - 5 && originalView.earlierIndex != nil { location = .navigation(index: originalView.entries[0].index) } if let location = location, location != strongSelf.currentLocation { strongSelf.setChatListLocation(location) } strongSelf.enqueueHistoryPreloadUpdate() } var rawUnreadCount: Int32 = 0 var filteredUnreadCount: Int32 = 0 var archiveVisible = false if let range = range.visibleRange { let entryCount = chatListView.filteredEntries.count for i in range.firstIndex ..< range.lastIndex { if i < 0 || i >= entryCount { assertionFailure() continue } switch chatListView.filteredEntries[entryCount - i - 1] { case let .PeerEntry(_, _, _, readState, notificationSettings, _, _, _, _, _, _, _, _, _): if let readState = readState { let count = readState.count rawUnreadCount += count if let notificationSettings = notificationSettings, !notificationSettings.isRemovedFromTotalUnreadCount { filteredUnreadCount += count } } case .GroupReferenceEntry: archiveVisible = true default: break } } } var visibleUnreadCountsValue = strongSelf.visibleUnreadCountsValue visibleUnreadCountsValue.raw = rawUnreadCount visibleUnreadCountsValue.filtered = filteredUnreadCount strongSelf.visibleUnreadCountsValue = visibleUnreadCountsValue if !archiveVisible && strongSelf.currentState.archiveShouldBeTemporaryRevealed { strongSelf.updateState { state in var state = state state.archiveShouldBeTemporaryRevealed = false return state } } } } self.interaction = nodeInteraction self.chatListDisposable.set(appliedTransition.start()) let initialLocation: ChatListNodeLocation switch mode { case .chatList: initialLocation = .initial(count: 50) case .peers: initialLocation = .initial(count: 200) } self.setChatListLocation(initialLocation) let postbox = context.account.postbox let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) let previousActivities = Atomic(value: nil) self.activityStatusesDisposable = (context.account.allPeerInputActivities() |> mapToSignal { activitiesByPeerId -> Signal<[PeerId: [(Peer, PeerInputActivity)]], NoError> in var foundAllPeers = true var cachedResult: [PeerId: [(Peer, PeerInputActivity)]] = [:] previousPeerCache.with { dict -> Void in for (chatPeerId, activities) in activitiesByPeerId { var cachedChatResult: [(Peer, PeerInputActivity)] = [] for (peerId, activity) in activities { if let peer = dict[peerId] { cachedChatResult.append((peer, activity)) } else { foundAllPeers = false break } cachedResult[chatPeerId] = cachedChatResult } } } if foundAllPeers { return .single(cachedResult) } else { return postbox.transaction { transaction -> [PeerId: [(Peer, PeerInputActivity)]] in var result: [PeerId: [(Peer, PeerInputActivity)]] = [:] var peerCache: [PeerId: Peer] = [:] for (chatPeerId, activities) in activitiesByPeerId { var chatResult: [(Peer, PeerInputActivity)] = [] for (peerId, activity) in activities { if let peer = transaction.getPeer(peerId) { chatResult.append((peer, activity)) peerCache[peerId] = peer } } result[chatPeerId] = chatResult } let _ = previousPeerCache.swap(peerCache) return result } } } |> map { activities -> ChatListNodePeerInputActivities? in return previousActivities.modify { current in var updated = false let currentList: [PeerId: [(Peer, PeerInputActivity)]] = current?.activities ?? [:] if currentList.count != activities.count { updated = true } else { outer: for (peerId, currentValue) in currentList { if let value = activities[peerId] { if currentValue.count != value.count { updated = true break outer } else { for i in 0 ..< currentValue.count { if !arePeersEqual(currentValue[i].0, value[i].0) { updated = true break outer } if currentValue[i].1 != value[i].1 { updated = true break outer } } } } else { updated = true break outer } } } if updated { if activities.isEmpty { return nil } else { return ChatListNodePeerInputActivities(activities: activities) } } else { return current } } } |> deliverOnMainQueue).start(next: { [weak self] activities in if let strongSelf = self { strongSelf.updateState { state in var state = state state.peerInputActivities = activities return state } } }) self.reorderItem = { [weak self] fromIndex, toIndex, transactionOpaqueState -> Signal in if let strongSelf = self, let filteredEntries = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView.filteredEntries { if fromIndex >= 0 && fromIndex < filteredEntries.count && toIndex >= 0 && toIndex < filteredEntries.count { let fromEntry = filteredEntries[filteredEntries.count - 1 - fromIndex] let toEntry = filteredEntries[filteredEntries.count - 1 - toIndex] var referenceId: PinnedItemId? var beforeAll = false switch toEntry { case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, isAd): if isAd { beforeAll = true } else { referenceId = .peer(index.messageIndex.id.peerId) } /*case let .GroupReferenceEntry(_, _, groupId, _, _, _, _): referenceId = .group(groupId)*/ default: break } if let _ = fromEntry.sortIndex.pinningIndex { return strongSelf.context.account.postbox.transaction { transaction -> Bool in var itemIds = transaction.getPinnedItemIds(groupId: groupId) var itemId: PinnedItemId? switch fromEntry { case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _): itemId = .peer(index.messageIndex.id.peerId) /*case let .GroupReferenceEntry(_, _, groupId, _, _, _, _): itemId = .group(groupId)*/ default: break } if let itemId = itemId { itemIds = itemIds.filter({ $0 != itemId }) if let referenceId = referenceId { var inserted = false for i in 0 ..< itemIds.count { if itemIds[i] == referenceId { if fromIndex < toIndex { itemIds.insert(itemId, at: i + 1) } else { itemIds.insert(itemId, at: i) } inserted = true break } } if !inserted { itemIds.append(itemId) } } else if beforeAll { itemIds.insert(itemId, at: 0) } else { itemIds.append(itemId) } return reorderPinnedItemIds(transaction: transaction, groupId: groupId, itemIds: itemIds) } else { return false } } } } } return .single(false) } var startedScrollingAtUpperBound = false self.beganInteractiveDragging = { [weak self] in guard let strongSelf = self else { return } switch strongSelf.visibleContentOffset() { case .none, .unknown: startedScrollingAtUpperBound = false case let .known(value): startedScrollingAtUpperBound = value <= 0.0 } if strongSelf.currentState.peerIdWithRevealedOptions != nil { strongSelf.updateState { state in var state = state state.peerIdWithRevealedOptions = nil return state } } } self.didEndScrolling = { [weak self] in guard let strongSelf = self else { return } startedScrollingAtUpperBound = false let _ = strongSelf.contentScrollingEnded?(strongSelf) let revealHiddenItems: Bool switch strongSelf.visibleContentOffset() { case .none, .unknown: revealHiddenItems = false case let .known(value): revealHiddenItems = value <= 54.0 } if !revealHiddenItems && strongSelf.currentState.archiveShouldBeTemporaryRevealed { strongSelf.updateState { state in var state = state state.archiveShouldBeTemporaryRevealed = false return state } } } self.scrollToTopOptionPromise.set(combineLatest( renderedTotalUnreadCount(accountManager: self.context.sharedContext.accountManager, postbox: self.context.account.postbox) |> deliverOnMainQueue, self.visibleUnreadCounts.get(), self.scrolledAtTop.get() ) |> map { badge, visibleUnreadCounts, scrolledAtTop -> ChatListGlobalScrollOption in if scrolledAtTop { if badge.0 != 0 { switch badge.1 { case .raw: if visibleUnreadCounts.raw < badge.0 { return .unread } case .filtered: if visibleUnreadCounts.filtered < badge.0 { return .unread } } return .none } else { return .none } } else { return .top } }) self.visibleContentOffsetChanged = { [weak self] offset in guard let strongSelf = self else { return } let atTop: Bool var revealHiddenItems: Bool = false switch offset { case .none, .unknown: atTop = false case let .known(value): atTop = value <= 0.0 if startedScrollingAtUpperBound && strongSelf.isTracking { revealHiddenItems = value <= -60.0 } } strongSelf.scrolledAtTopValue = atTop strongSelf.contentOffsetChanged?(offset) if revealHiddenItems && !strongSelf.currentState.archiveShouldBeTemporaryRevealed { var isHiddenArchiveVisible = false strongSelf.forEachItemNode({ itemNode in if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item { if case let .groupReference(groupReference) = item.content { if groupReference.hiddenByDefault { isHiddenArchiveVisible = true } } } }) if isHiddenArchiveVisible { if strongSelf.hapticFeedback == nil { strongSelf.hapticFeedback = HapticFeedback() } strongSelf.hapticFeedback?.impact(.medium) strongSelf.updateState { state in var state = state state.archiveShouldBeTemporaryRevealed = true return state } } } } } deinit { self.chatListDisposable.dispose() self.activityStatusesDisposable?.dispose() } public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { if theme !== self.currentState.presentationData.theme || strings !== self.currentState.presentationData.strings || dateTimeFormat != self.currentState.presentationData.dateTimeFormat || disableAnimations != self.currentState.presentationData.disableAnimations { self.theme = theme if self.keepTopItemOverscrollBackground != nil { self.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: theme.chatList.pinnedItemBackgroundColor, direction: true) } self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor self.updateState { state in var state = state state.presentationData = ChatListPresentationData(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations) return state } } } public func updateState(_ f: (ChatListNodeState) -> ChatListNodeState) { let state = f(self.currentState) if state != self.currentState { self.currentState = state self.statePromise.set(state) } } private func enqueueTransition(_ transition: ChatListNodeListViewTransition) -> Signal { return Signal { [weak self] subscriber in if let strongSelf = self { if let _ = strongSelf.enqueuedTransition { preconditionFailure() } strongSelf.enqueuedTransition = (transition, { subscriber.putCompletion() }) if strongSelf.isNodeLoaded, strongSelf.dequeuedInitialTransitionOnLayout { strongSelf.dequeueTransition() } else { if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) } } } else { subscriber.putCompletion() } return EmptyDisposable } |> runOn(Queue.mainQueue()) } private func dequeueTransition() { if let (transition, completion) = self.enqueuedTransition { self.enqueuedTransition = nil let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in if let strongSelf = self { strongSelf.chatListView = transition.chatListView var pinnedOverscroll = false if case .chatList = strongSelf.mode { let entryCount = transition.chatListView.filteredEntries.count if entryCount >= 1 { if transition.chatListView.filteredEntries[entryCount - 1].sortIndex.pinningIndex != nil { pinnedOverscroll = true } } } if pinnedOverscroll != (strongSelf.keepTopItemOverscrollBackground != nil) { if pinnedOverscroll { strongSelf.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: strongSelf.theme.chatList.pinnedItemBackgroundColor, direction: true) } else { strongSelf.keepTopItemOverscrollBackground = nil } } if let scrollToItem = transition.scrollToItem, case .center = scrollToItem.position { if let itemNode = strongSelf.itemNodeAtIndex(scrollToItem.index) as? ChatListItemNode { itemNode.flashHighlight() } } if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) } var isEmpty = false if transition.chatListView.filteredEntries.isEmpty { isEmpty = true } else { if transition.chatListView.filteredEntries.count == 1 { if case .GroupReferenceEntry = transition.chatListView.filteredEntries[0] { isEmpty = true } } } let isEmptyState: ChatListNodeEmptyState if transition.chatListView.isLoading { isEmptyState = .empty(isLoading: true) } else if isEmpty { isEmptyState = .empty(isLoading: false) } else { var containsChats = false loop: for entry in transition.chatListView.filteredEntries { switch entry { case .GroupReferenceEntry, .HoleEntry, .PeerEntry: containsChats = true break loop case .ArchiveIntro: break } } isEmptyState = .notEmpty(containsChats: containsChats) } if strongSelf.currentIsEmptyState != isEmptyState { strongSelf.currentIsEmptyState = isEmptyState strongSelf.isEmptyUpdated?(isEmptyState) } var insertedPeerIds: [PeerId] = [] for item in transition.insertItems { if let item = item.item as? ChatListItem { switch item.content { case let .peer(peer): insertedPeerIds.append(peer.peer.peerId) case .groupReference: break } } } if !insertedPeerIds.isEmpty { strongSelf.addedVisibleChatsWithPeerIds?(insertedPeerIds) } completion() } } var options = transition.options if !options.contains(.AnimateInsertion) { options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousResourceLoading) } if options.contains(.AnimateCrossfade) && !self.isDeceleratingAfterTracking { options.insert(.PreferSynchronousDrawing) } self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatListOpaqueTransactionState(chatListView: transition.chatListView), completion: completion) } } public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) { self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !self.dequeuedInitialTransitionOnLayout { self.dequeuedInitialTransitionOnLayout = true self.dequeueTransition() } } public func scrollToPosition(_ position: ChatListNodeScrollPosition) { if let view = self.chatListView?.originalView { if case .auto = position { switch self.visibleContentOffset() { case .none, .unknown: if let maxVisibleChatListIndex = self.currentlyVisibleLatestChatListIndex() { self.scrollToEarliestUnread(earlierThan: maxVisibleChatListIndex) return } case let .known(offset): if offset <= 0.0 { self.scrollToEarliestUnread(earlierThan: nil) return } else { if let maxVisibleChatListIndex = self.currentlyVisibleLatestChatListIndex() { self.scrollToEarliestUnread(earlierThan: maxVisibleChatListIndex) return } } } } else if case .autoUp = position, let maxVisibleChatListIndex = self.currentlyVisibleLatestChatListIndex() { self.scrollToEarliestUnread(earlierThan: maxVisibleChatListIndex) return } if view.laterIndex == nil { self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { let location: ChatListNodeLocation = .scroll(index: .absoluteUpperBound, sourceIndex: .absoluteLowerBound , scrollPosition: .top(0.0), animated: true) self.setChatListLocation(location) } } else { let location: ChatListNodeLocation = .scroll(index: .absoluteUpperBound, sourceIndex: .absoluteLowerBound , scrollPosition: .top(0.0), animated: true) self.setChatListLocation(location) } } private func setChatListLocation(_ location: ChatListNodeLocation) { self.currentLocation = location self.chatListLocation.set(location) } private func relativeUnreadChatListIndex(position: ChatListRelativePosition) -> Signal { let groupId = self.groupId let postbox = self.context.account.postbox return self.context.sharedContext.accountManager.transaction { transaction -> Signal in var filter = true if let inAppNotificationSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.inAppNotificationSettings) as? InAppNotificationSettings { switch inAppNotificationSettings.totalUnreadCountDisplayStyle { case .raw: filter = false case .filtered: filter = true } } return postbox.transaction { transaction -> ChatListIndex? in return transaction.getRelativeUnreadChatListIndex(filtered: filter, position: position, groupId: groupId) } } |> switchToLatest } public func scrollToEarliestUnread(earlierThan: ChatListIndex?) { let _ = (relativeUnreadChatListIndex(position: .earlier(than: earlierThan)) |> deliverOnMainQueue).start(next: { [weak self] index in guard let strongSelf = self else { return } if let index = index { let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: self?.currentlyVisibleLatestChatListIndex() ?? .absoluteUpperBound , scrollPosition: .center(.top), animated: true) strongSelf.setChatListLocation(location) } else { let location: ChatListNodeLocation = .scroll(index: .absoluteUpperBound, sourceIndex: .absoluteLowerBound , scrollPosition: .top(0.0), animated: true) strongSelf.setChatListLocation(location) } }) } public func selectChat(_ option: ChatListSelectionOption) { guard let interaction = self.interaction else { return } guard let chatListView = (self.opaqueTransactionState as? ChatListOpaqueTransactionState)?.chatListView else { return } guard let range = self.displayedItemRange.loadedRange else { return } let entryCount = chatListView.filteredEntries.count var current: (ChatListIndex, PeerId, Int)? = nil var previous: (ChatListIndex, PeerId)? = nil var next: (ChatListIndex, PeerId)? = nil outer: for i in range.firstIndex ..< range.lastIndex { if i < 0 || i >= entryCount { assertionFailure() continue } switch chatListView.filteredEntries[entryCount - i - 1] { case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _): if interaction.highlightedChatLocation?.location == ChatLocation.peer(peer.peerId) { current = (index, peer.peerId, entryCount - i - 1) break outer } default: break } } switch option { case .previous(unread: true), .next(unread: true): let position: ChatListRelativePosition if let current = current { if case .previous = option { position = .earlier(than: current.0) } else { position = .later(than: current.0) } } else { position = .later(than: nil) } let _ = (relativeUnreadChatListIndex(position: position) |> deliverOnMainQueue).start(next: { [weak self] index in guard let strongSelf = self, let index = index else { return } let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: strongSelf.currentlyVisibleLatestChatListIndex() ?? .absoluteUpperBound, scrollPosition: .center(.top), animated: true) strongSelf.setChatListLocation(location) strongSelf.peerSelected?(index.messageIndex.id.peerId, false, false) }) case .previous(unread: false), .next(unread: false): var target: (ChatListIndex, PeerId)? = nil if let current = current, entryCount > 1 { if current.2 > 0, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 - 1] { next = (index, peer.peerId) } if current.2 <= entryCount - 2, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 + 1] { previous = (index, peer.peerId) } if case .previous = option { target = previous } else { target = next } } else if entryCount > 0 { if case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _) = chatListView.filteredEntries[entryCount - 1] { target = (index, peer.peerId) } } if let target = target { let location: ChatListNodeLocation = .scroll(index: target.0, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true) self.setChatListLocation(location) self.peerSelected?(target.1, false, false) } case let .peerId(peerId): self.peerSelected?(peerId, false, false) case let .index(index): guard index < 10 else { return } let _ = (chatListViewForLocation(groupId: self.groupId, location: .initial(count: 10), account: self.context.account) |> take(1) |> deliverOnMainQueue).start(next: { update in let entries = update.view.entries if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _) = entries[10 - index - 1] { let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true) self.setChatListLocation(location) self.peerSelected?(renderedPeer.peerId, false, false) } }) break } } private func enqueueHistoryPreloadUpdate() { } public func updateSelectedChatLocation(_ chatLocation: ChatLocation?, progress: CGFloat, transition: ContainedViewLayoutTransition) { guard let interaction = self.interaction else { return } if let chatLocation = chatLocation { interaction.highlightedChatLocation = ChatListHighlightedLocation(location: chatLocation, progress: progress) } else { interaction.highlightedChatLocation = nil } self.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatListItemNode { itemNode.updateIsHighlighted(transition: transition) } } } private func currentlyVisibleLatestChatListIndex() -> ChatListIndex? { guard let chatListView = (self.opaqueTransactionState as? ChatListOpaqueTransactionState)?.chatListView else { return nil } if let range = self.displayedItemRange.visibleRange { let entryCount = chatListView.filteredEntries.count for i in range.firstIndex ..< range.lastIndex { if i < 0 || i >= entryCount { assertionFailure() continue } switch chatListView.filteredEntries[entryCount - i - 1] { case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _): return index default: break } } } return nil } }