import Foundation import Postbox import SwiftSignalKit import Display import AsyncDisplayKit import TelegramCore private let historyMessageCount: Int = 200 public enum ChatHistoryListMode: Equatable { case bubbles case list(search: Bool, reversed: Bool) public static func ==(lhs: ChatHistoryListMode, rhs: ChatHistoryListMode) -> Bool { switch lhs { case .bubbles: if case .bubbles = rhs { return true } else { return false } case let .list(search, reversed): if case .list(search, reversed) = rhs { return true } else { return false } } } } enum ChatHistoryViewScrollPosition { case unread(index: MessageIndex) case positionRestoration(index: MessageIndex, relativeOffset: CGFloat) case index(index: MessageHistoryAnchorIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) } enum ChatHistoryViewUpdateType { case Initial(fadeIn: Bool) case Generic(type: ViewUpdateType) } public struct ChatHistoryCombinedInitialReadStateData { public let unreadCount: Int32 public let totalState: ChatListTotalUnreadState? public let inAppNotificationSettings: InAppNotificationSettings? public let notificationSettings: PeerNotificationSettings? } public struct ChatHistoryCombinedInitialData { let initialData: InitialMessageHistoryData? let buttonKeyboardMessage: Message? let cachedData: CachedPeerData? let cachedDataMessages: [MessageId: Message]? let readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? } enum ChatHistoryViewUpdate { case Loading(initialData: ChatHistoryCombinedInitialData?) case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, originalScrollPosition: ChatHistoryViewScrollPosition?, initialData: ChatHistoryCombinedInitialData) } struct ChatHistoryView { let originalView: MessageHistoryView let filteredEntries: [ChatHistoryEntry] let associatedData: ChatMessageItemAssociatedData } enum ChatHistoryViewTransitionReason { case Initial(fadeIn: Bool) case InteractiveChanges case HoleChanges(filledHoleDirections: [MessageIndex: HoleFillDirection], removeHoleDirections: [MessageIndex: HoleFillDirection]) case Reload } struct ChatHistoryViewTransitionInsertEntry { let index: Int let previousIndex: Int? let entry: ChatHistoryEntry let directionHint: ListViewItemOperationDirectionHint? } struct ChatHistoryViewTransitionUpdateEntry { let index: Int let previousIndex: Int let entry: ChatHistoryEntry let directionHint: ListViewItemOperationDirectionHint? } struct ChatHistoryViewTransition { let historyView: ChatHistoryView let deleteItems: [ListViewDeleteItem] let insertEntries: [ChatHistoryViewTransitionInsertEntry] let updateEntries: [ChatHistoryViewTransitionUpdateEntry] let options: ListViewDeleteAndInsertOptions let scrollToItem: ListViewScrollToItem? let stationaryItemRange: (Int, Int)? let initialData: InitialMessageHistoryData? let keyboardButtonsMessage: Message? let cachedData: CachedPeerData? let cachedDataMessages: [MessageId: Message]? let readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? let scrolledToIndex: MessageHistoryAnchorIndex? let animateIn: Bool } struct ChatHistoryListViewTransition { let historyView: ChatHistoryView let deleteItems: [ListViewDeleteItem] let insertItems: [ListViewInsertItem] let updateItems: [ListViewUpdateItem] let options: ListViewDeleteAndInsertOptions let scrollToItem: ListViewScrollToItem? let stationaryItemRange: (Int, Int)? let initialData: InitialMessageHistoryData? let keyboardButtonsMessage: Message? let cachedData: CachedPeerData? let cachedDataMessages: [MessageId: Message]? let readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? let scrolledToIndex: MessageHistoryAnchorIndex? let animateIn: Bool } private func maxMessageIndexForEntries(_ view: ChatHistoryView, indexRange: (Int, Int)) -> (incoming: MessageIndex?, overall: MessageIndex?) { var incoming: MessageIndex? var overall: MessageIndex? var nextLowestIndex: MessageIndex? if indexRange.0 >= 0 && indexRange.0 < view.filteredEntries.count { if indexRange.0 > 0 { nextLowestIndex = view.filteredEntries[indexRange.0 - 1].index } } var nextHighestIndex: MessageIndex? if indexRange.1 >= 0 && indexRange.1 < view.filteredEntries.count { if indexRange.1 < view.filteredEntries.count - 1 { nextHighestIndex = view.filteredEntries[indexRange.1 + 1].index } } for i in (0 ..< view.originalView.entries.count).reversed() { let index = view.originalView.entries[i].index if let nextLowestIndex = nextLowestIndex { if index <= nextLowestIndex { continue } } if let nextHighestIndex = nextHighestIndex { if index >= nextHighestIndex { continue } } if case let .MessageEntry(messageEntry) = view.originalView.entries[i] { if overall == nil || overall! < index { overall = index } if messageEntry.0.flags.contains(.Incoming) { if incoming == nil || incoming! < index { incoming = index } } if incoming != nil { return (incoming, overall) } } } return (incoming, overall) } private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { case let .MessageEntry(message, presentationData, read, _, selection, isAdmin): let item: ListViewItem switch mode { case .bubbles: item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) case let .list(search, _): item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: search) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .MessageGroupEntry(_, messages, presentationData): let item: ListViewItem switch mode { case .bubbles: item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages)) case let .list(search, _): assertionFailure() item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: search) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .HoleEntry(_, presentationData): let item: ListViewItem switch mode { case .bubbles: item = ChatHoleItem(index: entry.entry.index, presentationData: presentationData) case .list: item = ListMessageHoleItem() } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .UnreadEntry(_, presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, presentationData: presentationData), directionHint: entry.directionHint) case let .ChatInfoEntry(text, presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(text: text, controllerInteraction: controllerInteraction, presentationData: presentationData), directionHint: entry.directionHint) case let .SearchEntry(theme, strings): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { controllerInteraction.openSearch() }), directionHint: entry.directionHint) } } } private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .MessageEntry(message, presentationData, read, _, selection, isAdmin): let item: ListViewItem switch mode { case .bubbles: item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) case let .list(search, _): item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: search) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .MessageGroupEntry(_, messages, presentationData): let item: ListViewItem switch mode { case .bubbles: item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages)) case let .list(search, _): assertionFailure() item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: search) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .HoleEntry(_, presentationData): let item: ListViewItem switch mode { case .bubbles: item = ChatHoleItem(index: entry.entry.index, presentationData: presentationData) case .list: item = ListMessageHoleItem() } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .UnreadEntry(_, presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, presentationData: presentationData), directionHint: entry.directionHint) case let .ChatInfoEntry(text, presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(text: text, controllerInteraction: controllerInteraction, presentationData: presentationData), directionHint: entry.directionHint) case let .SearchEntry(theme, strings): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { controllerInteraction.openSearch() }), directionHint: entry.directionHint) } } } private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, animateIn: transition.animateIn) } private final class ChatHistoryTransactionOpaqueState { let historyView: ChatHistoryView init(historyView: ChatHistoryView) { self.historyView = historyView } } private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: AutomaticDownloadNetworkType) -> ChatMessageItemAssociatedData { var automaticMediaDownloadPeerType: AutomaticMediaDownloadPeerType = .channel if case let .peer(peerId) = chatLocation { if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { var isContact = false for entry in view.additionalData { if case let .peerIsContact(_, value) = entry { isContact = value break } } automaticMediaDownloadPeerType = isContact ? .contact : .otherPrivate } else if peerId.namespace == Namespaces.Peer.CloudGroup { automaticMediaDownloadPeerType = .group } else if peerId.namespace == Namespaces.Peer.CloudChannel { for entry in view.additionalData { if case let .peer(_, value) = entry { if let channel = value as? TelegramChannel, case .group = channel.info { automaticMediaDownloadPeerType = .group } break } } } } let associatedData = ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false) return associatedData } public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let context: AccountContext private let chatLocation: ChatLocation private let messageId: MessageId? private let tagMask: MessageTags? private let controllerInteraction: ChatControllerInteraction private let mode: ChatHistoryListMode private var historyView: ChatHistoryView? private let historyDisposable = MetaDisposable() private let readHistoryDisposable = MetaDisposable() private let messageViewQueue = Queue() private var dequeuedInitialTransitionOnLayout = false private var enqueuedHistoryViewTransition: (ChatHistoryListViewTransition, () -> Void)? var layoutActionOnViewTransition: ((ChatHistoryListViewTransition) -> (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?))? public let historyState = ValuePromise() public var currentHistoryState: ChatHistoryNodeHistoryState? private let _initialData = Promise() private var didSetInitialData = false public var initialData: Signal { return self._initialData.get() } private let _cachedPeerDataAndMessages = Promise<(CachedPeerData?, [MessageId: Message]?)>() public var cachedPeerDataAndMessages: Signal<(CachedPeerData?, [MessageId: Message]?), NoError> { return self._cachedPeerDataAndMessages.get() } private var _buttonKeyboardMessage = Promise(nil) private var currentButtonKeyboardMessage: Message? public var buttonKeyboardMessage: Signal { return self._buttonKeyboardMessage.get() } private let maxVisibleIncomingMessageIndex = ValuePromise(ignoreRepeated: true) let canReadHistory = Promise() private var canReadHistoryValue: Bool = false private var canReadHistoryDisposable: Disposable? private let _chatHistoryLocation = ValuePromise() private var chatHistoryLocation: Signal { return self._chatHistoryLocation.get() } private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let messageProcessingManager = ChatMessageThrottledProcessingManager() private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager() private let messageMentionProcessingManager = ChatMessageThrottledProcessingManager(delay: 0.2) private var maxVisibleMessageIndexReported: MessageIndex? var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)? var scrolledToIndex: ((MessageHistoryAnchorIndex) -> Void)? private var currentPresentationData: PresentationData private var chatPresentationDataPromise: Promise private var presentationDataDisposable: Disposable? private let historyAppearsClearedPromise = ValuePromise(false) var historyAppearsCleared: Bool = false { didSet { if self.historyAppearsCleared != oldValue { self.historyAppearsClearedPromise.set(self.historyAppearsCleared) } } } private(set) var isScrollAtBottomPosition = false public var isScrollAtBottomPositionUpdated: (() -> Void)? private var interactiveReadActionDisposable: Disposable? public var contentPositionChanged: (ListViewVisibleContentOffset) -> Void = { _ in } public private(set) var loadState: ChatHistoryNodeLoadState? private var loadStateUpdated: ((ChatHistoryNodeLoadState, Bool) -> Void)? private var loadedMessagesFromCachedDataDisposable: Disposable? public init(context: AccountContext, chatLocation: ChatLocation, tagMask: MessageTags?, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles) { self.context = context self.chatLocation = chatLocation self.messageId = messageId self.tagMask = tagMask self.controllerInteraction = controllerInteraction self.mode = mode self.currentPresentationData = context.currentPresentationData.with { $0 } self.chatPresentationDataPromise = Promise(ChatPresentationData(theme: ChatPresentationThemeData(theme: self.currentPresentationData.theme, wallpaper: self.currentPresentationData.chatWallpaper), fontSize: self.currentPresentationData.fontSize, strings: self.currentPresentationData.strings, dateTimeFormat: self.currentPresentationData.dateTimeFormat, nameDisplayOrder: self.currentPresentationData.nameDisplayOrder, disableAnimations: self.currentPresentationData.disableAnimations)) super.init() self.dynamicBounceEnabled = !self.currentPresentationData.disableAnimations //self.debugInfo = true self.messageProcessingManager.process = { [weak context] messageIds in context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds) } self.unsupportedMessageProcessingManager.process = { [weak context] messageIds in context?.account.viewTracker.updateUnsupportedMediaForMessageIds(messageIds: messageIds) } self.messageMentionProcessingManager.process = { [weak context] messageIds in context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds) } self.preloadPages = false switch self.mode { case .bubbles: self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) case .list: break } //self.snapToBottomInsetUntilFirstInteraction = true let messageViewQueue = self.messageViewQueue let fixedCombinedReadStates = Atomic(value: nil) var additionalData: [AdditionalMessageHistoryViewData] = [] if case let .peer(peerId) = chatLocation { additionalData.append(.cachedPeerData(peerId)) additionalData.append(.cachedPeerDataMessages(peerId)) additionalData.append(.peerNotificationSettings(peerId)) additionalData.append(.preferencesEntry(ApplicationSpecificPreferencesKeys.inAppNotificationSettings)) if peerId.namespace == Namespaces.Peer.CloudChannel { additionalData.append(.cacheEntry(cachedChannelAdminIdsEntryId(peerId: peerId))) additionalData.append(.peer(peerId)) } if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { additionalData.append(.peerIsContact(peerId)) } } additionalData.append(.totalUnreadState) let historyViewUpdate = self.chatHistoryLocation |> distinctUntilChanged |> mapToSignal { location in return chatHistoryViewForLocation(location, account: context.account, chatLocation: chatLocation, fixedCombinedReadStates: fixedCombinedReadStates.with { $0 }, tagMask: tagMask, additionalData: additionalData) |> beforeNext { viewUpdate in switch viewUpdate { case let .HistoryView(view, _, _, _, _): let _ = fixedCombinedReadStates.swap(view.fixedReadStates) default: break } } } let previousView = Atomic(value: nil) let automaticDownloadNetworkType = context.account.networkType |> map { type -> AutomaticDownloadNetworkType in switch type { case .none, .wifi: return .wifi case .cellular: return .cellular } } |> distinctUntilChanged let previousHistoryAppearsCleared = Atomic(value: nil) let historyViewTransition = combineLatest(historyViewUpdate, self.chatPresentationDataPromise.get(), selectedMessages, automaticDownloadNetworkType, self.historyAppearsClearedPromise.get()) |> mapToQueue { [weak self] update, chatPresentationData, selectedMessages, networkType, historyAppearsCleared -> Signal in let initialData: ChatHistoryCombinedInitialData? switch update { case let .Loading(combinedInitialData): initialData = combinedInitialData Queue.mainQueue().async { [weak self] in if let strongSelf = self { if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true strongSelf._initialData.set(.single(combinedInitialData)) } strongSelf._cachedPeerDataAndMessages.set(.single((nil, nil))) 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, originalScrollPosition, data): initialData = data var updatedScrollPosition = scrollPosition var reverse = false var includeSearchEntry = false if case let .list(search, reverseValue) = mode { includeSearchEntry = search reverse = reverseValue } let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType) let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(location: chatLocation, view: view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData, historyAppearsCleared: historyAppearsCleared), associatedData: associatedData) let previous = previousView.swap(processedView) if scrollPosition == nil, let originalScrollPosition = originalScrollPosition { switch originalScrollPosition { case let .index(index, position, _, _): if case .upperBound = index { if let previous = previous, previous.filteredEntries.count == 1, case .HoleEntry = previous.filteredEntries[0] { updatedScrollPosition = .index(index: index, position: position, directionHint: .Down, animated: false) } } default: break } } let reason: ChatHistoryViewTransitionReason var prepareOnMainQueue = false let previousHistoryAppearsClearedValue = previousHistoryAppearsCleared.swap(historyAppearsCleared) if previousHistoryAppearsClearedValue != nil && previousHistoryAppearsClearedValue != historyAppearsCleared && !historyAppearsCleared { reason = ChatHistoryViewTransitionReason.Initial(fadeIn: !processedView.filteredEntries.isEmpty) } else if let previous = previous, previous.originalView.entries == processedView.originalView.entries { reason = ChatHistoryViewTransitionReason.InteractiveChanges updatedScrollPosition = nil } else { 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) } } } return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData) |> map({ mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> 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()) let previousMaxIncomingMessageIndexByNamespace = Atomic<[MessageId.Namespace: MessageIndex]>(value: [:]) let readHistory = combineLatest(self.maxVisibleIncomingMessageIndex.get(), self.canReadHistory.get()) |> map { messageIndex, canRead in if canRead { var apply = false let _ = previousMaxIncomingMessageIndexByNamespace.modify { dict in let previousIndex = dict[messageIndex.id.namespace] if previousIndex == nil || previousIndex! < messageIndex { apply = true var dict = dict dict[messageIndex.id.namespace] = messageIndex return dict } return dict } if apply { switch chatLocation { case .peer: let _ = applyMaxReadIndexInteractively(postbox: context.account.postbox, stateManager: context.account.stateManager, index: messageIndex).start() case let .group(groupId): let _ = context.account.postbox.transaction({ transaction -> Void in transaction.applyGroupFeedInteractiveReadMaxIndex(groupId: groupId, index: messageIndex) }).start() } } } } self.readHistoryDisposable.set(readHistory.start()) self.canReadHistoryDisposable = (self.canReadHistory.get() |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { if strongSelf.canReadHistoryValue != value { strongSelf.canReadHistoryValue = value strongSelf.updateReadHistoryActions() } } }) if let messageId = messageId { self._chatHistoryLocation.set(ChatHistoryLocation.InitialSearch(location: .id(messageId), count: 60)) } else { self._chatHistoryLocation.set(ChatHistoryLocation.Initial(count: 60)) } self.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in if let strongSelf = self { if let historyView = (opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView { if let visible = displayedRange.visibleRange { let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex) var readIndexRange = (0, historyView.filteredEntries.count - 1 - visible.firstIndex) /*if !visible.firstIndexFullyVisible { readIndexRange.1 -= 1 }*/ var messageIdsWithViewCount: [MessageId] = [] var messageIdsWithUnsupportedMedia: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = [] for i in (indexRange.0 ... indexRange.1) { switch historyView.filteredEntries[i] { case let .MessageEntry(message, _, _, _, _, _): var hasUnconsumedMention = false var hasUnconsumedContent = false if message.tags.contains(.unseenPersonalMessage) { for attribute in message.attributes { if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending { hasUnconsumedMention = true } } } for attribute in message.attributes { if attribute is ViewCountMessageAttribute { if message.id.namespace == Namespaces.Message.Cloud { messageIdsWithViewCount.append(message.id) } } else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed { hasUnconsumedContent = true } } for media in message.media { if let _ = media as? TelegramMediaUnsupported { messageIdsWithUnsupportedMedia.append(message.id) } } if hasUnconsumedMention && !hasUnconsumedContent { messageIdsWithUnseenPersonalMention.append(message.id) } case let .MessageGroupEntry(_, messages, _): for (message, _, _, _) in messages { var hasUnconsumedMention = false var hasUnconsumedContent = false if message.tags.contains(.unseenPersonalMessage) { for attribute in message.attributes { if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending { hasUnconsumedMention = true } } } for attribute in message.attributes { if attribute is ViewCountMessageAttribute { if message.id.namespace == Namespaces.Message.Cloud { messageIdsWithViewCount.append(message.id) } } else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed { hasUnconsumedContent = true } } if hasUnconsumedMention && !hasUnconsumedContent { messageIdsWithUnseenPersonalMention.append(message.id) } } default: break } } if !messageIdsWithViewCount.isEmpty { strongSelf.messageProcessingManager.add(messageIdsWithViewCount) } if !messageIdsWithUnsupportedMedia.isEmpty { strongSelf.unsupportedMessageProcessingManager.add(messageIdsWithUnsupportedMedia) } if !messageIdsWithUnseenPersonalMention.isEmpty { strongSelf.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention) } if readIndexRange.0 <= readIndexRange.1 { let (maxIncomingIndex, maxOverallIndex) = maxMessageIndexForEntries(historyView, indexRange: readIndexRange) if let maxIncomingIndex = maxIncomingIndex { strongSelf.updateMaxVisibleReadIncomingMessageIndex(maxIncomingIndex) } if let maxOverallIndex = maxOverallIndex, maxOverallIndex != strongSelf.maxVisibleMessageIndexReported { strongSelf.maxVisibleMessageIndexReported = maxOverallIndex strongSelf.maxVisibleMessageIndexUpdated?(maxOverallIndex) } } } if let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last { if loaded.firstIndex < 5 && historyView.originalView.laterId != nil { strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount)) } else if loaded.lastIndex >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil { strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount)) } } } } } self.presentationDataDisposable = (context.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.currentPresentationData.theme let previousStrings = strongSelf.currentPresentationData.strings let previousWallpaper = strongSelf.currentPresentationData.chatWallpaper let previousDisableAnimations = strongSelf.currentPresentationData.disableAnimations strongSelf.currentPresentationData = presentationData if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings || previousWallpaper != presentationData.chatWallpaper || previousDisableAnimations != presentationData.disableAnimations { let themeData = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper) let chatPresentationData = ChatPresentationData(theme: themeData, fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations) strongSelf.dynamicBounceEnabled = !presentationData.disableAnimations strongSelf.forEachItemHeaderNode { itemHeaderNode in if let dateNode = itemHeaderNode as? ChatMessageDateHeaderNode { dateNode.updatePresentationData(chatPresentationData) } else if let dateNode = itemHeaderNode as? ListMessageDateHeaderNode { dateNode.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) } } strongSelf.chatPresentationDataPromise.set(.single(chatPresentationData)) } } }) self.visibleContentOffsetChanged = { [weak self] offset in if let strongSelf = self { strongSelf.contentPositionChanged(offset) if strongSelf.tagMask == nil { var atBottom = false switch offset { case let .known(offsetValue): if offsetValue.isLessThanOrEqualTo(0.0) { atBottom = true } default: break } if atBottom != strongSelf.isScrollAtBottomPosition { strongSelf.isScrollAtBottomPosition = atBottom strongSelf.updateReadHistoryActions() strongSelf.isScrollAtBottomPositionUpdated?() } } } } self.loadedMessagesFromCachedDataDisposable = (self._cachedPeerDataAndMessages.get() |> map { dataAndMessages -> MessageId? in return dataAndMessages.0?.messageIds.first } |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { messageId -> Signal in if let messageId = messageId { return getMessagesLoadIfNecessary([messageId], postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId) |> map { _ -> Void in return Void() } } else { return .complete() } }).start() } deinit { self.historyDisposable.dispose() self.readHistoryDisposable.dispose() self.interactiveReadActionDisposable?.dispose() self.canReadHistoryDisposable?.dispose() self.loadedMessagesFromCachedDataDisposable?.dispose() } public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) { self.loadStateUpdated = f } public func scrollScreenToTop() { var currentMessage: Message? if let historyView = self.historyView { if let visibleRange = self.displayedItemRange.loadedRange { var index = historyView.filteredEntries.count - 1 loop: for entry in historyView.filteredEntries { if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { if case let .MessageEntry(message, _, _, _, _, _) = entry { currentMessage = message break loop } else if case let .MessageGroupEntry(_, messages, _) = entry { currentMessage = messages.first?.0 break loop } } index -= 1 } } } if let currentMessage = currentMessage { self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .message(MessageIndex(currentMessage)), anchorIndex: .message(MessageIndex(currentMessage)), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true)) } } public func scrollToStartOfHistory() { self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true)) } public func scrollToEndOfHistory() { switch self.visibleContentOffset() { case .known(0.0): break default: 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, animated: Bool, highlight: Bool = true, scrollPosition: ListViewScrollPosition = .center(.bottom)) { self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .message(toIndex), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated)) } func scrollWithDeltaOffset(_ offset: CGFloat) { } public func anchorMessageInCurrentHistoryView() -> Message? { if let historyView = self.historyView { if let visibleRange = self.displayedItemRange.visibleRange { var index = 0 for entry in historyView.filteredEntries.reversed() { if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { if case let .MessageEntry(message, _, _, _, _, _) = entry { return message } } index += 1 } } for case let .MessageEntry(message, _, _, _, _, _) in historyView.filteredEntries { return message } } return nil } public func isMessageVisibleOnScreen(_ id: MessageId) -> Bool { var result = false self.forEachItemNode({ itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.content.contains(where: { $0.id == id }) { if self.itemNodeVisibleInsideInsets(itemNode) { result = true } } }) return result } public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { if let historyView = self.historyView { for entry in historyView.filteredEntries { if case let .MessageEntry(message, _, _, _, _, _) = entry { if message.id == id { return message } } else if case let .MessageGroupEntry(_, messages, _) = entry { for (message, _, _, _) in messages { if message.id == id { return message } } } } } return nil } public func messageGroupInCurrentHistoryView(_ id: MessageId) -> [Message]? { if let historyView = self.historyView { for entry in historyView.filteredEntries { if case let .MessageEntry(message, _, _, _, _, _) = entry { if message.id == id { return [message] } } else if case let .MessageGroupEntry(_, messages, _) = entry { for (message, _, _, _) in messages { if message.id == id { return messages.map { $0.0 } } } } } } return nil } public func forEachMessageInCurrentHistoryView(_ f: (Message) -> Bool) { if let historyView = self.historyView { for entry in historyView.filteredEntries { if case let .MessageEntry(message, _, _, _, _, _) = entry { if !f(message) { return } } else if case let .MessageGroupEntry(_, messages, _) = entry { for (message, _, _, _) in messages { if !f(message) { return } } } } } } private func updateMaxVisibleReadIncomingMessageIndex(_ index: MessageIndex) { self.maxVisibleIncomingMessageIndex.set(index) } private func enqueueHistoryViewTransition(_ transition: ChatHistoryListViewTransition) -> Signal { return Signal { [weak self] subscriber in if let strongSelf = self { if let _ = strongSelf.enqueuedHistoryViewTransition { preconditionFailure() } if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData))) } strongSelf.enqueuedHistoryViewTransition = (transition, { if let scrolledToIndex = transition.scrolledToIndex { if let strongSelf = self { strongSelf.scrolledToIndex?(scrolledToIndex) } } subscriber.putCompletion() }) if strongSelf.isNodeLoaded { strongSelf.dequeueHistoryViewTransition() } else { strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages))) let loadState: ChatHistoryNodeLoadState if transition.historyView.filteredEntries.isEmpty { loadState = .empty } else { loadState = .messages } if strongSelf.loadState != loadState { strongSelf.loadState = loadState strongSelf.loadStateUpdated?(loadState, transition.options.contains(.AnimateInsertion)) } 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 animated = transition.options.contains(.AnimateInsertion) let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in if let strongSelf = self { strongSelf.historyView = transition.historyView 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, animated) } if let range = visibleRange.loadedRange { strongSelf.context.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.firstIndex].index) if let visible = visibleRange.visibleRange { var visibleFirstIndex = visible.firstIndex /*if !visible.firstIndexFullyVisible { visibleFirstIndex += 1 }*/ if visibleFirstIndex <= visible.lastIndex { let (messageIndex, _) = maxMessageIndexForEntries(transition.historyView, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visibleFirstIndex)) if let messageIndex = messageIndex { strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex) } } } } if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData))) } strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages))) let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState strongSelf.historyState.set(historyState) } var buttonKeyboardMessageUpdated = false if let currentButtonKeyboardMessage = strongSelf.currentButtonKeyboardMessage, let buttonKeyboardMessage = transition.keyboardButtonsMessage { if currentButtonKeyboardMessage.id != buttonKeyboardMessage.id || currentButtonKeyboardMessage.stableVersion != buttonKeyboardMessage.stableVersion { buttonKeyboardMessageUpdated = true } } else if (strongSelf.currentButtonKeyboardMessage != nil) != (transition.keyboardButtonsMessage != nil) { buttonKeyboardMessageUpdated = true } if buttonKeyboardMessageUpdated { strongSelf.currentButtonKeyboardMessage = transition.keyboardButtonsMessage strongSelf._buttonKeyboardMessage.set(.single(transition.keyboardButtonsMessage)) } if transition.animateIn { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } completion() } } if let layoutActionOnViewTransition = self.layoutActionOnViewTransition { self.layoutActionOnViewTransition = nil let (mappedTransition, updateSizeAndInsets) = layoutActionOnViewTransition(transition) self.transaction(deleteIndices: mappedTransition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: mappedTransition.options, scrollToItem: mappedTransition.scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: mappedTransition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: completion) } else { self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: completion) } } } public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) { self.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: 0.0, scrollToTop: false) } public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets, additionalScrollDistance: CGFloat, scrollToTop: Bool) { var scrollToItem: ListViewScrollToItem? if scrollToTop, case .known = self.visibleContentOffset() { scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: updateSizeAndInsets.duration), directionHint: .Up) } self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, additionalScrollDistance: scrollToTop ? 0.0 : additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !self.dequeuedInitialTransitionOnLayout { self.dequeuedInitialTransitionOnLayout = true self.dequeueHistoryViewTransition() } } public func disconnect() { self.historyDisposable.set(nil) } private func updateReadHistoryActions() { let canRead = self.canReadHistoryValue && self.isScrollAtBottomPosition if canRead != (self.interactiveReadActionDisposable != nil) { if let interactiveReadActionDisposable = self.interactiveReadActionDisposable { if !canRead { interactiveReadActionDisposable.dispose() self.interactiveReadActionDisposable = nil } } else if self.interactiveReadActionDisposable == nil { if case let .peer(peerId) = self.chatLocation { self.interactiveReadActionDisposable = installInteractiveReadMessagesAction(postbox: self.context.account.postbox, stateManager: self.context.account.stateManager, peerId: peerId) } } } } func immediateScrollState() -> ChatInterfaceHistoryScrollState? { var currentMessage: Message? if let historyView = self.historyView { if let visibleRange = self.displayedItemRange.visibleRange { var index = 0 loop: for entry in historyView.filteredEntries.reversed() { if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { if case let .MessageEntry(message, _, _, _, _, _) = entry { if index != 0 || historyView.originalView.laterId != nil { currentMessage = message } break loop } else if case let .MessageGroupEntry(_, messages, _) = entry { if index != 0 || historyView.originalView.laterId != nil { currentMessage = messages.first?.0 } break loop } } index += 1 } } } if let message = currentMessage { var relativeOffset: CGFloat = 0.0 self.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == message.id { if let offsetValue = self.itemNodeRelativeOffset(itemNode) { relativeOffset = offsetValue } } } return ChatInterfaceHistoryScrollState(messageIndex: MessageIndex(message), relativeOffset: Double(relativeOffset)) } return nil } func scrollToNextMessage() { if let historyView = self.historyView { var scrolled = false if let scrollState = self.immediateScrollState() { var index = historyView.filteredEntries.count - 1 loop: for entry in historyView.filteredEntries.reversed() { if entry.index == scrollState.messageIndex { break loop } index -= 1 } if index != 0 { var nextItem = false self.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView, itemNode.item?.content.index == scrollState.messageIndex { if itemNode.frame.maxY >= self.bounds.size.height - self.insets.bottom - 4.0 { nextItem = true } } } if !nextItem { scrolled = true self.scrollToMessage(from: scrollState.messageIndex, to: scrollState.messageIndex, animated: true, highlight: false) } else { loop: for i in (index + 1) ..< historyView.filteredEntries.count { let entry = historyView.filteredEntries[i] switch entry { case .MessageEntry, .MessageGroupEntry: scrolled = true self.scrollToMessage(from: scrollState.messageIndex, to: entry.index, animated: true, highlight: false) break loop default: break } } } } } if !scrolled { self.scrollToEndOfHistory() } } } func requestMessageUpdate(_ id: MessageId) { if let historyView = self.historyView { var messageItem: ChatMessageItem? self.forEachItemNode({ itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { for message in item.content { if message.id == id { messageItem = item break } } } }) if let messageItem = messageItem { let associatedData = messageItem.associatedData loop: for i in 0 ..< historyView.filteredEntries.count { switch historyView.filteredEntries[i] { case let .MessageEntry(message, presentationData, read, _, selection, isAdmin): if message.id == id { let index = historyView.filteredEntries.count - 1 - i let item: ListViewItem switch self.mode { case .bubbles: item = ChatMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) case let .list(search, _): item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, context: self.context, chatLocation: self.chatLocation, controllerInteraction: self.controllerInteraction, message: message, selection: selection, displayHeader: search) } let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil) self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) break loop } default: break } } } } } }