From c197d9cd4c51508d041ce453bfb7b7abd3760771 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 25 Sep 2020 19:46:36 +0400 Subject: [PATCH] Comments [WIP] --- submodules/Display/Source/ListView.swift | 2 +- submodules/Postbox/Sources/ChatLocation.swift | 2 +- submodules/Postbox/Sources/Postbox.swift | 2 +- .../Sources/AccountViewTracker.swift | 10 +- .../Sources/HistoryViewStateValidation.swift | 301 +++++++++++++++++- .../TelegramUI/Sources/AccountContext.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 6 +- .../Sources/ChatHistoryListNode.swift | 18 +- 8 files changed, 329 insertions(+), 14 deletions(-) diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index faf1fefcfb..0a29ea8295 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -3654,7 +3654,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } - private func updateVisibleItemRange(force: Bool = false) { + public func updateVisibleItemRange(force: Bool = false) { let currentRange = self.immediateDisplayedItemRange() if currentRange != self.displayedItemRange || force { diff --git a/submodules/Postbox/Sources/ChatLocation.swift b/submodules/Postbox/Sources/ChatLocation.swift index cf24d9d842..095d1da8b6 100644 --- a/submodules/Postbox/Sources/ChatLocation.swift +++ b/submodules/Postbox/Sources/ChatLocation.swift @@ -3,7 +3,7 @@ import SwiftSignalKit public enum ChatLocationInput { case peer(PeerId) - case external(PeerId, Signal) + case external(PeerId, Int64, Signal) } public enum ResolvedChatLocationInput { diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 5e81d19360..45d347a365 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -2416,7 +2416,7 @@ public final class Postbox { switch chatLocation { case let .peer(peerId): return .single((.peer(peerId), false)) - case let .external(_, input): + case let .external(_, _, input): return Signal { subscriber in var isHoleFill = false return (input diff --git a/submodules/TelegramCore/Sources/AccountViewTracker.swift b/submodules/TelegramCore/Sources/AccountViewTracker.swift index e4b42bb2ce..9c4e78111e 100644 --- a/submodules/TelegramCore/Sources/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/AccountViewTracker.swift @@ -190,7 +190,7 @@ private func wrappedHistoryViewAdditionalData(chatLocation: ChatLocationInput, a result.append(.peerChatState(peerId)) } } - case let .external(peerId, _): + case let .external(peerId, _, _): if peerId.namespace == Namespaces.Peer.CloudChannel { if result.firstIndex(where: { if case .peerChatState = $0 { return true } else { return false } }) == nil { result.append(.peerChatState(peerId)) @@ -1241,6 +1241,8 @@ public final class AccountViewTracker { strongSelf.updatePolls(viewId: viewId, messageIds: pollMessageIds, messages: pollMessageDict) if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel { strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0) + } else if case let .external(peerId, _, _) = chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel { + strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0, location: chatLocation) } } } @@ -1254,9 +1256,9 @@ public final class AccountViewTracker { if peerId.namespace == Namespaces.Peer.CloudChannel { strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil) } - case let .external(peerId, _): + case let .external(peerId, _, _): if peerId.namespace == Namespaces.Peer.CloudChannel { - strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil) + strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: chatLocation) } } } @@ -1267,7 +1269,7 @@ public final class AccountViewTracker { switch chatLocation { case let .peer(peerIdValue): peerId = peerIdValue - case let .external(peerIdValue, _): + case let .external(peerIdValue, _, _): peerId = peerIdValue } if peerId.namespace == Namespaces.Peer.CloudChannel { diff --git a/submodules/TelegramCore/Sources/HistoryViewStateValidation.swift b/submodules/TelegramCore/Sources/HistoryViewStateValidation.swift index 590ad6d6d1..2c5d6a3b8e 100644 --- a/submodules/TelegramCore/Sources/HistoryViewStateValidation.swift +++ b/submodules/TelegramCore/Sources/HistoryViewStateValidation.swift @@ -146,6 +146,7 @@ final class HistoryViewStateValidationContexts { } return } + var historyState: HistoryState? for entry in view.additionalData { if case let .peerChatState(peerId, chatState) = entry { @@ -156,7 +157,101 @@ final class HistoryViewStateValidationContexts { } } - if let historyState = historyState, historyState.hasInvalidationIndex { + if let location = location, case let .external(peerId, threadId, _) = location { + var rangesToInvalidate: [[MessageId]] = [] + let addToRange: (MessageId, inout [[MessageId]]) -> Void = { id, ranges in + if ranges.isEmpty { + ranges = [[id]] + } else { + ranges[ranges.count - 1].append(id) + } + } + + let addRangeBreak: (inout [[MessageId]]) -> Void = { ranges in + if ranges.last?.count != 0 { + ranges.append([]) + } + } + + for entry in view.entries { + if entry.message.id.peerId == peerId && entry.message.id.namespace == Namespaces.Message.Cloud { + addToRange(entry.message.id, &rangesToInvalidate) + } + } + + if !rangesToInvalidate.isEmpty && rangesToInvalidate[rangesToInvalidate.count - 1].isEmpty { + rangesToInvalidate.removeLast() + } + + var invalidatedMessageIds = Set() + + if !rangesToInvalidate.isEmpty { + let context: HistoryStateValidationContext + if let current = self.contexts[id] { + context = current + } else { + context = HistoryStateValidationContext() + self.contexts[id] = context + } + + var addedRanges: [[MessageId]] = [] + for messages in rangesToInvalidate { + for id in messages { + invalidatedMessageIds.insert(id) + + if context.batchReferences[id] != nil { + addRangeBreak(&addedRanges) + } else { + addToRange(id, &addedRanges) + } + } + addRangeBreak(&addedRanges) + } + + if !addedRanges.isEmpty && addedRanges[addedRanges.count - 1].isEmpty { + addedRanges.removeLast() + } + + for rangeMessages in addedRanges { + for messages in slicedForValidationMessages(rangeMessages) { + let disposable = MetaDisposable() + let batch = HistoryStateValidationBatch(disposable: disposable, invalidatedState: historyState) + for messageId in messages { + context.batchReferences[messageId] = batch + } + + disposable.set((validateReplyThreadMessagesBatch(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: peerId, threadMessageId: makeThreadIdMessageId(peerId: peerId, threadId: threadId).id, messageIds: messages) + |> deliverOn(self.queue)).start(completed: { [weak self, weak batch] in + if let strongSelf = self, let context = strongSelf.contexts[id], let batch = batch { + var completedMessageIds: [MessageId] = [] + for (messageId, messageBatch) in context.batchReferences { + if messageBatch === batch { + completedMessageIds.append(messageId) + } + } + for messageId in completedMessageIds { + //context.batchReferences.removeValue(forKey: messageId) + } + } + })) + } + } + } + + if let context = self.contexts[id] { + var removeIds: [MessageId] = [] + + for batchMessageId in context.batchReferences.keys { + if !invalidatedMessageIds.contains(batchMessageId) { + removeIds.append(batchMessageId) + } + } + + for messageId in removeIds { + context.batchReferences.removeValue(forKey: messageId) + } + } + } else if let historyState = historyState, historyState.hasInvalidationIndex { var rangesToInvalidate: [[MessageId]] = [] let addToRange: (MessageId, inout [[MessageId]]) -> Void = { id, ranges in if ranges.isEmpty { @@ -421,6 +516,59 @@ private func validateChannelMessagesBatch(postbox: Postbox, network: Network, ac } |> switchToLatest } +private func validateReplyThreadMessagesBatch(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, threadMessageId: Int32, messageIds: [MessageId]) -> Signal { + return postbox.transaction { transaction -> Signal in + var previousMessages: [Message] = [] + var previous: [MessageId: Message] = [:] + for messageId in messageIds { + if let message = transaction.getMessage(messageId) { + previousMessages.append(message) + previous[message.id] = message + } + } + + var signal: Signal + let hash = hashForMessages(previousMessages, withChannelIds: false) + Logger.shared.log("HistoryValidation", "validate reply thread batch for \(peerId): \(previousMessages.map({ $0.id }))") + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + let requestSignal: Signal + requestSignal = network.request(Api.functions.messages.getReplies(peer: inputPeer, msgId: threadMessageId, offsetId: messageIds[messageIds.count - 1].id, offsetDate: 0, addOffset: -1, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1, hash: hash)) + + signal = requestSignal + |> map { result -> ValidatedMessages in + let messages: [Api.Message] + let chats: [Api.Chat] + let users: [Api.User] + var channelPts: Int32? + + switch result { + case let .messages(messages: apiMessages, chats: apiChats, users: apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case let .messagesSlice(_, _, _, messages: apiMessages, chats: apiChats, users: apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case let .channelMessages(_, pts, _, apiMessages, apiChats, apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + channelPts = pts + case .messagesNotModified: + return .notModified + } + return .messages(messages, chats, users, channelPts) + } + } else { + return .complete() + } + + return validateReplyThreadBatch(postbox: postbox, network: network, transaction: transaction, accountPeerId: accountPeerId, peerId: peerId, threadId: makeMessageThreadId(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: threadMessageId)), signal: signal, previous: previous, messageNamespace: Namespaces.Message.Cloud) + } + |> switchToLatest +} + private func validateScheduledMessagesBatch(postbox: Postbox, network: Network, accountPeerId: PeerId, tag: MessageTags?, messages: [Message], historyState: HistoryState) -> Signal { return postbox.transaction { transaction -> Signal in var signal: Signal @@ -715,3 +863,154 @@ private func validateBatch(postbox: Postbox, network: Network, transaction: Tran } } } + +private func validateReplyThreadBatch(postbox: Postbox, network: Network, transaction: Transaction, accountPeerId: PeerId, peerId: PeerId, threadId: Int64, signal: Signal, previous: [MessageId: Message], messageNamespace: MessageId.Namespace) -> Signal { + return signal + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .complete() + } + switch result { + case let .messages(messages, _, _, channelPts): + var storeMessages: [StoreMessage] = [] + + for message in messages { + if let storeMessage = StoreMessage(apiMessage: message, namespace: messageNamespace) { + var attributes = storeMessage.attributes + if let channelPts = channelPts { + attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts)) + } + storeMessages.append(storeMessage.withUpdatedAttributes(attributes)) + } + } + + var validMessageIds = Set() + for message in storeMessages { + if case let .Id(id) = message.id { + validMessageIds.insert(id) + } + } + + var maybeRemovedMessageIds: [MessageId] = [] + for id in previous.keys { + if !validMessageIds.contains(id) { + maybeRemovedMessageIds.append(id) + } + } + + let actuallyRemovedMessagesSignal: Signal, NoError> + if maybeRemovedMessageIds.isEmpty { + actuallyRemovedMessagesSignal = .single(Set()) + } else { + actuallyRemovedMessagesSignal = postbox.transaction { transaction -> Signal, NoError> in + if let inputChannel = transaction.getPeer(peerId).flatMap(apiInputChannel) { + return network.request(Api.functions.channels.getMessages(channel: inputChannel, id: maybeRemovedMessageIds.map({ Api.InputMessage.inputMessageID(id: $0.id) }))) + |> map { result -> Set in + let apiMessages: [Api.Message] + switch result { + case let .channelMessages(_, _, _, messages, _, _): + apiMessages = messages + case let .messages(messages, _, _): + apiMessages = messages + case let .messagesSlice(_, _, _, messages, _, _): + apiMessages = messages + case .messagesNotModified: + return Set() + } + var ids = Set() + for message in apiMessages { + if let parsedMessage = StoreMessage(apiMessage: message, namespace: messageNamespace), case let .Id(id) = parsedMessage.id { + ids.insert(id) + } + } + return Set(maybeRemovedMessageIds).subtracting(ids) + } + |> `catch` { _ -> Signal, NoError> in + return .single(Set(maybeRemovedMessageIds)) + } + } + return .single(Set(maybeRemovedMessageIds)) + } + |> switchToLatest + } + + return actuallyRemovedMessagesSignal + |> mapToSignal { removedMessageIds -> Signal in + return postbox.transaction { transaction -> Void in + var validMessageIds = Set() + for message in storeMessages { + if case let .Id(id) = message.id { + validMessageIds.insert(id) + let previousMessage = previous[id] ?? transaction.getMessage(id) + + if let previousMessage = previousMessage { + var updatedTimestamp = message.timestamp + inner: for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + updatedTimestamp = attribute.date + break inner + } + } + + var timestamp = previousMessage.timestamp + inner: for attribute in previousMessage.attributes { + if let attribute = attribute as? EditedMessageAttribute { + timestamp = attribute.date + break inner + } + } + + transaction.updateMessage(id, update: { currentMessage in + if updatedTimestamp != timestamp { + var updatedLocalTags = message.localTags + if currentMessage.localTags.contains(.OutgoingLiveLocation) { + updatedLocalTags.insert(.OutgoingLiveLocation) + } + return .update(message.withUpdatedLocalTags(updatedLocalTags)) + } else { + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType) + } + var attributes = currentMessage.attributes + if let channelPts = channelPts { + for i in (0 ..< attributes.count).reversed() { + if let _ = attributes[i] as? ChannelMessageStateVersionAttribute { + attributes.remove(at: i) + } + } + attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts)) + } + + let updatedFlags = StoreMessageFlags(currentMessage.flags) + + return .update(StoreMessage(id: message.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: updatedFlags, tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + } + }) + + if previous[id] == nil { + print("\(id) missing") + } + } else { + let _ = transaction.addMessages([message], location: .Random) + } + } + } + + for id in removedMessageIds { + if !validMessageIds.contains(id) { + deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [id]) + Logger.shared.log("HistoryValidation", "deleting thread message \(id) in \(id.peerId)") + } + } + } + } + case .notModified: + return .complete() + } + } +} diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 6587155840..68516fc79e 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -305,7 +305,7 @@ public final class AccountContextImpl: AccountContext { return .peer(peerId) case let .replyThread(data): let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) - return .external(data.messageId.peerId, context.state) + return .external(data.messageId.peerId, makeMessageThreadId(data.messageId), context.state) } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c9b666aca2..26274f8558 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -5398,7 +5398,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - self.willAppear = true + if self.willAppear { + self.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages() + } else { + self.willAppear = true + } if self.scheduledActivateInput { self.scheduledActivateInput = false diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index e9ba3c5a2d..60630a7cdc 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -547,6 +547,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let isTopReplyThreadMessageShown = ValuePromise(false, ignoreRepeated: true) + private let clientId: Atomic + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles) { self.context = context self.chatLocation = chatLocation @@ -563,6 +565,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.prefetchManager = InChatPrefetchManager(context: context) + let clientId = Atomic(value: nextClientId) + self.clientId = clientId + nextClientId += 1 + super.init() self.dynamicBounceEnabled = !self.currentPresentationData.disableAnimations @@ -570,11 +576,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { //self.debugInfo = true - let clientId = nextClientId - nextClientId += 1 - self.messageProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds, clientId: clientId) + context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds, clientId: clientId.with { $0 }) } self.messageReactionsProcessingManager.process = { [weak context] messageIds in context?.account.viewTracker.updateReactionsForMessageIds(messageIds: messageIds) @@ -1098,6 +1101,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.loadStateUpdated = f } + func refreshPollActionsForVisibleMessages() { + let _ = self.clientId.swap(nextClientId) + nextClientId += 1 + + self.updateVisibleItemRange(force: true) + } + private func processDisplayedItemRangeChanged(displayedRange: ListViewDisplayedItemRange, transactionState: ChatHistoryTransactionOpaqueState) { let historyView = transactionState.historyView var isTopReplyThreadMessageShownValue = false