import Foundation import Postbox import SwiftSignalKit import Display import AsyncDisplayKit import TelegramCore struct ChatHistoryGridViewTransition { let historyView: ChatHistoryView let topOffsetWithinMonth: Int let deleteItems: [Int] let insertItems: [GridNodeInsertItem] let updateItems: [GridNodeUpdateItem] let scrollToItem: GridNodeScrollToItem? let stationaryItems: GridNodeStationaryItems } private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionInsertEntry], theme: PresentationTheme, strings: PresentationStrings) -> [GridNodeInsertItem] { return entries.map { entry -> GridNodeInsertItem in switch entry.entry { case let .MessageEntry(message, _, _, _, _, _): return GridNodeInsertItem(index: entry.index, item: GridMessageItem(theme: theme, strings: strings, account: account, message: message, controllerInteraction: controllerInteraction), previousIndex: entry.previousIndex) case .MessageGroupEntry: return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) case .HoleEntry: return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) case .UnreadEntry: assertionFailure() return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) case .ChatInfoEntry, .EmptyChatInfoEntry, .SearchEntry: assertionFailure() return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) } } } private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionUpdateEntry], theme: PresentationTheme, strings: PresentationStrings) -> [GridNodeUpdateItem] { return entries.map { entry -> GridNodeUpdateItem in switch entry.entry { case let .MessageEntry(message, _, _, _, _, _): return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridMessageItem(theme: theme, strings: strings, account: account, message: message, controllerInteraction: controllerInteraction)) case .MessageGroupEntry: return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) case .HoleEntry: return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) case .UnreadEntry: assertionFailure() return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) case .ChatInfoEntry, .EmptyChatInfoEntry, .SearchEntry: assertionFailure() return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) } } } private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, transition: ChatHistoryViewTransition, from: ChatHistoryView?, presentationData: ChatPresentationData) -> ChatHistoryGridViewTransition { var mappedScrollToItem: GridNodeScrollToItem? if let scrollToItem = transition.scrollToItem { let mappedPosition: GridNodeScrollToItemPosition switch scrollToItem.position { case .top: mappedPosition = .top case .center: mappedPosition = .center case .bottom: mappedPosition = .bottom } let scrollTransition: ContainedViewLayoutTransition if scrollToItem.animated { switch scrollToItem.curve { case .Default: scrollTransition = .animated(duration: 0.3, curve: .easeInOut) case let .Spring(duration): scrollTransition = .animated(duration: duration, curve: .spring) } } else { scrollTransition = .immediate } let directionHint: GridNodePreviousItemsTransitionDirectionHint switch scrollToItem.directionHint { case .Up: directionHint = .up case .Down: directionHint = .down } mappedScrollToItem = GridNodeScrollToItem(index: scrollToItem.index, position: mappedPosition, transition: scrollTransition, directionHint: directionHint, adjustForSection: true, adjustForTopInset: true) } var stationaryItems: GridNodeStationaryItems = .none if let previousView = from { if let stationaryRange = transition.stationaryItemRange { var fromStableIds = Set() for i in 0 ..< previousView.filteredEntries.count { if i >= stationaryRange.0 && i <= stationaryRange.1 { fromStableIds.insert(previousView.filteredEntries[i].stableId) } } var index = 0 var indices = Set() for entry in transition.historyView.filteredEntries { if fromStableIds.contains(entry.stableId) { indices.insert(transition.historyView.filteredEntries.count - 1 - index) } index += 1 } stationaryItems = .indices(indices) } else { var fromStableIds = Set() for i in 0 ..< previousView.filteredEntries.count { fromStableIds.insert(previousView.filteredEntries[i].stableId) } var index = 0 var indices = Set() for entry in transition.historyView.filteredEntries { if fromStableIds.contains(entry.stableId) { indices.insert(transition.historyView.filteredEntries.count - 1 - index) } index += 1 } stationaryItems = .indices(indices) } } var topOffsetWithinMonth: Int = 0 if let lastEntry = transition.historyView.filteredEntries.last { switch lastEntry { case let .MessageEntry(_, _, _, monthLocation, _, _): if let monthLocation = monthLocation { topOffsetWithinMonth = Int(monthLocation.indexInMonth) } default: break } } return ChatHistoryGridViewTransition(historyView: transition.historyView, topOffsetWithinMonth: topOffsetWithinMonth, deleteItems: transition.deleteItems.map { $0.index }, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.insertEntries, theme: presentationData.theme, strings: presentationData.strings), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.updateEntries, theme: presentationData.theme, strings: presentationData.strings), scrollToItem: mappedScrollToItem, stationaryItems: stationaryItems) } private func itemSizeForContainerLayout(size: CGSize) -> CGSize { let side = floor(size.width / 4.0) return CGSize(width: side, height: side) } public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { private let account: Account private let peerId: PeerId private let messageId: MessageId? private let tagMask: MessageTags? private var historyView: ChatHistoryView? private let historyDisposable = MetaDisposable() private let messageViewQueue = Queue() private var dequeuedInitialTransitionOnLayout = false private var enqueuedHistoryViewTransition: (ChatHistoryGridViewTransition, () -> Void)? var layoutActionOnViewTransition: ((ChatHistoryGridViewTransition) -> (ChatHistoryGridViewTransition, ListViewUpdateSizeAndInsets?))? public let historyState = ValuePromise() private var currentHistoryState: ChatHistoryNodeHistoryState? public var preloadPages: Bool = true { didSet { if self.preloadPages != oldValue { } } } private let _chatHistoryLocation = ValuePromise(ignoreRepeated: true) private var chatHistoryLocation: Signal { return self._chatHistoryLocation.get() } private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private var presentationData: PresentationData private let chatPresentationDataPromise = Promise() public private(set) var loadState: ChatHistoryNodeLoadState? private var loadStateUpdated: ((ChatHistoryNodeLoadState, Bool) -> Void)? public init(account: Account, peerId: PeerId, messageId: MessageId?, tagMask: MessageTags?, controllerInteraction: ChatControllerInteraction) { self.account = account self.peerId = peerId self.messageId = messageId self.tagMask = tagMask self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } super.init() self.chatPresentationDataPromise.set(.single((ChatPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.fontSize, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, timeFormat: self.presentationData.timeFormat)))) self.floatingSections = true //self.preloadPages = false let messageViewQueue = self.messageViewQueue let historyViewUpdate = self.chatHistoryLocation |> distinctUntilChanged |> mapToSignal { location in return chatHistoryViewForLocation(location, account: account, chatLocation: .peer(peerId), fixedCombinedReadStates: nil, tagMask: tagMask, additionalData: [], orderStatistics: [.locationWithinMonth]) } let previousView = Atomic(value: nil) let historyViewTransition = combineLatest(historyViewUpdate, self.chatPresentationDataPromise.get()) |> mapToQueue { [weak self] update, chatPresentationData -> Signal in switch update { case .Loading: Queue.mainQueue().async { [weak self] in if let strongSelf = self { let loadState: ChatHistoryNodeLoadState = .loading if strongSelf.loadState != loadState { strongSelf.loadState = loadState strongSelf.loadStateUpdated?(loadState, false) } let historyState: ChatHistoryNodeHistoryState = .loading if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState strongSelf.historyState.set(historyState) } } } return .complete() case let .HistoryView(view, type, scrollPosition, _, _): let reason: ChatHistoryViewTransitionReason var prepareOnMainQueue = false switch type { case let .Initial(fadeIn): reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn) prepareOnMainQueue = !fadeIn case let .Generic(genericType): switch genericType { case .InitialUnread: reason = ChatHistoryViewTransitionReason.Initial(fadeIn: false) case .Generic: reason = ChatHistoryViewTransitionReason.InteractiveChanges case .UpdateVisible: reason = ChatHistoryViewTransitionReason.Reload case let .FillHole(insertions, deletions): reason = ChatHistoryViewTransitionReason.HoleChanges(filledHoleDirections: insertions, removeHoleDirections: deletions) } } let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(location: .peer(peerId), view: view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, includeSearchEntry: false, reverse: false, groupMessages: false, selectedMessages: nil, presentationData: chatPresentationData)) let previous = previousView.swap(processedView) return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: false, account: account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, presentationData: chatPresentationData) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } let appliedTransition = historyViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in if let strongSelf = self { return strongSelf.enqueueHistoryViewTransition(transition) } return .complete() } self.historyDisposable.set(appliedTransition.start()) if let messageId = messageId { self._chatHistoryLocation.set(ChatHistoryLocation.InitialSearch(location: .id(messageId), count: 100)) } else { self._chatHistoryLocation.set(ChatHistoryLocation.Initial(count: 100)) } self.visibleItemsUpdated = { [weak self] visibleItems in if let strongSelf = self, let historyView = strongSelf.historyView, let top = visibleItems.top, let bottom = visibleItems.bottom, let visibleTop = visibleItems.topVisible, let visibleBottom = visibleItems.bottomVisible { if top.0 < 5 && historyView.originalView.laterId != nil { let lastEntry = historyView.filteredEntries[historyView.filteredEntries.count - 1 - visibleTop.0] strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: 200)) } else if bottom.0 >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil { let firstEntry = historyView.filteredEntries[historyView.filteredEntries.count - 1 - visibleBottom.0] strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: 200)) } } } } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.historyDisposable.dispose() } public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) { self.loadStateUpdated = f } public func scrollToStartOfHistory() { self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true)) } public func scrollToEndOfHistory() { self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true)) } public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex) { self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .message(toIndex), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: .center(.bottom), animated: true)) } public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { if let historyView = self.historyView { for case let .MessageEntry(message, _, _, _, _, _) in historyView.filteredEntries where message.id == id { return message } } return nil } private func enqueueHistoryViewTransition(_ transition: ChatHistoryGridViewTransition) -> Signal { return Signal { [weak self] subscriber in if let strongSelf = self { if let _ = strongSelf.enqueuedHistoryViewTransition { preconditionFailure() } strongSelf.enqueuedHistoryViewTransition = (transition, { subscriber.putCompletion() }) if strongSelf.isNodeLoaded { strongSelf.dequeueHistoryViewTransition() } else { let loadState: ChatHistoryNodeLoadState if transition.historyView.filteredEntries.isEmpty { loadState = .empty } else { loadState = .messages } if strongSelf.loadState != loadState { strongSelf.loadState = loadState strongSelf.loadStateUpdated?(loadState, false) } let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState strongSelf.historyState.set(historyState) } } } else { subscriber.putCompletion() } return EmptyDisposable } |> runOn(Queue.mainQueue()) } private func dequeueHistoryViewTransition() { if let (transition, completion) = self.enqueuedHistoryViewTransition { self.enqueuedHistoryViewTransition = nil let completion: (GridNodeDisplayedItemRange) -> Void = { [weak self] visibleRange in if let strongSelf = self { strongSelf.historyView = transition.historyView if let range = visibleRange.loadedRange { strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.upperBound].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lowerBound].index) } let loadState: ChatHistoryNodeLoadState if let historyView = strongSelf.historyView { if historyView.filteredEntries.isEmpty { loadState = .empty } else { loadState = .messages } } else { loadState = .loading } if strongSelf.loadState != loadState { strongSelf.loadState = loadState strongSelf.loadStateUpdated?(loadState, false) } let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState strongSelf.historyState.set(historyState) } completion() } } if let layoutActionOnViewTransition = self.layoutActionOnViewTransition { self.layoutActionOnViewTransition = nil let (mappedTransition, updateSizeAndInsets) = layoutActionOnViewTransition(transition) var updateLayout: GridNodeUpdateLayout? if let updateSizeAndInsets = updateSizeAndInsets { updateLayout = GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, type: .fixed(itemSize: CGSize(width: 200.0, height: 200.0), lineSpacing: 0.0)), transition: .immediate) } self.transaction(GridNodeTransaction(deleteItems: mappedTransition.deleteItems, insertItems: mappedTransition.insertItems, updateItems: mappedTransition.updateItems, scrollToItem: mappedTransition.scrollToItem, updateLayout: updateLayout, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: mappedTransition.topOffsetWithinMonth), completion: completion) } else { self.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.topOffsetWithinMonth), completion: completion) } } } public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) { self.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, type: .fixed(itemSize: itemSizeForContainerLayout(size: updateSizeAndInsets.size), lineSpacing: 0.0)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in }) if !self.dequeuedInitialTransitionOnLayout { self.dequeuedInitialTransitionOnLayout = true self.dequeueHistoryViewTransition() } } public func disconnect() { self.historyDisposable.set(nil) } }