import Foundation import Postbox import TelegramCore import SwiftSignalKit import Display import MergeLists import SearchUI import TelegramUIPreferences struct ChatListNodeView { let originalList: EngineChatList let filteredEntries: [ChatListNodeEntry] let isLoading: Bool let filter: ChatListFilter? } enum ChatListNodeViewTransitionReason { case initial case interactiveChanges case holeChanges case reload } struct ChatListNodeViewTransitionInsertEntry { let index: Int let previousIndex: Int? let entry: ChatListNodeEntry let directionHint: ListViewItemOperationDirectionHint? } struct ChatListNodeViewTransitionUpdateEntry { let index: Int let previousIndex: Int let entry: ChatListNodeEntry let directionHint: ListViewItemOperationDirectionHint? } struct ChatListNodeViewTransition { let chatListView: ChatListNodeView let deleteItems: [ListViewDeleteItem] let insertEntries: [ChatListNodeViewTransitionInsertEntry] let updateEntries: [ChatListNodeViewTransitionUpdateEntry] let options: ListViewDeleteAndInsertOptions let scrollToItem: ListViewScrollToItem? let stationaryItemRange: (Int, Int)? let adjustScrollToFirstItem: Bool let animateCrossfade: Bool } enum ChatListNodeViewScrollPosition { case index(index: ChatListIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) } func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toView: ChatListNodeView, reason: ChatListNodeViewTransitionReason, previewing: Bool, disableAnimations: Bool, account: Account, scrollPosition: ChatListNodeViewScrollPosition?, searchMode: Bool) -> Signal { return Signal { subscriber in let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) var adjustedDeleteIndices: [ListViewDeleteItem] = [] let previousCount: Int if let fromView = fromView { previousCount = fromView.filteredEntries.count } else { previousCount = 0 } for index in deleteIndices { adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil)) } var adjustedIndicesAndItems: [ChatListNodeViewTransitionInsertEntry] = [] var adjustedUpdateItems: [ChatListNodeViewTransitionUpdateEntry] = [] let updatedCount = toView.filteredEntries.count var options: ListViewDeleteAndInsertOptions = [] var maxAnimatedInsertionIndex = -1 var scrollToItem: ListViewScrollToItem? switch reason { case .initial: let _ = options.insert(.LowLatency) let _ = options.insert(.Synchronous) case .interactiveChanges: for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) { let adjustedIndex = updatedCount - 1 - index if adjustedIndex == maxAnimatedInsertionIndex + 1 { maxAnimatedInsertionIndex += 1 } } var minTimestamp: Int32? var maxTimestamp: Int32? for (_, item, _) in indicesAndItems { if case .PeerEntry = item, case let .index(index) = item.sortIndex, case let .chatList(chatListIndex) = index, chatListIndex.pinningIndex == nil { let timestamp = chatListIndex.messageIndex.timestamp if minTimestamp == nil || timestamp < minTimestamp! { minTimestamp = timestamp } if maxTimestamp == nil || timestamp > maxTimestamp! { maxTimestamp = timestamp } } } let _ = options.insert(.AnimateAlpha) if !disableAnimations { let _ = options.insert(.AnimateInsertion) } case .reload: break case .holeChanges: break } for (index, entry, previousIndex) in indicesAndItems { let adjustedIndex = updatedCount - 1 - index let adjustedPrevousIndex: Int? if let previousIndex = previousIndex { adjustedPrevousIndex = previousCount - 1 - previousIndex } else { adjustedPrevousIndex = nil } var directionHint: ListViewItemOperationDirectionHint? if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex { directionHint = .Down } adjustedIndicesAndItems.append(ChatListNodeViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry, directionHint: directionHint)) } for (index, entry, previousIndex) in updateIndices { let adjustedIndex = updatedCount - 1 - index let adjustedPreviousIndex = previousCount - 1 - previousIndex let directionHint: ListViewItemOperationDirectionHint? = nil adjustedUpdateItems.append(ChatListNodeViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint)) } if let scrollPosition = scrollPosition { switch scrollPosition { case let .index(scrollIndex, position, directionHint, animated): var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if entry.sortIndex >= .index(.chatList(scrollIndex)) { scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint) break } index -= 1 } if scrollToItem == nil { var index = 0 for entry in toView.filteredEntries.reversed() { if entry.sortIndex < .index(.chatList(scrollIndex)) { scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint) break } index += 1 } } } } var fromEmptyView: Bool fromEmptyView = false var animateCrossfade: Bool animateCrossfade = false if let fromView = fromView { var wasSingleHeader = false if fromView.filteredEntries.count == 1, case .HeaderEntry = fromView.filteredEntries[0] { wasSingleHeader = true } var isSingleHeader = false if toView.filteredEntries.count == 1, case .HeaderEntry = toView.filteredEntries[0] { isSingleHeader = true } if (wasSingleHeader || isSingleHeader), case .interactiveChanges = reason { if wasSingleHeader != isSingleHeader { if wasSingleHeader { animateCrossfade = true options.remove(.AnimateInsertion) options.remove(.AnimateAlpha) } else { let _ = options.insert(.AnimateInsertion) } } } else if fromView.filteredEntries.isEmpty || fromView.filter != toView.filter { var updateEmpty = true if !fromView.filteredEntries.isEmpty, let fromFilter = fromView.filter, let toFilter = toView.filter, case var .filter(_, _, _, fromData) = fromFilter, case let .filter(_, _, _, toData) = toFilter, fromData.includePeers.pinnedPeers != toData.includePeers.pinnedPeers { fromData.includePeers = toData.includePeers if fromData == toData { options.insert(.AnimateInsertion) updateEmpty = false } } if updateEmpty { options.remove(.AnimateInsertion) options.remove(.AnimateAlpha) fromEmptyView = true } } } else { fromEmptyView = true } if let fromView = fromView, !fromView.isLoading, toView.isLoading { options.remove(.AnimateInsertion) options.remove(.AnimateAlpha) } var adjustScrollToFirstItem = false if !previewing && !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 1 { adjustScrollToFirstItem = true } subscriber.putNext(ChatListNodeViewTransition(chatListView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: nil, adjustScrollToFirstItem: adjustScrollToFirstItem, animateCrossfade: animateCrossfade)) subscriber.putCompletion() return EmptyDisposable } }