import Foundation import UIKit import Postbox import SwiftSignalKit import Display import AsyncDisplayKit import TelegramCore private let historyMessageCount: Int = 100 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 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?, type: ChatHistoryViewUpdateType) case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, flashIndicators: Bool, originalScrollPosition: ChatHistoryViewScrollPosition?, initialData: ChatHistoryCombinedInitialData, id: Int32) } struct ChatHistoryView { let originalView: MessageHistoryView let filteredEntries: [ChatHistoryEntry] let associatedData: ChatMessageItemAssociatedData let id: Int32 } enum ChatHistoryViewTransitionReason { case Initial(fadeIn: Bool) case InteractiveChanges case Reload case HoleReload } 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 let reason: ChatHistoryViewTransitionReason let flashIndicators: 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 peerType: MediaAutoDownloadPeerType let networkType: MediaAutoDownloadNetworkType let animateIn: Bool let reason: ChatHistoryViewTransitionReason let flashIndicators: 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 } } let messageEntry = view.originalView.entries[i] if overall == nil || overall! < index { overall = index } if messageEntry.message.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, attributes): 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, attributes: attributes)) 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 .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, attributes): 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, attributes: attributes)) 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 .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, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn, reason: transition.reason, flashIndicators: transition.flashIndicators) } private final class ChatHistoryTransactionOpaqueState { let historyView: ChatHistoryView init(historyView: ChatHistoryView) { self.historyView = historyView } } private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType) -> ChatMessageItemAssociatedData { var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var contactsPeerIds: Set = Set() 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 for entry in view.entries { if entry.attributes.authorIsContact, let peerId = entry.message.author?.id { contactsPeerIds.insert(peerId) } } } 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 } } if automaticMediaDownloadPeerType == .group { for entry in view.entries { if entry.attributes.authorIsContact, let peerId = entry.message.author?.id { contactsPeerIds.insert(peerId) } } } } } let associatedData = ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, contactsPeerIds: contactsPeerIds) return associatedData } private extension ChatHistoryLocationInput { var isAtUpperBound: Bool { switch self.content { case .Navigation(index: .upperBound, anchorIndex: .upperBound, count: _): return true case .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: _, scrollPosition: _, animated: _): return true default: return false } } } 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 var chatHistoryLocationValue: ChatHistoryLocationInput? { didSet { if let chatHistoryLocationValue = self.chatHistoryLocationValue, chatHistoryLocationValue != oldValue { chatHistoryLocationPromise.set(chatHistoryLocationValue) } } } private let chatHistoryLocationPromise = ValuePromise() private var nextHistoryLocationId: Int32 = 1 private func takeNextHistoryLocationId() -> Int32 { let id = self.nextHistoryLocationId self.nextHistoryLocationId += 1 return id } private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let messageProcessingManager = ChatMessageThrottledProcessingManager() private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager() private let messageMentionProcessingManager = ChatMessageThrottledProcessingManager(delay: 0.2) let prefetchManager: InChatPrefetchManager private var currentEarlierPrefetchMessages: [(Message, Media)] = [] private var currentLaterPrefetchMessages: [(Message, Media)] = [] private var currentPrefetchDirectionIsToLater: Bool = true private var maxVisibleMessageIndexReported: MessageIndex? var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)? var scrolledToIndex: ((MessageHistoryAnchorIndex) -> Void)? private let hasVisiblePlayableItemNodesPromise = ValuePromise(false, ignoreRepeated: true) var hasVisiblePlayableItemNodes: Signal { return self.hasVisiblePlayableItemNodesPromise.get() } private var isInteractivelyScrollingValue: Bool = false private let isInteractivelyScrollingPromise = ValuePromise(false, ignoreRepeated: true) var isInteractivelyScrolling: Signal { return self.isInteractivelyScrollingPromise.get() } 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.sharedContext.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, largeEmoji: self.currentPresentationData.largeEmoji)) self.prefetchManager = InChatPrefetchManager(context: context) super.init() self.dynamicBounceEnabled = !self.currentPresentationData.disableAnimations self.experimentalSnapScrollToItem = true //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)) 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 currentViewVersion = Atomic(value: nil) let historyViewUpdate = self.chatHistoryLocationPromise.get() |> 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 } } } |> map { view -> (ChatHistoryViewUpdate, Int) in let version = currentViewVersion.modify({ value in if let value = value { return value + 1 } else { return 0 } })! return (view, version) } let previousView = Atomic(value: nil) let automaticDownloadNetworkType = context.account.networkType |> map { type -> MediaAutoDownloadNetworkType in switch type { case .none, .wifi: return .wifi case .cellular: return .cellular } } |> distinctUntilChanged let previousHistoryAppearsCleared = Atomic(value: nil) let historyViewTransition = combineLatest(queue: messageViewQueue, historyViewUpdate, self.chatPresentationDataPromise.get(), selectedMessages, automaticDownloadNetworkType, self.historyAppearsClearedPromise.get()) |> introduceError(Void.self) |> mapToQueue { [weak self] update, chatPresentationData, selectedMessages, networkType, historyAppearsCleared -> Signal in func applyHole() { Queue.mainQueue().async { if let strongSelf = self { let historyView = (strongSelf.opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView let displayRange = strongSelf.displayedItemRange if let filteredEntries = historyView?.filteredEntries, let visibleRange = displayRange.visibleRange { let lastEntry = filteredEntries[filteredEntries.count - 1 - visibleRange.lastIndex] strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount), id: 0) } else { if let messageId = messageId { strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: 60), id: 0) } else { strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: 60), id: 0) } } } } } let initialData: ChatHistoryCombinedInitialData? switch update.0 { case let .Loading(combinedInitialData, type): if case .Generic(.FillHole) = type { applyHole() return .fail(Void()) } initialData = combinedInitialData Queue.mainQueue().async { 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, flashIndicators, originalScrollPosition, data, id): if case .Generic(.FillHole) = type { applyHole() return .fail(Void()) } 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, id: id) 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.isEmpty { 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.id == processedView.id, 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, .Initial: reason = ChatHistoryViewTransitionReason.Initial(fadeIn: false) case .Generic: reason = ChatHistoryViewTransitionReason.InteractiveChanges case .UpdateVisible: reason = ChatHistoryViewTransitionReason.Reload case .FillHole: reason = ChatHistoryViewTransitionReason.HoleReload } } } 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, flashIndicators: flashIndicators) |> map({ mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) |> introduceError(Void.self) } } let appliedTransition = historyViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in if let strongSelf = self { return strongSelf.enqueueHistoryViewTransition(transition) |> introduceError(Void.self) } return .complete() } let restartedTransition = ( appliedTransition |> `catch` { _ -> Signal in return .complete() } ) |> restart self.historyDisposable.set(restartedTransition.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: if !context.sharedContext.immediateExperimentalUISettings.skipReadHistory { 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.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: 60), id: 0) } else { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: 60), id: 0) } self.chatHistoryLocationPromise.set(self.chatHistoryLocationValue!) self.generalScrollDirectionUpdated = { [weak self] direction in guard let strongSelf = self else { return } let prefetchDirectionIsToLater = direction == .up if strongSelf.currentPrefetchDirectionIsToLater != prefetchDirectionIsToLater { strongSelf.currentPrefetchDirectionIsToLater = prefetchDirectionIsToLater if strongSelf.currentPrefetchDirectionIsToLater { strongSelf.prefetchManager.updateMessages(strongSelf.currentLaterPrefetchMessages, directionIsToLater: strongSelf.currentPrefetchDirectionIsToLater) } else { strongSelf.prefetchManager.updateMessages(strongSelf.currentEarlierPrefetchMessages, directionIsToLater: strongSelf.currentPrefetchDirectionIsToLater) } } } self.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in if let strongSelf = self, let transactionState = opaqueTransactionState as? ChatHistoryTransactionOpaqueState { self?.processDisplayedItemRangeChanged(displayedRange: displayedRange, transactionState: transactionState) } } self.presentationDataDisposable = (context.sharedContext.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, largeEmoji: presentationData.largeEmoji) 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() self.beganInteractiveDragging = { [weak self] in self?.isInteractivelyScrollingValue = true self?.isInteractivelyScrollingPromise.set(true) } self.didEndScrolling = { [weak self] in self?.isInteractivelyScrollingValue = false self?.isInteractivelyScrollingPromise.set(false) } } 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 } private func processDisplayedItemRangeChanged(displayedRange: ListViewDisplayedItemRange, transactionState: ChatHistoryTransactionOpaqueState) { let historyView = transactionState.historyView if let visible = displayedRange.visibleRange { let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex) let readIndexRange = (0, historyView.filteredEntries.count - 1 - visible.firstIndex) let toEarlierRange = (0, historyView.filteredEntries.count - 1 - visible.lastIndex - 1) let toLaterRange = (historyView.filteredEntries.count - 1 - (visible.firstIndex - 1), historyView.filteredEntries.count - 1) var messageIdsWithViewCount: [MessageId] = [] var messageIdsWithUnsupportedMedia: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = [] var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = [] var messagesWithPreloadableMediaToLater: [(Message, Media)] = [] 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 } } } var contentRequiredValidation = false 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 } else if let _ = attribute as? ContentRequiresValidationMessageAttribute { contentRequiredValidation = true } } for media in message.media { if let _ = media as? TelegramMediaUnsupported { contentRequiredValidation = true } } if contentRequiredValidation { 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 } } func addMediaToPrefetch(_ message: Message, _ media: Media, _ messages: inout [(Message, Media)]) -> Bool { if media is TelegramMediaImage || media is TelegramMediaFile { messages.append((message, media)) } if messages.count >= 3 { return false } else { return true } } var toEarlierMediaMessages: [(Message, Media)] = [] if toEarlierRange.0 <= toEarlierRange.1 { outer: for i in (toEarlierRange.0 ... toEarlierRange.1).reversed() { switch historyView.filteredEntries[i] { case let .MessageEntry(message, _, _, _, _, _): for media in message.media { if !addMediaToPrefetch(message, media, &toEarlierMediaMessages) { break outer } } case let .MessageGroupEntry(_, messages, _): for (message, _, _, _) in messages { var stop = false for media in message.media { if !addMediaToPrefetch(message, media, &toEarlierMediaMessages) { stop = true } } if stop { break outer } } default: break } } } var toLaterMediaMessages: [(Message, Media)] = [] if toLaterRange.0 <= toLaterRange.1 { outer: for i in (toLaterRange.0 ... toLaterRange.1) { switch historyView.filteredEntries[i] { case let .MessageEntry(message, _, _, _, _, _): for media in message.media { if !addMediaToPrefetch(message, media, &toLaterMediaMessages) { break outer } } case let .MessageGroupEntry(_, messages, _): for (message, _, _, _) in messages { for media in message.media { if !addMediaToPrefetch(message, media, &toLaterMediaMessages) { break outer } } } default: break } } } if !messageIdsWithViewCount.isEmpty { self.messageProcessingManager.add(messageIdsWithViewCount) } if !messageIdsWithUnsupportedMedia.isEmpty { self.unsupportedMessageProcessingManager.add(messageIdsWithUnsupportedMedia) } if !messageIdsWithUnseenPersonalMention.isEmpty { self.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention) } self.currentEarlierPrefetchMessages = toEarlierMediaMessages self.currentLaterPrefetchMessages = toLaterMediaMessages if self.currentPrefetchDirectionIsToLater { self.prefetchManager.updateMessages(toLaterMediaMessages, directionIsToLater: self.currentPrefetchDirectionIsToLater) } else { self.prefetchManager.updateMessages(toEarlierMediaMessages, directionIsToLater: self.currentPrefetchDirectionIsToLater) } if readIndexRange.0 <= readIndexRange.1 { let (maxIncomingIndex, maxOverallIndex) = maxMessageIndexForEntries(historyView, indexRange: readIndexRange) if let maxIncomingIndex = maxIncomingIndex { self.updateMaxVisibleReadIncomingMessageIndex(maxIncomingIndex) } if let maxOverallIndex = maxOverallIndex, maxOverallIndex != self.maxVisibleMessageIndexReported { self.maxVisibleMessageIndexReported = maxOverallIndex self.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 { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount), id: 0) } else if loaded.firstIndex < 5, historyView.originalView.laterId == nil, !historyView.originalView.holeLater, let chatHistoryLocationValue = self.chatHistoryLocationValue, !chatHistoryLocationValue.isAtUpperBound, historyView.originalView.anchorIndex != .upperBound { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .upperBound, anchorIndex: .upperBound, count: historyMessageCount), id: 0) } else if loaded.lastIndex >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount), id: 0) } } var containsPlayableWithSoundItemNode = false self.forEachVisibleItemNode { itemNode in if let chatItemView = itemNode as? ChatMessageItemView, chatItemView.playMediaWithSound() != nil { containsPlayableWithSoundItemNode = true } } self.hasVisiblePlayableItemNodesPromise.set(containsPlayableWithSoundItemNode) if containsPlayableWithSoundItemNode && !self.isInteractivelyScrollingValue { self.isInteractivelyScrollingPromise.set(true) self.isInteractivelyScrollingPromise.set(false) } } 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.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(currentMessage.index), anchorIndex: .message(currentMessage.index), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true), id: self.takeNextHistoryLocationId()) } } public func scrollToStartOfHistory() { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true), id: self.takeNextHistoryLocationId()) } public func scrollToEndOfHistory() { switch self.visibleContentOffset() { case .known(0.0): break default: self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true), id: self.takeNextHistoryLocationId()) } } public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, scrollPosition: ListViewScrollPosition = .center(.bottom)) { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(toIndex), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated), id: self.takeNextHistoryLocationId()) } 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 latestMessageInCurrentHistoryView() -> Message? { if let historyView = self.historyView { if historyView.originalView.laterId == nil, let firstEntry = historyView.filteredEntries.last { if case let .MessageEntry(message, _, _, _, _, _) = firstEntry { return message } } } return nil } 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() } strongSelf.prefetchManager.updateOptions(InChatPrefetchOptions(networkType: transition.networkType, peerType: transition.peerType)) 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 { 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) } if transition.flashIndicators { //self.flashHeaderItems() } } } 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 { if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { 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: message.index, 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, attributes): 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, attributes: attributes)) 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 } } } } } }