ChatHistoryListNode: rewrite transition queue

ChatMessageBubbleItemNode: another possible crash point
This commit is contained in:
Peter 2019-06-13 01:12:50 +01:00
parent bb94bd7e88
commit 819583121e
6 changed files with 395 additions and 432 deletions

View File

@ -6,7 +6,7 @@ import MtProtoKitMac
#else #else
import Postbox import Postbox
import SwiftSignalKit import SwiftSignalKit
#if BUCK #if BUCK
import MtProtoKit import MtProtoKit
#else #else
import MtProtoKitDynamic import MtProtoKitDynamic

View File

@ -262,7 +262,8 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
let previousView = Atomic<ChatHistoryView?>(value: nil) let previousView = Atomic<ChatHistoryView?>(value: nil)
let historyViewTransition = combineLatest(historyViewUpdate, self.chatPresentationDataPromise.get()) |> mapToQueue { [weak self] update, chatPresentationData -> Signal<ChatHistoryGridViewTransition, NoError> in let historyViewTransition = combineLatest(queue: messageViewQueue, historyViewUpdate, self.chatPresentationDataPromise.get())
|> mapToQueue { [weak self] update, chatPresentationData -> Signal<ChatHistoryGridViewTransition, NoError> in
switch update { switch update {
case .Loading: case .Loading:
Queue.mainQueue().async { [weak self] in Queue.mainQueue().async { [weak self] in
@ -283,11 +284,9 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
return .complete() return .complete()
case let .HistoryView(view, type, scrollPosition, flashIndicators, _, _, id): case let .HistoryView(view, type, scrollPosition, flashIndicators, _, _, id):
let reason: ChatHistoryViewTransitionReason let reason: ChatHistoryViewTransitionReason
var prepareOnMainQueue = false
switch type { switch type {
case let .Initial(fadeIn): case let .Initial(fadeIn):
reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn) reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn)
prepareOnMainQueue = !fadeIn
case let .Generic(genericType): case let .Generic(genericType):
switch genericType { switch genericType {
case .InitialUnread, .Initial: case .InitialUnread, .Initial:
@ -304,7 +303,9 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(location: .peer(peerId), view: view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, includeSearchEntry: false, reverse: false, groupMessages: false, selectedMessages: nil, presentationData: chatPresentationData, historyAppearsCleared: false), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: false), id: id) let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(location: .peer(peerId), view: view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, includeSearchEntry: false, reverse: false, groupMessages: false, selectedMessages: nil, presentationData: chatPresentationData, historyAppearsCleared: false), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: false), id: id)
let previous = previousView.swap(processedView) let previous = previousView.swap(processedView)
return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: false, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil, flashIndicators: flashIndicators) |> map({ mappedChatHistoryViewListTransition(context: context, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, presentationData: chatPresentationData) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: false, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil, flashIndicators: flashIndicators)
let mappedTransition = mappedChatHistoryViewListTransition(context: context, peerId: peerId, controllerInteraction: controllerInteraction, transition: rawTransition, from: previous, presentationData: chatPresentationData)
return .single(mappedTransition)
} }
} }

View File

