From 7fc078d31500ac6fd6619af31d00e3f34e30b29a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 17 Jul 2020 21:25:20 +0400 Subject: [PATCH] Message history animation fix --- .../Postbox/Sources/MessageHistoryView.swift | 24 +++++++ submodules/Postbox/Sources/Postbox.swift | 24 ++++++- .../Sources/AccountIntermediateState.swift | 41 +++++++++--- .../Sources/AccountStateManagementUtils.swift | 64 +++++++++++++++++-- .../Sources/AccountViewTracker.swift | 4 +- .../Sources/ChatHistoryListNode.swift | 26 +++++++- .../Sources/ChatHistoryViewForLocation.swift | 4 +- 7 files changed, 165 insertions(+), 22 deletions(-) diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index 615ef4082c..87c2e0db6a 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -272,6 +272,8 @@ final class MutableMessageHistoryView { fileprivate(set) var sampledState: HistoryViewSample + fileprivate var isAddedToChatList: Bool + init(postbox: Postbox, orderStatistics: MessageHistoryViewOrderStatistics, clipHoles: Bool, peerIds: MessageHistoryViewPeerIds, anchor inputAnchor: HistoryViewInputAnchor, combinedReadStates: MessageHistoryViewReadState?, transientReadStates: MessageHistoryViewReadState?, tag: MessageTags?, namespaces: MessageIdNamespaces, count: Int, topTaggedMessages: [MessageId.Namespace: MessageHistoryTopTaggedMessage?], additionalDatas: [AdditionalMessageHistoryViewDataEntry], getMessageCountInRange: (MessageIndex, MessageIndex) -> Int32) { self.anchor = inputAnchor @@ -286,6 +288,15 @@ final class MutableMessageHistoryView { self.topTaggedMessages = topTaggedMessages self.additionalDatas = additionalDatas + let mainPeerId: PeerId + switch peerIds { + case let .associated(peerId, _): + mainPeerId = peerId + case let .single(peerId): + mainPeerId = peerId + } + self.isAddedToChatList = postbox.chatListTable.getPeerChatListIndex(peerId: mainPeerId) != nil + self.state = HistoryViewState(postbox: postbox, inputAnchor: inputAnchor, tag: tag, namespaces: namespaces, statistics: self.orderStatistics, halfLimit: count + 1, locations: peerIds) if case let .loading(loadingState) = self.state { let sampledState = loadingState.checkAndSample(postbox: postbox) @@ -351,6 +362,17 @@ final class MutableMessageHistoryView { var operations: [[MessageHistoryOperation]] = [] var peerIdsSet = Set() + if !transaction.chatListOperations.isEmpty { + let mainPeerId: PeerId + switch peerIds { + case let .associated(peerId, _): + mainPeerId = peerId + case let .single(peerId): + mainPeerId = peerId + } + self.isAddedToChatList = postbox.chatListTable.getPeerChatListIndex(peerId: mainPeerId) != nil + } + switch self.peerIds { case let .single(peerId): peerIdsSet.insert(peerId) @@ -718,10 +740,12 @@ public final class MessageHistoryView { public let topTaggedMessages: [Message] public let additionalData: [AdditionalMessageHistoryViewDataEntry] public let isLoading: Bool + public let isAddedToChatList: Bool init(_ mutableView: MutableMessageHistoryView) { self.tagMask = mutableView.tag self.namespaces = mutableView.namespaces + self.isAddedToChatList = mutableView.isAddedToChatList var entries: [MessageHistoryEntry] switch mutableView.sampledState { case .loading: diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 0b453a89b8..03959746fe 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -945,6 +945,10 @@ public final class Transaction { self.postbox?.scanMessages(peerId: peerId, namespace: namespace, tag: tag, f) } + public func scanMessageAttributes(peerId: PeerId, namespace: MessageId.Namespace, _ f: (MessageId, [MessageAttribute]) -> Bool) { + self.postbox?.scanMessageAttributes(peerId: peerId, namespace: namespace, f) + } + public func invalidateMessageHistoryTagsSummary(peerId: PeerId, namespace: MessageId.Namespace, tagMask: MessageTags) { assert(!self.disposed) self.postbox?.invalidateMessageHistoryTagsSummary(peerId: peerId, namespace: namespace, tagMask: tagMask) @@ -1043,7 +1047,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, #if DEBUG //debugSaveState(basePath: basePath, name: "previous1") - //debugRestoreState(basePath: basePath, name: "previous1") + debugRestoreState(basePath: basePath, name: "previous1") #endif let startTime = CFAbsoluteTimeGetCurrent() @@ -3170,6 +3174,24 @@ public final class Postbox { } } + fileprivate func scanMessageAttributes(peerId: PeerId, namespace: MessageId.Namespace, _ f: (MessageId, [MessageAttribute]) -> Bool) { + var index = MessageIndex.upperBound(peerId: peerId, namespace: namespace) + while true { + let messages = self.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: nil, from: index, includeFrom: false, to: MessageIndex.lowerBound(peerId: peerId, namespace: namespace), limit: 32) + for message in messages { + let attributes = MessageHistoryTable.renderMessageAttributes(message) + if !f(message.id, attributes) { + break + } + } + if let last = messages.last { + index = last.index + } else { + break + } + } + } + fileprivate func invalidateMessageHistoryTagsSummary(peerId: PeerId, namespace: MessageId.Namespace, tagMask: MessageTags) { self.invalidatedMessageHistoryTagsSummaryTable.insert(InvalidatedMessageHistoryTagsSummaryKey(peerId: peerId, namespace: namespace, tagMask: tagMask), operations: &self.currentInvalidateMessageTagSummaries) } diff --git a/submodules/TelegramCore/Sources/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/AccountIntermediateState.swift index e55221d479..21ccd2c9c8 100644 --- a/submodules/TelegramCore/Sources/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/AccountIntermediateState.swift @@ -109,6 +109,22 @@ enum AccountStateMutationOperation { case UpdateChatListFilter(id: Int32, filter: Api.DialogFilter?) } +struct HoleFromPreviousState { + var validateChannelPts: Int32? + + func mergedWith(_ other: HoleFromPreviousState) -> HoleFromPreviousState { + var result = self + if let pts = self.validateChannelPts, let otherPts = other.validateChannelPts { + result.validateChannelPts = max(pts, otherPts) + } else if let pts = self.validateChannelPts { + result.validateChannelPts = pts + } else if let otherPts = other.validateChannelPts { + result.validateChannelPts = otherPts + } + return result + } +} + struct AccountMutableState { let initialState: AccountInitialState let branchOperationIndex: Int @@ -122,7 +138,7 @@ struct AccountMutableState { var referencedMessageIds: Set var storedMessages: Set var readInboxMaxIds: [PeerId: MessageId] - var namespacesWithHolesFromPreviousState: [PeerId: Set] + var namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]] var updatedOutgoingUniqueMessageIds: [Int64: Int32] var storedMessagesByPeerIdAndTimestamp: [PeerId: Set] @@ -154,7 +170,7 @@ struct AccountMutableState { self.updatedOutgoingUniqueMessageIds = [:] } - init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedMessageIds: Set, storedMessages: Set, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: Set], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], branchOperationIndex: Int) { + init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedMessageIds: Set, storedMessages: Set, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], branchOperationIndex: Int) { self.initialState = initialState self.operations = operations self.state = state @@ -187,10 +203,14 @@ struct AccountMutableState { self.externallyUpdatedPeerId.formUnion(other.externallyUpdatedPeerId) for (peerId, namespaces) in other.namespacesWithHolesFromPreviousState { if self.namespacesWithHolesFromPreviousState[peerId] == nil { - self.namespacesWithHolesFromPreviousState[peerId] = Set() + self.namespacesWithHolesFromPreviousState[peerId] = [:] } - for namespace in namespaces { - self.namespacesWithHolesFromPreviousState[peerId]!.insert(namespace) + for (namespace, namespaceState) in namespaces { + if self.namespacesWithHolesFromPreviousState[peerId]![namespace] == nil { + self.namespacesWithHolesFromPreviousState[peerId]![namespace] = namespaceState + } else { + self.namespacesWithHolesFromPreviousState[peerId]![namespace] = self.namespacesWithHolesFromPreviousState[peerId]![namespace]!.mergedWith(namespaceState) + } } } self.updatedOutgoingUniqueMessageIds.merge(other.updatedOutgoingUniqueMessageIds, uniquingKeysWith: { lhs, _ in lhs }) @@ -296,11 +316,16 @@ struct AccountMutableState { self.addOperation(.UpdateGlobalNotificationSettings(subject, notificationSettings)) } - mutating func setNeedsHoleFromPreviousState(peerId: PeerId, namespace: MessageId.Namespace) { + mutating func setNeedsHoleFromPreviousState(peerId: PeerId, namespace: MessageId.Namespace, validateChannelPts: Int32?) { if self.namespacesWithHolesFromPreviousState[peerId] == nil { - self.namespacesWithHolesFromPreviousState[peerId] = Set() + self.namespacesWithHolesFromPreviousState[peerId] = [:] + } + let namespaceState = HoleFromPreviousState(validateChannelPts: validateChannelPts) + if self.namespacesWithHolesFromPreviousState[peerId]![namespace] == nil { + self.namespacesWithHolesFromPreviousState[peerId]![namespace] = namespaceState + } else { + self.namespacesWithHolesFromPreviousState[peerId]![namespace] = self.namespacesWithHolesFromPreviousState[peerId]![namespace]!.mergedWith(namespaceState) } - self.namespacesWithHolesFromPreviousState[peerId]!.insert(namespace) } mutating func mergeChats(_ chats: [Api.Chat]) { diff --git a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift index 430fdbec38..ffde8b508d 100644 --- a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift @@ -1510,7 +1510,7 @@ private func resolveMissingPeerChatInfos(network: Network, state: AccountMutable case let .dialog(_, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, notifySettings, pts, _, folderId): let peerId = peer.peerId - updatedState.setNeedsHoleFromPreviousState(peerId: peerId, namespace: Namespaces.Message.Cloud) + updatedState.setNeedsHoleFromPreviousState(peerId: peerId, namespace: Namespaces.Message.Cloud, validateChannelPts: pts) if topMessage != 0 { topMessageIds.insert(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: topMessage)) @@ -1590,7 +1590,7 @@ private func resolveMissingPeerChatInfos(network: Network, state: AccountMutable } func keepPollingChannel(postbox: Postbox, network: Network, peerId: PeerId, stateManager: AccountStateManager) -> Signal { - return postbox.transaction { transaction -> Signal in + let signal: Signal = postbox.transaction { transaction -> Signal in if let accountState = (transaction.getState() as? AuthorizedAccountState)?.state, let peer = transaction.getPeer(peerId) { var channelStates: [PeerId: AccountStateChannelState] = [:] if let channelState = transaction.getPeerChatState(peerId) as? ChannelState { @@ -1635,6 +1635,9 @@ func keepPollingChannel(postbox: Postbox, network: Network, peerId: PeerId, stat } |> switchToLatest |> restart + + return signal + |> delay(5.0, queue: .concurrentDefaultQueue()) } private func resetChannels(network: Network, peers: [Peer], state: AccountMutableState) -> Signal { @@ -1751,7 +1754,11 @@ private func resetChannels(network: Network, peers: [Peer], state: AccountMutabl for message in storeMessages { if case let .Id(id) = message.id, id.namespace == Namespaces.Message.Cloud { - updatedState.setNeedsHoleFromPreviousState(peerId: id.peerId, namespace: id.namespace) + var channelPts: Int32? + if let state = channelStates[id.peerId] { + channelPts = state.pts + } + updatedState.setNeedsHoleFromPreviousState(peerId: id.peerId, namespace: id.namespace, validateChannelPts: channelPts) channelSynchronizedUntilMessage[id.peerId] = id.id } } @@ -1841,7 +1848,11 @@ private func pollChannel(network: Network, peer: Peer, state: AccountMutableStat updatedState.mergeUsers(users) for apiMessage in newMessages { - if let message = StoreMessage(apiMessage: apiMessage) { + if var message = StoreMessage(apiMessage: apiMessage) { + var attributes = message.attributes + attributes.append(ChannelMessageStateVersionAttribute(pts: pts)) + message = message.withUpdatedAttributes(attributes) + if let preCachedResources = apiMessage.preCachedResources { for (resource, data) in preCachedResources { updatedState.addPreCachedResource(resource, data: data) @@ -1941,10 +1952,14 @@ private func pollChannel(network: Network, peer: Peer, state: AccountMutableStat updatedState.mergeChats(chats) updatedState.mergeUsers(users) - updatedState.setNeedsHoleFromPreviousState(peerId: peer.peerId, namespace: Namespaces.Message.Cloud) + updatedState.setNeedsHoleFromPreviousState(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, validateChannelPts: pts) for apiMessage in messages { - if let message = StoreMessage(apiMessage: apiMessage) { + if var message = StoreMessage(apiMessage: apiMessage) { + var attributes = message.attributes + attributes.append(ChannelMessageStateVersionAttribute(pts: pts)) + message = message.withUpdatedAttributes(attributes) + if let preCachedResources = apiMessage.preCachedResources { for (resource, data) in preCachedResources { updatedState.addPreCachedResource(resource, data: data) @@ -2175,9 +2190,14 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP var syncChatListFilters = false var holesFromPreviousStateMessageIds: [MessageId] = [] + var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:] for (peerId, namespaces) in finalState.state.namespacesWithHolesFromPreviousState { - for namespace in namespaces { + for (namespace, namespaceState) in namespaces { + if let pts = namespaceState.validateChannelPts { + clearHolesFromPreviousStateForChannelMessagesWithPts[PeerIdAndMessageNamespace(peerId: peerId, namespace: namespace)] = pts + } + var topId: Int32? if namespace == Namespaces.Message.Cloud, let channelState = transaction.getPeerChatState(peerId) as? ChannelState { if let synchronizedUntilMessageId = channelState.synchronizedUntilMessageId { @@ -2838,6 +2858,36 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP } } + for (peerIdAndNamespace, pts) in clearHolesFromPreviousStateForChannelMessagesWithPts { + var upperMessageId: Int32? + var lowerMessageId: Int32? + transaction.scanMessageAttributes(peerId: peerIdAndNamespace.peerId, namespace: peerIdAndNamespace.namespace, { id, attributes in + for attribute in attributes { + if let attribute = attribute as? ChannelMessageStateVersionAttribute { + if attribute.pts >= pts { + if upperMessageId == nil { + upperMessageId = id.id + } + if let lowerMessageIdValue = lowerMessageId { + lowerMessageId = min(id.id, lowerMessageIdValue) + } else { + lowerMessageId = id.id + } + return true + } else { + return false + } + } + } + return false + }) + if let upperMessageId = upperMessageId, let lowerMessageId = lowerMessageId { + if upperMessageId != lowerMessageId { + transaction.removeHole(peerId: peerIdAndNamespace.peerId, namespace: peerIdAndNamespace.namespace, space: .everywhere, range: lowerMessageId ... upperMessageId) + } + } + } + if !peerActivityTimestamps.isEmpty { updatePeerPresenceLastActivities(transaction: transaction, accountPeerId: accountPeerId, activities: peerActivityTimestamps) } diff --git a/submodules/TelegramCore/Sources/AccountViewTracker.swift b/submodules/TelegramCore/Sources/AccountViewTracker.swift index b2e591aba4..b34eaede56 100644 --- a/submodules/TelegramCore/Sources/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/AccountViewTracker.swift @@ -1111,13 +1111,13 @@ public final class AccountViewTracker { let combinedDisposable = MetaDisposable() self.queue.async { var addHole = false - /*if let context = self.channelPollingContexts[peerId] { + if let context = self.channelPollingContexts[peerId] { if context.subscribers.isEmpty { addHole = true } } else { addHole = true - }*/ + } if addHole { let _ = self.account?.postbox.transaction({ transaction -> Void in if transaction.getPeerChatListIndex(peerId) == nil { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 8d8807ab29..33aca30807 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1435,7 +1435,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { loadState = .loading } + var animateIn = false if strongSelf.loadState != loadState { + if case .loading = strongSelf.loadState { + if case .messages = loadState { + animateIn = true + } + } strongSelf.loadState = loadState strongSelf.loadStateUpdated?(loadState, animated) } @@ -1478,8 +1484,24 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf._buttonKeyboardMessage.set(.single(transition.keyboardButtonsMessage)) } - if transition.animateIn { - strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + if transition.animateIn || animateIn { + let heightNorm = strongSelf.bounds.height - strongSelf.insets.top + strongSelf.forEachVisibleItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + let delayFactor = itemNode.frame.minY / heightNorm + let delay = Double(delayFactor * 0.1) + + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) + itemNode.layer.animateScale(from: 0.9, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) + } + } + strongSelf.forEachItemHeaderNode { itemNode in + let delayFactor = itemNode.frame.minY / heightNorm + let delay = Double(delayFactor * 0.2) + + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) + itemNode.layer.animateScale(from: 0.9, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) + } } if let scrolledToIndex = transition.scrolledToIndex { diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index e74f25cafe..75ed081bcb 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -83,7 +83,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, account: A } var scrollPosition: ChatHistoryViewScrollPosition? - if let maxReadIndex = view.maxReadIndex, tagMask == nil { + if let maxReadIndex = view.maxReadIndex, tagMask == nil, view.isAddedToChatList { let aroundIndex = maxReadIndex scrollPosition = .unread(index: maxReadIndex) @@ -120,7 +120,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, account: A } } } - } else if let historyScrollState = (initialData?.chatInterfaceState as? ChatInterfaceState)?.historyScrollState, tagMask == nil { + } else if view.isAddedToChatList, let historyScrollState = (initialData?.chatInterfaceState as? ChatInterfaceState)?.historyScrollState, tagMask == nil { scrollPosition = .positionRestoration(index: historyScrollState.messageIndex, relativeOffset: CGFloat(historyScrollState.relativeOffset)) } else { if view.entries.isEmpty && (view.holeEarlier || view.holeLater) {