mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
323 lines
16 KiB
Swift
323 lines
16 KiB
Swift
import Foundation
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
|
|
private struct ChatMediaInputPanelTransition {
|
|
let deletions: [ListViewDeleteItem]
|
|
let insertions: [ListViewInsertItem]
|
|
let updates: [ListViewUpdateItem]
|
|
}
|
|
|
|
private struct ChatMediaInputGridTransition {
|
|
let deletions: [Int]
|
|
let insertions: [GridNodeInsertItem]
|
|
let updates: [GridNodeUpdateItem]
|
|
let updateFirstIndexInSectionOffset: Int?
|
|
let stationaryItems: GridNodeStationaryItems
|
|
}
|
|
|
|
private func preparedChatMediaInputPanelEntryTransition(account: Account, from fromEntries: [ChatMediaInputPanelEntry], to toEntries: [ChatMediaInputPanelEntry], inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputPanelTransition {
|
|
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
|
|
|
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
|
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction), directionHint: nil) }
|
|
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction), directionHint: nil) }
|
|
|
|
return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates)
|
|
}
|
|
|
|
private func preparedChatMediaInputGridEntryTransition(account: Account, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputGridTransition {
|
|
var stationaryItems: GridNodeStationaryItems = .none
|
|
switch update {
|
|
case .generic:
|
|
break
|
|
case .scroll:
|
|
var fromStableIds = Set<ChatMediaInputGridEntryStableId>()
|
|
for entry in fromEntries {
|
|
fromStableIds.insert(entry.stableId)
|
|
}
|
|
var index = 0
|
|
var indices = Set<Int>()
|
|
for entry in toEntries {
|
|
if fromStableIds.contains(entry.stableId) {
|
|
indices.insert(index)
|
|
}
|
|
index += 1
|
|
}
|
|
stationaryItems = .indices(indices)
|
|
}
|
|
|
|
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
|
|
|
let deletions = deleteIndices
|
|
let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) }
|
|
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction)) }
|
|
|
|
var firstIndexInSectionOffset = 0
|
|
if !toEntries.isEmpty {
|
|
firstIndexInSectionOffset = Int(toEntries[0].index.itemIndex.index)
|
|
}
|
|
|
|
return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems)
|
|
}
|
|
|
|
private func chatMediaInputPanelEntries(view: ItemCollectionsView) -> [ChatMediaInputPanelEntry] {
|
|
var entries: [ChatMediaInputPanelEntry] = []
|
|
var index = 0
|
|
for (_, info, item) in view.collectionInfos {
|
|
if let info = info as? StickerPackCollectionInfo {
|
|
entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem))
|
|
index += 1
|
|
}
|
|
}
|
|
return entries
|
|
}
|
|
|
|
private func chatMediaInputGridEntries(view: ItemCollectionsView) -> [ChatMediaInputGridEntry] {
|
|
var entries: [ChatMediaInputGridEntry] = []
|
|
|
|
var stickerPackInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:]
|
|
for (id, info, _) in view.collectionInfos {
|
|
if let info = info as? StickerPackCollectionInfo {
|
|
stickerPackInfos[id] = info
|
|
}
|
|
}
|
|
|
|
for entry in view.entries {
|
|
if let item = entry.item as? StickerPackItem {
|
|
entries.append(ChatMediaInputGridEntry(index: entry.index, stickerItem: item, stickerPackInfo: stickerPackInfos[entry.index.collectionId]))
|
|
}
|
|
}
|
|
return entries
|
|
}
|
|
|
|
private enum StickerPacksCollectionPosition: Equatable {
|
|
case initial
|
|
case scroll(aroundIndex: ItemCollectionViewEntryIndex)
|
|
|
|
static func ==(lhs: StickerPacksCollectionPosition, rhs: StickerPacksCollectionPosition) -> Bool {
|
|
switch lhs {
|
|
case .initial:
|
|
if case .initial = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .scroll(aroundIndex):
|
|
if case .scroll(aroundIndex) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum StickerPacksCollectionUpdate {
|
|
case generic
|
|
case scroll
|
|
}
|
|
|
|
final class ChatMediaInputNodeInteraction {
|
|
var highlightedItemCollectionId: ItemCollectionId?
|
|
}
|
|
|
|
final class ChatMediaInputNode: ChatInputNode {
|
|
private let account: Account
|
|
private let controllerInteraction: ChatControllerInteraction
|
|
|
|
private var inputNodeInteraction: ChatMediaInputNodeInteraction!
|
|
|
|
private let collectionListPanel: ASDisplayNode
|
|
private let collectionListSeparator: ASDisplayNode
|
|
|
|
private let disposable = MetaDisposable()
|
|
|
|
private let listView: ListView
|
|
private let gridNode: GridNode
|
|
|
|
private let itemCollectionsViewPosition = Promise<StickerPacksCollectionPosition>()
|
|
private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition?
|
|
private var currentView: ItemCollectionsView?
|
|
|
|
init(account: Account, controllerInteraction: ChatControllerInteraction) {
|
|
self.account = account
|
|
self.controllerInteraction = controllerInteraction
|
|
|
|
self.collectionListPanel = ASDisplayNode()
|
|
self.collectionListPanel.backgroundColor = UIColor(0xF5F6F8)
|
|
|
|
self.collectionListSeparator = ASDisplayNode()
|
|
self.collectionListSeparator.isLayerBacked = true
|
|
self.collectionListSeparator.backgroundColor = UIColor(0xBEC2C6)
|
|
|
|
self.listView = ListView()
|
|
self.listView.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0)
|
|
|
|
self.gridNode = GridNode()
|
|
|
|
super.init()
|
|
|
|
self.inputNodeInteraction = ChatMediaInputNodeInteraction()
|
|
|
|
self.clipsToBounds = true
|
|
self.backgroundColor = UIColor(0xE8EBF0)
|
|
|
|
self.addSubnode(self.collectionListPanel)
|
|
self.addSubnode(self.collectionListSeparator)
|
|
self.addSubnode(self.listView)
|
|
self.addSubnode(self.gridNode)
|
|
|
|
let itemCollectionsView = self.itemCollectionsViewPosition.get()
|
|
|> distinctUntilChanged
|
|
|> mapToSignal { position -> Signal<(ItemCollectionsView, StickerPacksCollectionUpdate), NoError> in
|
|
switch position {
|
|
case .initial:
|
|
return account.postbox.itemCollectionsView(namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50)
|
|
|> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in
|
|
return (view, .generic)
|
|
}
|
|
case let .scroll(aroundIndex):
|
|
var firstTime = true
|
|
return account.postbox.itemCollectionsView(namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 140)
|
|
|> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in
|
|
let update: StickerPacksCollectionUpdate
|
|
if firstTime {
|
|
firstTime = false
|
|
update = .scroll
|
|
} else {
|
|
update = .generic
|
|
}
|
|
return (view, update)
|
|
}
|
|
}
|
|
}
|
|
|
|
let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], []))
|
|
|
|
let inputNodeInteraction = self.inputNodeInteraction!
|
|
|
|
let transitions = itemCollectionsView
|
|
|> map { (view, update) -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in
|
|
let panelEntries = chatMediaInputPanelEntries(view: view)
|
|
let gridEntries = chatMediaInputGridEntries(view: view)
|
|
let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries))
|
|
return (view, preparedChatMediaInputPanelEntryTransition(account: account, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: account, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty)
|
|
}
|
|
|
|
self.disposable.set((transitions |> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, panelFirstTime, gridTransition, gridFirstTime) in
|
|
if let strongSelf = self {
|
|
strongSelf.currentView = view
|
|
strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime)
|
|
strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime)
|
|
}
|
|
}))
|
|
|
|
self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in
|
|
if let strongSelf = self {
|
|
if let topVisible = visibleItems.topVisible {
|
|
if let item = topVisible.1 as? ChatMediaInputStickerGridItem {
|
|
let collectionId = item.index.collectionId
|
|
if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId {
|
|
strongSelf.inputNodeInteraction.highlightedItemCollectionId = collectionId
|
|
var selectedItemNode: ChatMediaInputStickerPackItemNode?
|
|
strongSelf.listView.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode {
|
|
itemNode.updateIsHighlighted()
|
|
if itemNode.currentCollectionId == collectionId {
|
|
strongSelf.listView.ensureItemNodeVisible(itemNode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let currentView = strongSelf.currentView, let (topIndex, topItem) = visibleItems.top, let (bottomIndex, bottomItem) = visibleItems.bottom {
|
|
if topIndex <= 10 && currentView.lower != nil {
|
|
let position: StickerPacksCollectionPosition = .scroll(aroundIndex: (topItem as! ChatMediaInputStickerGridItem).index)
|
|
if strongSelf.currentStickerPacksCollectionPosition != position {
|
|
strongSelf.currentStickerPacksCollectionPosition = position
|
|
strongSelf.itemCollectionsViewPosition.set(.single(position))
|
|
}
|
|
} else if bottomIndex >= visibleItems.count - 10 && currentView.higher != nil {
|
|
let position: StickerPacksCollectionPosition = .scroll(aroundIndex: (bottomItem as! ChatMediaInputStickerGridItem).index)
|
|
if strongSelf.currentStickerPacksCollectionPosition != position {
|
|
strongSelf.currentStickerPacksCollectionPosition = position
|
|
strongSelf.itemCollectionsViewPosition.set(.single(position))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.currentStickerPacksCollectionPosition = .initial
|
|
self.itemCollectionsViewPosition.set(.single(.initial))
|
|
}
|
|
|
|
deinit {
|
|
self.disposable.dispose()
|
|
}
|
|
|
|
override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat {
|
|
let separatorHeight = UIScreenPixel
|
|
|
|
transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: 41.0)))
|
|
transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0), size: CGSize(width: width, height: separatorHeight)))
|
|
|
|
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width)
|
|
self.listView.position = CGPoint(x: width / 2.0, y: 41.0 / 2.0)
|
|
|
|
var duration: Double = 0.0
|
|
var curve: UInt = 0
|
|
switch transition {
|
|
case .immediate:
|
|
break
|
|
case let .animated(animationDuration, animationCurve):
|
|
duration = animationDuration
|
|
switch animationCurve {
|
|
case .easeInOut:
|
|
break
|
|
case .spring:
|
|
curve = 7
|
|
}
|
|
}
|
|
|
|
let listViewCurve: ListViewAnimationCurve
|
|
if curve == 7 {
|
|
listViewCurve = .Spring(duration: duration)
|
|
} else {
|
|
listViewCurve = .Default
|
|
}
|
|
|
|
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: width), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), duration: duration, curve: listViewCurve)
|
|
|
|
self.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, completion: { _ in })
|
|
|
|
self.gridNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 41.0), size: CGSize(width: width, height: 258.0 - 41.0))
|
|
|
|
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: width, height: 258.0 - 41.0), insets: UIEdgeInsets(), preloadSize: 300.0, itemSize: CGSize(width: 75.0, height: 75.0)), transition: .immediate), stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
|
|
|
return 258.0
|
|
}
|
|
|
|
private func enqueuePanelTransition(_ transition: ChatMediaInputPanelTransition, firstTime: Bool) {
|
|
var options = ListViewDeleteAndInsertOptions()
|
|
if firstTime {
|
|
options.insert(.Synchronous)
|
|
options.insert(.LowLatency)
|
|
} else {
|
|
options.insert(.AnimateInsertion)
|
|
}
|
|
self.listView.deleteAndInsertItems(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, completion: { [weak self] _ in
|
|
})
|
|
}
|
|
|
|
private func enqueueGridTransition(_ transition: ChatMediaInputGridTransition, firstTime: Bool) {
|
|
self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in })
|
|
}
|
|
}
|