@ -323,7 +323,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private let messageViewQueue = Queue(name: "ChatHistoryListNode processing") private let messageViewQueue = Queue(name: "ChatHistoryListNode processing")
private var dequeuedInitialTransitionOnLayout = false private var dequeuedInitialTransitionOnLayout = false
private var enqueuedHistoryViewTransition: (ChatHistoryListViewTransition, () -> Void)? private var enqueuedHistoryViewTransitions: [ChatHistoryListViewTransition] = []
private var hasActiveTransition = false
var layoutActionOnViewTransition: ((ChatHistoryListViewTransition) -> (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?))? var layoutActionOnViewTransition: ((ChatHistoryListViewTransition) -> (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?))?
public let historyState = ValuePromise<ChatHistoryNodeHistoryState>() public let historyState = ValuePromise<ChatHistoryNodeHistoryState>()
@ -518,9 +519,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let nextTransitionVersion = Atomic<Int>(value: 0) let nextTransitionVersion = Atomic<Int>(value: 0)
let historyViewTransition = combineLatest(queue: messageViewQueue, historyViewUpdate, self.chatPresentationDataPromise.get(), selectedMessages, automaticDownloadNetworkType, self.historyAppearsClearedPromise.get()) let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, historyViewUpdate, self.chatPresentationDataPromise.get(), selectedMessages, automaticDownloadNetworkType, self.historyAppearsClearedPromise.get()).start(next: { [weak self] update, chatPresentationData, selectedMessages, networkType, historyAppearsCleared in
|> introduceError(Void.self)
|> mapToQueue { [weak self] update, chatPresentationData, selectedMessages, networkType, historyAppearsCleared -> Signal<(ChatHistoryListViewTransition, Int), Void> in
func applyHole() { func applyHole() {
Queue.mainQueue().async { Queue.mainQueue().async {
if let strongSelf = self { if let strongSelf = self {
@ -546,7 +545,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
case let .Loading(combinedInitialData, type): case let .Loading(combinedInitialData, type):
if case .Generic(.FillHole) = type { if case .Generic(.FillHole) = type {
applyHole() applyHole()
return .fail(Void()) return
} }
initialData = combinedInitialData initialData = combinedInitialData
@ -572,11 +571,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
} }
} }
return .complete() return
case let .HistoryView(view, type, scrollPosition, flashIndicators, originalScrollPosition, data, id): case let .HistoryView(view, type, scrollPosition, flashIndicators, originalScrollPosition, data, id):
if case .Generic(.FillHole) = type { if case .Generic(.FillHole) = type {
applyHole() applyHole()
return .fail(Void()) return
} }
initialData = data initialData = data
@ -616,7 +615,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
let reason: ChatHistoryViewTransitionReason let reason: ChatHistoryViewTransitionReason
var prepareOnMainQueue = false
let previousHistoryAppearsClearedValue = previousHistoryAppearsCleared.swap(historyAppearsCleared) let previousHistoryAppearsClearedValue = previousHistoryAppearsCleared.swap(historyAppearsCleared)
if previousHistoryAppearsClearedValue != nil && previousHistoryAppearsClearedValue != historyAppearsCleared && !historyAppearsCleared { if previousHistoryAppearsClearedValue != nil && previousHistoryAppearsClearedValue != historyAppearsCleared && !historyAppearsCleared {
@ -628,7 +626,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
switch type { switch type {
case let .Initial(fadeIn): case let .Initial(fadeIn):
reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn) reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn)
prepareOnMainQueue = !fadeIn
case let .Generic(genericType): case let .Generic(genericType):
switch genericType { switch genericType {
case .InitialUnread, .Initial: case .InitialUnread, .Initial:
@ -642,41 +639,18 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
} }
} }
let transitionVersion = nextTransitionVersion.modify { $0 + 1 } let rawTransition = 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)
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) let mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, transition: rawTransition)
|> map({ Queue.mainQueue().async {
(mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, transition: $0), transitionVersion) guard let strongSelf = self else {
return
}
strongSelf.enqueueHistoryViewTransition(mappedTransition)
}
}
}) })
|> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
|> introduceError(Void.self)
}
}
let appliedTransitionVersion = Atomic<Int?>(value: nil) self.historyDisposable.set(historyViewTransitionDisposable)
let appliedTransition = historyViewTransition
|> deliverOnMainQueue
|> mapToQueue { [weak self] (transition, version) -> Signal<Void, Void> in
if let strongSelf = self {
let previousAppliedVersion = appliedTransitionVersion.swap(version) ?? 0
if !GlobalExperimentalSettings.isAppStoreBuild {
precondition(version == previousAppliedVersion + 1)
}
assert(version == previousAppliedVersion + 1)
return strongSelf.enqueueHistoryViewTransition(transition)
|> introduceError(Void.self)
}
return .complete()
}
let restartedTransition = (
appliedTransition
|> `catch` { _ -> Signal<Void, Void> in
return .complete()
}
)
|> restart
self.historyDisposable.set(restartedTransition.start())
let previousMaxIncomingMessageIndexByNamespace = Atomic<[MessageId.Namespace: MessageIndex]>(value: [:]) let previousMaxIncomingMessageIndexByNamespace = Atomic<[MessageId.Namespace: MessageIndex]>(value: [:])
let readHistory = combineLatest(self.maxVisibleIncomingMessageIndex.get(), self.canReadHistory.get()) let readHistory = combineLatest(self.maxVisibleIncomingMessageIndex.get(), self.canReadHistory.get())
@ -743,7 +717,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in self.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in
if let strongSelf = self, let transactionState = opaqueTransactionState as? ChatHistoryTransactionOpaqueState { if let strongSelf = self, let transactionState = opaqueTransactionState as? ChatHistoryTransactionOpaqueState {
self?.processDisplayedItemRangeChanged(displayedRange: displayedRange, transactionState: transactionState) strongSelf.processDisplayedItemRangeChanged(displayedRange: displayedRange, transactionState: transactionState)
} }
} }
@ -1204,33 +1178,19 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.maxVisibleIncomingMessageIndex.set(index) self.maxVisibleIncomingMessageIndex.set(index)
} }
private func enqueueHistoryViewTransition(_ transition: ChatHistoryListViewTransition) -> Signal<Void, NoError> { private func enqueueHistoryViewTransition(_ transition: ChatHistoryListViewTransition) {
return Signal { [weak self] subscriber in self.enqueuedHistoryViewTransitions.append(transition)
if let strongSelf = self { self.prefetchManager.updateOptions(InChatPrefetchOptions(networkType: transition.networkType, peerType: transition.peerType))
if let _ = strongSelf.enqueuedHistoryViewTransition {
preconditionFailure() if !self.didSetInitialData {
self.didSetInitialData = true
self._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData)))
} }
strongSelf.prefetchManager.updateOptions(InChatPrefetchOptions(networkType: transition.networkType, peerType: transition.peerType)) if self.isNodeLoaded {
self.dequeueHistoryViewTransitions()
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 { } else {
strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages))) self._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages)))
let loadState: ChatHistoryNodeLoadState let loadState: ChatHistoryNodeLoadState
if transition.historyView.filteredEntries.isEmpty { if transition.historyView.filteredEntries.isEmpty {
@ -1238,28 +1198,25 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} else { } else {
loadState = .messages loadState = .messages
} }
if strongSelf.loadState != loadState { if self.loadState != loadState {
strongSelf.loadState = loadState self.loadState = loadState
strongSelf.loadStateUpdated?(loadState, transition.options.contains(.AnimateInsertion)) self.loadStateUpdated?(loadState, transition.options.contains(.AnimateInsertion))
} }
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty)
if strongSelf.currentHistoryState != historyState { if self.currentHistoryState != historyState {
strongSelf.currentHistoryState = historyState self.currentHistoryState = historyState
strongSelf.historyState.set(historyState) self.historyState.set(historyState)
} }
} }
} else {
subscriber.putCompletion()
} }
return EmptyDisposable private func dequeueHistoryViewTransitions() {
} |> runOn(Queue.mainQueue()) if self.enqueuedHistoryViewTransitions.isEmpty || self.hasActiveTransition {
return
} }
self.hasActiveTransition = true
private func dequeueHistoryViewTransition() { let transition = self.enqueuedHistoryViewTransitions.removeFirst()
if let (transition, completion) = self.enqueuedHistoryViewTransition {
self.enqueuedHistoryViewTransition = nil
let animated = transition.options.contains(.AnimateInsertion) let animated = transition.options.contains(.AnimateInsertion)
@ -1325,7 +1282,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
} }
completion() if let scrolledToIndex = transition.scrolledToIndex {
if let strongSelf = self {
strongSelf.scrolledToIndex?(scrolledToIndex)
}
}
strongSelf.hasActiveTransition = false
strongSelf.dequeueHistoryViewTransitions()
} }
} }
@ -1342,7 +1306,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
//self.flashHeaderItems() //self.flashHeaderItems()
} }
} }
}
public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) { public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) {
self.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: 0.0, scrollToTop: false) self.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: 0.0, scrollToTop: false)
@ -1357,7 +1320,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if !self.dequeuedInitialTransitionOnLayout { if !self.dequeuedInitialTransitionOnLayout {
self.dequeuedInitialTransitionOnLayout = true self.dequeuedInitialTransitionOnLayout = true
self.dequeueHistoryViewTransition() self.dequeueHistoryViewTransitions()
} }
} }

