import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData import TelegramUIPreferences import ItemListUI import PresentationDataUtils import HorizontalPeerItem import AccountContext import MergeLists private struct PeersEntry: Comparable, Identifiable { let index: Int let peer: EnginePeer let theme: PresentationTheme let strings: PresentationStrings var stableId: EnginePeer.Id { return self.peer.id } static func ==(lhs: PeersEntry, rhs: PeersEntry) -> Bool { if lhs.index != rhs.index { return false } if lhs.peer != rhs.peer { return false } if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } return true } static func <(lhs: PeersEntry, rhs: PeersEntry) -> Bool { return lhs.index < rhs.index } func item(context: AccountContext) -> ListViewItem { return HorizontalPeerItem( theme: self.theme, strings: self.strings, mode: .list(compact: true), accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network, energyUsageSettings: context.sharedContext.energyUsageSettings, contentSettings: context.currentContentSettings.with { $0 }, animationCache: context.animationCache, animationRenderer: context.animationRenderer, resolveInlineStickers: context.engine.stickers.resolveInlineStickers, peer: self.peer, presence: nil, unreadBadge: nil, action: { _ in }, contextAction: nil, isPeerSelected: { _ in return false }, customWidth: nil ) } } private struct DeleteAccountPeersItemNodeTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let firstTime: Bool let animated: Bool } private func preparedPeersTransition(context: AccountContext, from fromEntries: [PeersEntry], to toEntries: [PeersEntry], firstTime: Bool, animated: Bool) -> DeleteAccountPeersItemNodeTransition { 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(context: context), directionHint: .Down) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context), directionHint: nil) } return DeleteAccountPeersItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, animated: animated) } class DeleteAccountPeersItem: ListViewItem, ItemListItem { var sectionId: ItemListSectionId let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let peers: [EnginePeer] init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peers: [EnginePeer], sectionId: ItemListSectionId) { self.context = context self.theme = theme self.strings = strings self.peers = peers self.sectionId = sectionId } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = DeleteAccountPeersItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets Queue.mainQueue().async { completion(node, { return (nil, { _ in apply() }) }) } } } func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? DeleteAccountPeersItemNode { let makeLayout = nodeValue.asyncLayout() async { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { _ in apply() }) } } } } } } class DeleteAccountPeersItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode private let dataPromise = Promise<(AccountContext, [EnginePeer], PresentationTheme, PresentationStrings)>() private var disposable: Disposable? private var item: DeleteAccountPeersItem? private var layoutParams: ListViewItemLayoutParams? private let listView: ListView private var queuedTransitions: [DeleteAccountPeersItemNodeTransition] = [] var tag: ItemListItemTag? { return self.item?.tag } init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode.isLayerBacked = true self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false self.listView = ListView() self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.listView) let previous: Atomic<[PeersEntry]> = Atomic(value: []) let firstTime:Atomic = Atomic(value: true) self.disposable = (self.dataPromise.get() |> deliverOnMainQueue).start(next: { [weak self] data in if let strongSelf = self { let (context, peers, theme, strings) = data var entries: [PeersEntry] = [] for peer in peers { entries.append(PeersEntry(index: entries.count, peer: peer, theme: theme, strings: strings)) } let animated = !firstTime.swap(false) let transition = preparedPeersTransition(context: context, from: previous.swap(entries), to: entries, firstTime: !animated, animated: animated) strongSelf.enqueueTransition(transition) } }) } deinit { self.disposable?.dispose() } private func enqueueTransition(_ transition: DeleteAccountPeersItemNodeTransition) { self.queuedTransitions.append(transition) self.dequeueTransitions() } private func dequeueTransitions() { while !self.queuedTransitions.isEmpty { let transition = self.queuedTransitions.removeFirst() var options = ListViewDeleteAndInsertOptions() if transition.firstTime { options.insert(.PreferSynchronousResourceLoading) options.insert(.PreferSynchronousDrawing) options.insert(.Synchronous) options.insert(.LowLatency) } else if transition.animated { options.insert(.AnimateInsertion) } self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { _ in }) } } func asyncLayout() -> (_ item: DeleteAccountPeersItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { return { item, params, neighbors in let contentSize: CGSize var insets: UIEdgeInsets let separatorHeight = UIScreenPixel contentSize = CGSize(width: params.width, height: 109.0) insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size return (layout, { [weak self] in if let strongSelf = self { strongSelf.item = item strongSelf.layoutParams = params strongSelf.dataPromise.set(.single((item.context, item.peers, item.theme, item.strings))) strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) } if strongSelf.topStripeNode.supernode == nil { strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) } if strongSelf.bottomStripeNode.supernode == nil { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) } if strongSelf.maskNode.supernode == nil { strongSelf.addSubnode(strongSelf.maskNode) } let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false switch neighbors.top { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: hasTopCorners = true strongSelf.topStripeNode.isHidden = hasCorners } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat switch neighbors.bottom { case .sameSection(false): bottomStripeInset = params.leftInset + 16.0 bottomStripeOffset = -separatorHeight strongSelf.bottomStripeNode.isHidden = false default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 hasBottomCorners = true strongSelf.bottomStripeNode.isHidden = hasCorners } strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) let listInsets = UIEdgeInsets(top: params.leftInset, left: 0.0, bottom: params.rightInset, right: 0.0) strongSelf.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: params.width) strongSelf.listView.position = CGPoint(x: params.width / 2.0, y: contentSize.height / 2.0) strongSelf.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: params.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) strongSelf.dequeueTransitions() } }) } } override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } }