import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SyncCore import SwiftSignalKit import AccountContext import TelegramPresentationData import TelegramUIPreferences import MergeLists private struct StickerPackPreviewGridEntry: Comparable, Identifiable { let index: Int let stickerItem: StickerPackItem var stableId: MediaId { return self.stickerItem.file.fileId } static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool { return lhs.index < rhs.index } func item(account: Account, interaction: StickerPackPreviewInteraction) -> StickerPackPreviewGridItem { return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction) } } private struct StickerPackPreviewGridTransaction { let deletions: [Int] let insertions: [GridNodeInsertItem] let updates: [GridNodeUpdateItem] init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction) { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) self.deletions = deleteIndices self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) } self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) } } } private enum StickerPackAction { case add case remove } private enum StickerPackNextAction { case navigatedNext case dismiss } private final class StickerPackContainer: ASDisplayNode { private let context: AccountContext private var presentationData: PresentationData private let stickerPack: StickerPackReference private let decideNextAction: (StickerPackContainer, StickerPackAction) -> StickerPackNextAction private let requestDismiss: () -> Void private let presentInGlobalOverlay: (ViewController, Any?) -> Void private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? private let backgroundNode: ASImageNode private let gridNode: GridNode private let actionAreaBackgroundNode: ASDisplayNode private let actionAreaSeparatorNode: ASDisplayNode private let buttonNode: HighlightableButtonNode private let titleNode: ImmediateTextNode private let titleContainer: ASDisplayNode private let titleSeparatorNode: ASDisplayNode private(set) var validLayout: (ContainerViewLayout, CGRect, CGFloat, UIEdgeInsets)? private var currentEntries: [StickerPackPreviewGridEntry] = [] private var enqueuedTransactions: [StickerPackPreviewGridTransaction] = [] private var itemsDisposable: Disposable? private(set) var currentStickerPack: (StickerPackCollectionInfo, [ItemCollectionItem], Bool)? private let isReadyValue = Promise() private var didSetReady = false var isReady: Signal { return self.isReadyValue.get() } var expandProgress: CGFloat = 0.0 var modalProgress: CGFloat = 0.0 let expandProgressUpdated: (StickerPackContainer, ContainedViewLayoutTransition) -> Void private let interaction: StickerPackPreviewInteraction init(context: AccountContext, presentationData: PresentationData, stickerPack: StickerPackReference, decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction, requestDismiss: @escaping () -> Void, expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) { self.context = context self.presentationData = presentationData self.stickerPack = stickerPack self.decideNextAction = decideNextAction self.requestDismiss = requestDismiss self.presentInGlobalOverlay = presentInGlobalOverlay self.expandProgressUpdated = expandProgressUpdated self.sendSticker = sendSticker self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = true self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) self.gridNode = GridNode() self.gridNode.scrollView.alwaysBounceVertical = true self.gridNode.scrollView.showsVerticalScrollIndicator = false self.actionAreaBackgroundNode = ASDisplayNode() self.actionAreaBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor self.actionAreaSeparatorNode = ASDisplayNode() self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor self.buttonNode = HighlightableButtonNode() self.titleNode = ImmediateTextNode() self.titleContainer = ASDisplayNode() self.titleSeparatorNode = ASDisplayNode() self.titleSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: true) super.init() self.addSubnode(self.backgroundNode) self.addSubnode(self.gridNode) self.addSubnode(self.actionAreaBackgroundNode) self.addSubnode(self.actionAreaSeparatorNode) self.addSubnode(self.buttonNode) self.titleContainer.addSubnode(self.titleNode) self.addSubnode(self.titleContainer) self.addSubnode(self.titleSeparatorNode) self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) } self.gridNode.interactiveScrollingEnded = { [weak self] in guard let strongSelf = self else { return } let contentOffset = strongSelf.gridNode.scrollView.contentOffset let insets = strongSelf.gridNode.scrollView.contentInset if contentOffset.y <= -insets.top - 30.0 { DispatchQueue.main.async { self?.requestDismiss() } } } self.itemsDisposable = (loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: stickerPack, forceActualized: false) |> deliverOnMainQueue).start(next: { [weak self] contents in guard let strongSelf = self else { return } strongSelf.updateStickerPackContents(contents) }) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonNode.alpha = 0.8 } else { strongSelf.buttonNode.alpha = 1.0 strongSelf.buttonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3) } } } } deinit { self.itemsDisposable?.dispose() } override func didLoad() { super.didLoad() self.gridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in if let strongSelf = self { if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { return strongSelf.context.account.postbox.transaction { transaction -> Bool in return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId) } |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { var menuItems: [PeekControllerMenuItem] = [] if let (info, _, _) = strongSelf.currentStickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { if strongSelf.sendSticker != nil { menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in if let strongSelf = self { return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false } else { return false } })) } menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() } else { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } return true })) menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true })) } return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { return nil } } } } return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { return sourceNode }) strongSelf.presentInGlobalOverlay(controller, nil) return controller } return nil }, updateContent: { [weak self] content in if let strongSelf = self { var item: StickerPreviewPeekItem? if let content = content as? StickerPreviewPeekContent { item = content.item } strongSelf.updatePreviewingItem(item: item, animated: true) } }, activateBySingleTap: true)) } @objc func buttonPressed() { guard let (info, items, installed) = currentStickerPack else { return } let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] sharedData in guard let strongSelf = self else { return } var stickerSettings = StickerSettings.defaultSettings if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings] as? StickerSettings { stickerSettings = value } if installed { let _ = removeStickerPackInteractively(postbox: strongSelf.context.account.postbox, id: info.id, option: .delete).start() } else { let _ = addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items).start() } switch strongSelf.decideNextAction(strongSelf, installed ? .remove : .add) { case .dismiss: strongSelf.requestDismiss() case .navigatedNext: strongSelf.updateStickerPackContents(.result(info: info, items: items, installed: !installed)) } }) } private func updateStickerPackContents(_ contents: LoadedStickerPack) { var entries: [StickerPackPreviewGridEntry] = [] var updateLayout = false switch contents { case .fetching: entries = [] case .none: entries = [] case let .result(info, items, installed): self.currentStickerPack = (info, items, installed) if installed { let text: String if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { text = self.presentationData.strings.StickerPack_RemoveStickerCount(info.count) } else { text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count) } self.buttonNode.setTitle(text.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemDestructiveColor, for: .normal) self.buttonNode.setBackgroundImage(nil, for: []) } else { let text: String if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { text = self.presentationData.strings.StickerPack_AddStickerCount(info.count) } else { text = self.presentationData.strings.StickerPack_AddMaskCount(info.count) } self.buttonNode.setTitle(text.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) let roundedAccentBackground = generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(self.presentationData.theme.list.itemCheckColors.fillColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) })?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25) self.buttonNode.setBackgroundImage(roundedAccentBackground, for: []) } self.titleNode.attributedText = NSAttributedString(string: info.title, font: Font.semibold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) updateLayout = true for item in items { guard let item = item as? StickerPackItem else { continue } entries.append(StickerPackPreviewGridEntry(index: entries.count, stickerItem: item)) } } let previousEntries = self.currentEntries self.currentEntries = entries if updateLayout, let (layout, _, _, _) = self.validLayout { let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 12.0 * 2.0, height: .greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: floor((-titleSize.width) / 2.0), y: floor((-titleSize.height) / 2.0)), size: titleSize) } let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, account: self.context.account, interaction: self.interaction) self.enqueueTransaction(transaction) } var topContentInset: CGFloat { guard let (_, gridFrame, titleAreaInset, gridInsets) = self.validLayout else { return 0.0 } return gridFrame.minY + gridInsets.top - titleAreaInset } func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { var insets = layout.insets(options: [.statusBar]) insets.top += 10.0 let buttonHeight: CGFloat = 50.0 let actionAreaTopInset: CGFloat = 12.0 let buttonSideInset: CGFloat = 10.0 let titleAreaInset: CGFloat = 50.0 var actionAreaHeight: CGFloat = 0.0 actionAreaHeight += insets.bottom transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: layout.size.height - actionAreaHeight - buttonHeight), size: CGSize(width: layout.size.width - buttonSideInset * 2.0, height: buttonHeight))) actionAreaHeight += buttonHeight actionAreaHeight += actionAreaTopInset transition.updateFrame(node: self.actionAreaBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: actionAreaHeight))) transition.updateFrame(node: self.actionAreaSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top + titleAreaInset), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - titleAreaInset)) let itemsPerRow = 4 let fillingWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) let itemWidth = floor(fillingWidth / CGFloat(itemsPerRow)) let gridLeftInset = floor((layout.size.width - fillingWidth) / 2.0) let initialRevealedRowCount: CGFloat = 4.5 let topInset = max(0.0, layout.size.height - floor(initialRevealedRowCount * itemWidth) - insets.top - actionAreaHeight - titleAreaInset) let gridInsets = UIEdgeInsets(top: insets.top + topInset, left: gridLeftInset, bottom: actionAreaHeight, right: layout.size.width - fillingWidth - gridLeftInset) let firstTime = self.validLayout == nil self.validLayout = (layout, gridFrame, titleAreaInset, gridInsets) transition.updateFrame(node: self.gridNode, frame: gridFrame) self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridFrame.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: 200.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in guard let strongSelf = self else { return } if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf.isReadyValue.set(.single(true)) } }) if firstTime { while !self.enqueuedTransactions.isEmpty { self.dequeueTransaction() } } } private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) { guard let (layout, gridFrame, titleAreaInset, gridInsets) = self.validLayout else { return } let minBackgroundY = gridFrame.minY - titleAreaInset let unclippedBackgroundY = gridFrame.minY - presentationLayout.contentOffset.y - titleAreaInset let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top let expandHeight: CGFloat = 100.0 let expandProgress = max(0.0, min(1.0, offsetFromInitialPosition / expandHeight)) var expandProgressTransition = transition var expandUpdated = false let modalProgress: CGFloat = unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0 if abs(self.modalProgress - modalProgress) > CGFloat.ulpOfOne { self.modalProgress = modalProgress expandUpdated = true expandProgressTransition = .animated(duration: 0.3, curve: .easeInOut) } if abs(self.expandProgress - expandProgress) > CGFloat.ulpOfOne { self.expandProgress = expandProgress expandUpdated = true } if expandUpdated { self.expandProgressUpdated(self, expandProgressTransition) } let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: max(minBackgroundY, unclippedBackgroundY)), size: CGSize(width: layout.size.width, height: layout.size.height)) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) transition.updateFrame(node: self.titleContainer, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((50.0) / 2.0)), size: CGSize())) transition.updateFrame(node: self.titleSeparatorNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 50.0 - UIScreenPixel), size: CGSize(width: backgroundFrame.width, height: UIScreenPixel))) self.titleSeparatorNode.alpha = unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0 } private func enqueueTransaction(_ transaction: StickerPackPreviewGridTransaction) { self.enqueuedTransactions.append(transaction) if let _ = self.validLayout { self.dequeueTransaction() } } private func dequeueTransaction() { if self.enqueuedTransactions.isEmpty { return } let transaction = self.enqueuedTransactions.removeFirst() self.gridNode.transaction(GridNodeTransaction(deleteItems: transaction.deletions, insertItems: transaction.insertions, updateItems: transaction.updates, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.bounds.contains(point) { if !self.backgroundNode.bounds.contains(self.convert(point, to: self.backgroundNode)) { return nil } } let result = super.hitTest(point, with: event) return result } private func updatePreviewingItem(item: StickerPreviewPeekItem?, animated: Bool) { if self.interaction.previewedItem != item { self.interaction.previewedItem = item self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? StickerPackPreviewGridItemNode { itemNode.updatePreviewing(animated: animated) } } } } } private final class StickerPackScreenNode: ViewControllerTracingNode { private let context: AccountContext private var presentationData: PresentationData private let stickerPacks: [StickerPackReference] private let modalProgressUpdated: (CGFloat, ContainedViewLayoutTransition) -> Void private let dismissed: () -> Void private let dimNode: ASDisplayNode private let containerContainingNode: ASDisplayNode private var containers: [StickerPackContainer] = [] private var selectedStickerPackIndex: Int private var relativeToSelectedStickerPackTransition: CGFloat = 0.0 private var validLayout: ContainerViewLayout? private var isDismissed: Bool = false private let _ready = Promise() var ready: Promise { return self._ready } init(context: AccountContext, stickerPacks: [StickerPackReference], initialSelectedStickerPackIndex: Int, modalProgressUpdated: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.stickerPacks = stickerPacks self.selectedStickerPackIndex = initialSelectedStickerPackIndex self.modalProgressUpdated = modalProgressUpdated self.dismissed = dismissed self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.dimNode.alpha = 0.0 self.containerContainingNode = ASDisplayNode() super.init() self.containers = self.stickerPacks.map { stickerPack in return StickerPackContainer(context: context, presentationData: self.presentationData, stickerPack: stickerPack, decideNextAction: { [weak self] container, action in guard let strongSelf = self, let layout = strongSelf.validLayout, let index = strongSelf.containers.index(where: { $0 === container }) else { return .dismiss } if index == strongSelf.containers.count - 1 { return .dismiss } else { switch action { case .add: var allAdded = true for i in index + 1 ..< strongSelf.containers.count { if let (_, _, installed) = strongSelf.containers[i].currentStickerPack { if !installed { allAdded = false } } else { allAdded = false } } if allAdded { return .dismiss } case .remove: if strongSelf.containers.count == 1 { return .dismiss } } } strongSelf.selectedStickerPackIndex = strongSelf.selectedStickerPackIndex + 1 strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring)) return .navigatedNext }, requestDismiss: { [weak self] in self?.dismiss() }, expandProgressUpdated: { [weak self] container, transition in guard let strongSelf = self, let layout = strongSelf.validLayout, let index = strongSelf.containers.index(where: { $0 === container }) else { return } if index == strongSelf.selectedStickerPackIndex { strongSelf.containerLayoutUpdated(layout, transition: transition) } }, presentInGlobalOverlay: presentInGlobalOverlay, sendSticker: sendSticker) } for container in self.containers { self.containerContainingNode.addSubnode(container) } self.addSubnode(self.dimNode) self.addSubnode(self.containerContainingNode) self._ready.set(combineLatest(self.containers.map { $0.isReady }) |> map { values -> Bool in for value in values { if !value { return false } } return true }) } override func didLoad() { super.didLoad() self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapGesture(_:)))) self.containerContainingNode.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { let firstTime = self.validLayout == nil self.validLayout = layout transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.containerContainingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) let expandProgress: CGFloat if self.containers.count == 1 { expandProgress = 1.0 } else { expandProgress = self.containers[self.selectedStickerPackIndex].expandProgress } let scaledInset: CGFloat = 16.0 let scaledDistance: CGFloat = 4.0 let minScale = (layout.size.width - scaledInset * 2.0) / layout.size.width let containerScale = expandProgress * 1.0 + (1.0 - expandProgress) * minScale let containerVerticalOffset: CGFloat = (1.0 - expandProgress) * scaledInset * 2.0 for i in 0 ..< self.containers.count { let container = self.containers[i] let indexOffset = i - self.selectedStickerPackIndex var scaledOffset: CGFloat = 0.0 scaledOffset = -CGFloat(indexOffset) * (1.0 - expandProgress) * (scaledInset * 2.0) + CGFloat(indexOffset) * scaledDistance transition.updateFrame(node: container, frame: CGRect(origin: CGPoint(x: CGFloat(indexOffset) * layout.size.width + self.relativeToSelectedStickerPackTransition + scaledOffset, y: containerVerticalOffset), size: layout.size)) transition.updateSublayerTransformScale(node: container, scale: containerScale) if container.validLayout?.0 != layout { container.updateLayout(layout: layout, transition: transition) } } let modalProgress = self.containers[self.selectedStickerPackIndex].modalProgress self.modalProgressUpdated(modalProgress, transition) } @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: break case .changed: let translation = recognizer.translation(in: self.view) self.relativeToSelectedStickerPackTransition = translation.x if self.selectedStickerPackIndex == 0 { self.relativeToSelectedStickerPackTransition = min(0.0, self.relativeToSelectedStickerPackTransition) } if self.selectedStickerPackIndex == self.containers.count - 1 { self.relativeToSelectedStickerPackTransition = max(0.0, self.relativeToSelectedStickerPackTransition) } if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: .immediate) } case .ended, .cancelled: let translation = recognizer.translation(in: self.view) let velocity = recognizer.velocity(in: self.view) if abs(translation.x) > 30.0 { let deltaIndex = translation.x > 0 ? -1 : 1 self.selectedStickerPackIndex = max(0, min(self.containers.count - 1, Int(self.selectedStickerPackIndex + deltaIndex))) } else if abs(velocity.x) > 100.0 { let deltaIndex = velocity.x > 0 ? -1 : 1 self.selectedStickerPackIndex = max(0, min(self.containers.count - 1, Int(self.selectedStickerPackIndex + deltaIndex))) } self.relativeToSelectedStickerPackTransition = 0.0 if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: .animated(duration: 0.2, curve: .easeInOut)) } default: break } } func animateIn() { self.dimNode.alpha = 1.0 self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let minInset: CGFloat = (self.containers.map { container -> CGFloat in container.topContentInset }).max() ?? 0.0 self.containerContainingNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } func animateOut(completion: @escaping () -> Void) { self.dimNode.alpha = 0.0 self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) let minInset: CGFloat = (self.containers.map { container -> CGFloat in container.topContentInset }).max() ?? 0.0 self.containerContainingNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in completion() }) } func dismiss() { if self.isDismissed { return } self.isDismissed = true self.animateOut(completion: { [weak self] in self?.dismissed() }) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } let selectedContainer = self.containers[self.selectedStickerPackIndex] if selectedContainer.hitTest(self.view.convert(point, to: selectedContainer.view), with: event) == nil { return self.dimNode.view } let result = super.hitTest(point, with: event) return result } @objc private func dimNodeTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.dismiss() } } } public final class StickerPackScreen: ViewController { private let context: AccountContext private let stickerPacks: [StickerPackReference] private let initialSelectedStickerPackIndex: Int private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? private var controllerNode: StickerPackScreenNode { return self.displayNode as! StickerPackScreenNode } private let _ready = Promise() override public var ready: Promise { return self._ready } private var alreadyDidAppear: Bool = false public init(context: AccountContext, stickerPacks: [StickerPackReference], selectedStickerPackIndex: Int = 0, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) { self.context = context self.stickerPacks = stickerPacks self.initialSelectedStickerPackIndex = selectedStickerPackIndex self.sendSticker = sendSticker super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Ignore } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func loadDisplayNode() { self.displayNode = StickerPackScreenNode(context: self.context, stickerPacks: self.stickerPacks, initialSelectedStickerPackIndex: self.initialSelectedStickerPackIndex, modalProgressUpdated: { [weak self] value, transition in DispatchQueue.main.async { guard let strongSelf = self else { return } strongSelf.updateModalStyleOverlayTransitionFactor(value, transition: transition) } }, dismissed: { [weak self] in self?.dismiss() }, presentInGlobalOverlay: { [weak self] c, a in self?.presentInGlobalOverlay(c, with: a) }, sendSticker: self.sendSticker.flatMap { [weak self] sendSticker in return { file, sourceNode, sourceRect in if sendSticker(file, sourceNode, sourceRect) { self?.dismiss() return true } else { return false } } }) self._ready.set(self.controllerNode.ready.get()) super.displayNodeDidLoad() } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !self.alreadyDidAppear { self.alreadyDidAppear = true self.controllerNode.animateIn() } } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, transition: transition) } }