View File

@ -1537,6 +1537,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
for (relativeFrame, _, apply) in contentNodeFramesPropertiesAndApply { for (relativeFrame, _, apply) in contentNodeFramesPropertiesAndApply {
apply(animation, synchronousLoads) apply(animation, synchronousLoads)
if contentNodeIndex >= strongSelf.contentNodes.count {
break
}
let contentNode = strongSelf.contentNodes[contentNodeIndex] let contentNode = strongSelf.contentNodes[contentNodeIndex]
let contentNodeFrame = relativeFrame.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y) let contentNodeFrame = relativeFrame.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y)
let previousContentNodeFrame = contentNode.frame let previousContentNodeFrame = contentNode.frame

View File

@ -4,8 +4,7 @@ import Postbox
import TelegramCore import TelegramCore
import Display import Display
func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, reverse: Bool, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?, flashIndicators: Bool) -> Signal<ChatHistoryViewTransition, NoError> { func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, reverse: Bool, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?, flashIndicators: Bool) -> ChatHistoryViewTransition {
return Signal { subscriber in
let mergeResult: (deleteIndices: [Int], indicesAndItems: [(Int, ChatHistoryEntry, Int?)], updateIndices: [(Int, ChatHistoryEntry, Int)]) let mergeResult: (deleteIndices: [Int], indicesAndItems: [(Int, ChatHistoryEntry, Int?)], updateIndices: [(Int, ChatHistoryEntry, Int)])
let allUpdated = fromView?.associatedData != toView.associatedData let allUpdated = fromView?.associatedData != toView.associatedData
if reverse { if reverse {
@ -192,9 +191,5 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie
} }
} }
subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData, scrolledToIndex: scrolledToIndex, animateIn: animateIn, reason: reason, flashIndicators: flashIndicators)) return ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData, scrolledToIndex: scrolledToIndex, animateIn: animateIn, reason: reason, flashIndicators: flashIndicators)
subscriber.putCompletion()
return EmptyDisposable
}
} }

View File

@ -7,12 +7,12 @@
<key>Lottie.xcscheme_^#shared#^_</key> <key>Lottie.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>4</integer>
</dict> </dict>
<key>Lottie_iOS.xcscheme_^#shared#^_</key> <key>Lottie_iOS.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>32</integer> <integer>31</integer>
</dict> </dict>
</dict> </dict>
</dict> </dict>