import Foundation import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore import Postbox enum ChatListNodeMode { case chatList case peers } 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 ChatListNodeInteraction { let activateSearch: () -> Void let peerSelected: (Peer) -> Void let messageSelected: (Message) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let setPeerPinned: (PeerId, Bool) -> Void let setPeerMuted: (PeerId, Bool) -> Void let deletePeer: (PeerId) -> Void init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer) -> Void, messageSelected: @escaping (Message) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setPeerPinned: @escaping (PeerId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void) { self.activateSearch = activateSearch self.peerSelected = peerSelected self.messageSelected = messageSelected self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.setPeerPinned = setPeerPinned self.setPeerMuted = setPeerMuted self.deletePeer = deletePeer } } struct ChatListNodeState: Equatable { let theme: PresentationTheme let strings: PresentationStrings let editing: Bool let peerIdWithRevealedOptions: PeerId? func withUpdatedPresentationData(theme: PresentationTheme, strings: PresentationStrings) -> ChatListNodeState { return ChatListNodeState(theme: theme, strings: strings, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions) } func withUpdatedEditing(_ editing: Bool) -> ChatListNodeState { return ChatListNodeState(theme: self.theme, strings: self.strings, editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions) } func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChatListNodeState { return ChatListNodeState(theme: self.theme, strings: self.strings, editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions) } static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } if lhs.editing != rhs.editing { return false } if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions { return false } return true } } private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { case let .SearchEntry(theme, text): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: text, activate: { nodeInteraction.activateSearch() }), directionHint: entry.directionHint) case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, editing, hasActiveRevealControls): switch mode { case .chatList: return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(theme: theme, strings: strings, account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) case .peers: var peer: Peer? var chatPeer: Peer? if let message = message { peer = messageMainPeer(message) chatPeer = message.peers[message.id.peerId] } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, 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(), directionHint: entry.directionHint) } } } private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .SearchEntry(theme, text): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: text, activate: { nodeInteraction.activateSearch() }), directionHint: entry.directionHint) case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, editing, hasActiveRevealControls): switch mode { case .chatList: return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(theme: theme, strings: strings, account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) case .peers: var peer: Peer? var chatPeer: Peer? if let message = message { peer = messageMainPeer(message) chatPeer = message.peers[message.id.peerId] } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, 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(), directionHint: entry.directionHint) } } } private func mappedChatListNodeViewListTransition(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, nodeInteraction: nodeInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, nodeInteraction: nodeInteraction, 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 } } final class ChatListNode: ListView { private let _ready = ValuePromise() private var didSetReady = false var ready: Signal { return _ready.get() } var peerSelected: ((PeerId) -> Void)? var activateSearch: (() -> Void)? var deletePeerChat: ((PeerId) -> Void)? var presentAlert: ((String) -> Void)? private let viewProcessingQueue = Queue() private var chatListView: ChatListNodeView? private var dequeuedInitialTransitionOnLayout = false private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)? private var currentState: ChatListNodeState private let statePromise: ValuePromise private var currentLocation: ChatListNodeLocation? private let chatListLocation = ValuePromise() private let chatListDisposable = MetaDisposable() init(account: Account, mode: ChatListNodeMode, theme: PresentationTheme, strings: PresentationStrings) { self.currentState = ChatListNodeState(theme: theme, strings: strings, editing: false, peerIdWithRevealedOptions: nil) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) super.init() self.backgroundColor = theme.chatList.backgroundColor 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) } }, messageSelected: { [weak self] message in if let strongSelf = self, let peerSelected = strongSelf.peerSelected { peerSelected(message.id.peerId) } }, setPeerIdWithRevealedOptions: { [weak self] peerId, fromPeerId in if let strongSelf = self { strongSelf.updateState { state in if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { return state.withUpdatedPeerIdWithRevealedOptions(peerId) } else { return state } } } }, setPeerPinned: { peerId, _ in let _ = (togglePeerChatPinned(postbox: account.postbox, peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { switch result { case .done: break case .limitExceeded: strongSelf.presentAlert?(strongSelf.currentState.strings.DialogList_PinLimitError("5").0) } } }) }, setPeerMuted: { peerId, _ in let _ = togglePeerMuted(account: account, peerId: peerId).start() }, deletePeer: { [weak self] peerId in self?.deletePeerChat?(peerId) }) let viewProcessingQueue = self.viewProcessingQueue let chastListViewUpdate = self.chatListLocation.get() |> distinctUntilChanged |> mapToSignal { location in return chatListViewForLocation(location, account: account) } let previousView = Atomic(value: nil) let chatListNodeViewTransition = combineLatest(chastListViewUpdate, self.statePromise.get()) |> mapToQueue { (update, state) -> Signal in let processedView = ChatListNodeView(originalView: update.view, filteredEntries: chatListNodeEntriesForView(update.view, state: state)) let previous = previousView.swap(processedView) let reason: ChatListNodeViewTransitionReason var prepareOnMainQueue = false var previousWasEmptyOrSingleHole = false if let previous = previous { if previous.filteredEntries.count == 1 { if case .HoleEntry = previous.filteredEntries[0] { previousWasEmptyOrSingleHole = true } } } else { previousWasEmptyOrSingleHole = true } if previousWasEmptyOrSingleHole { reason = .initial if previous == nil { prepareOnMainQueue = true } } else { switch update.type { case .InitialUnread: reason = .initial prepareOnMainQueue = true case .Generic: reason = .interactiveChanges case .UpdateVisible: reason = .reload case .FillHole: reason = .reload } } return preparedChatListNodeViewTransition(from: previous, to: processedView, reason: reason, account: account, scrollPosition: update.scrollPosition) |> map({ mappedChatListNodeViewListTransition(account: account, nodeInteraction: nodeInteraction, 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 range = range.loadedRange, let view = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView.originalView { var location: ChatListNodeLocation? if range.firstIndex < 5 && view.laterIndex != nil { location = .navigation(index: view.entries[view.entries.count - 1].index) } else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlierIndex != nil { location = .navigation(index: view.entries[0].index) } if let location = location, location != strongSelf.currentLocation { strongSelf.currentLocation = location strongSelf.chatListLocation.set(location) } } } self.chatListDisposable.set(appliedTransition.start()) let initialLocation: ChatListNodeLocation = .initial(count: 50) self.currentLocation = initialLocation self.chatListLocation.set(initialLocation) } deinit { self.chatListDisposable.dispose() } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { if theme !== self.currentState.theme || strings !== self.currentState.strings { self.backgroundColor = theme.chatList.backgroundColor self.updateState { return $0.withUpdatedPresentationData(theme: theme, strings: strings) } } } 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.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 if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) } completion() } } self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatListOpaqueTransactionState(chatListView: transition.chatListView), completion: completion) } } 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() } } func scrollToLatest() { if let view = self.chatListView?.originalView, view.laterIndex == nil { self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { let location: ChatListNodeLocation = .scroll(index: ChatListIndex.absoluteUpperBound, sourceIndex: ChatListIndex.absoluteLowerBound , scrollPosition: .Top, animated: true) self.currentLocation = location self.chatListLocation.set(location) } } }