import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import AccountContext import TelegramPresentationData import TelegramUIPreferences import MergeLists import ShimmerEffect import ContextUI import MoreButtonNode import UndoUI import ShareController import TextFormat import PremiumUI import OverlayStatusController import PresentationDataUtils import StickerPeekUI import AnimationCache import MultiAnimationRenderer import Pasteboard import StickerPackEditTitleController import EntityKeyboard private enum StickerPackPreviewGridEntry: Comparable, Identifiable { case sticker(index: Int, stableId: Int, stickerItem: StickerPackItem?, isEmpty: Bool, isPremium: Bool, isLocked: Bool, isEditing: Bool, isAdd: Bool) case add case emojis(index: Int, stableId: Int, info: StickerPackCollectionInfo, items: [StickerPackItem], title: String?, isInstalled: Bool?) var stableId: Int { switch self { case let .sticker(_, stableId, _, _, _, _, _, _): return stableId case .add: return -1 case let .emojis(_, stableId, _, _, _, _): return stableId } } var index: Int { switch self { case let .sticker(index, _, _, _, _, _, _, _): return index case .add: return 100000 case let .emojis(index, _, _, _, _, _): return index } } static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool { return lhs.index < rhs.index } func item(context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isEditable: Bool, isEditing: Bool) -> GridItem { switch self { case let .sticker(_, _, stickerItem, isEmpty, isPremium, isLocked, _, isAdd): return StickerPackPreviewGridItem(context: context, stickerItem: stickerItem, interaction: interaction, theme: theme, isPremium: isPremium, isLocked: isLocked, isEmpty: isEmpty, isEditable: isEditable, isEditing: isEditing, isAdd: isAdd) case .add: return StickerPackPreviewGridItem(context: context, stickerItem: nil, interaction: interaction, theme: theme, isPremium: false, isLocked: false, isEmpty: false, isEditable: false, isEditing: false, isAdd: true) case let .emojis(_, _, info, items, title, isInstalled): return StickerPackEmojisItem(context: context, animationCache: animationCache, animationRenderer: animationRenderer, interaction: interaction, info: info, items: items, theme: theme, strings: strings, title: title, isInstalled: isInstalled, isEmpty: false) } } } private struct StickerPackPreviewGridTransaction { let deletions: [Int] let insertions: [GridNodeInsertItem] let updates: [GridNodeUpdateItem] let scrollToItem: GridNodeScrollToItem? init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, scrollToItem: GridNodeScrollToItem?, isEditable: Bool, isEditing: Bool) { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) self.deletions = deleteIndices self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings, animationCache: animationCache, animationRenderer: animationRenderer, isEditable: isEditable, isEditing: isEditing), previousIndex: $0.2) } self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings, animationCache: animationCache, animationRenderer: animationRenderer, isEditable: isEditable, isEditing: isEditing)) } self.scrollToItem = scrollToItem } init(list: [StickerPackPreviewGridEntry], context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, scrollToItem: GridNodeScrollToItem?, isEditable: Bool, isEditing: Bool) { self.deletions = [] self.insertions = [] var index = 0 var updates: [GridNodeUpdateItem] = [] for i in 0 ..< list.count { updates.append(GridNodeUpdateItem(index: i, previousIndex: i, item: list[i].item(context: context, interaction: interaction, theme: theme, strings: strings, animationCache: animationCache, animationRenderer: animationRenderer, isEditable: isEditable, isEditing: isEditing))) index += 1 } self.updates = updates self.scrollToItem = nil } } private enum StickerPackAction { case add case remove } private enum StickerPackNextAction { case navigatedNext case dismiss case ignored } private final class StickerPackContainer: ASDisplayNode { let index: Int private let context: AccountContext private weak var controller: StickerPackScreenImpl? private var presentationData: PresentationData private let stickerPacks: [StickerPackReference] private let decideNextAction: (StickerPackContainer, StickerPackAction) -> StickerPackNextAction private let requestDismiss: () -> Void private let presentInGlobalOverlay: (ViewController, Any?) -> Void private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? private let backgroundNode: ASImageNode private let gridNode: GridNode private let actionAreaBackgroundNode: NavigationBackgroundNode private let actionAreaSeparatorNode: ASDisplayNode private let buttonNode: HighlightableButtonNode private let titleBackgroundnode: NavigationBackgroundNode private let titleNode: ImmediateTextNode private var titlePlaceholderNode: ShimmerEffectNode? private let titleContainer: ASDisplayNode private let titleSeparatorNode: ASDisplayNode private let topContainerNode: ASDisplayNode private let cancelButtonNode: HighlightableButtonNode private let moreButtonNode: MoreButtonNode private(set) var validLayout: (ContainerViewLayout, CGRect, CGFloat, UIEdgeInsets)? private var nextStableId: Int = 1 private var currentEntries: [StickerPackPreviewGridEntry] = [] private var enqueuedTransactions: [StickerPackPreviewGridTransaction] = [] private var updatedTitle: String? private var itemsDisposable: Disposable? private var currentContents: [LoadedStickerPack]? private(set) var currentStickerPack: (StickerPackCollectionInfo, [StickerPackItem], Bool)? private(set) var currentStickerPacks: [(StickerPackCollectionInfo, [StickerPackItem], Bool)] = [] private var didReceiveStickerPackResult = false private let isReadyValue = Promise() private var didSetReady = false var isReady: Signal { return self.isReadyValue.get() } var expandProgress: CGFloat = 0.0 var expandScrollProgress: CGFloat = 0.0 var modalProgress: CGFloat = 0.0 var isAnimatingAutoscroll: Bool = false let expandProgressUpdated: (StickerPackContainer, ContainedViewLayoutTransition, ContainedViewLayoutTransition) -> Void private var isDismissed: Bool = false private let interaction: StickerPackPreviewInteraction private weak var peekController: PeekController? var onLoading: () -> Void = {} var onReady: () -> Void = {} var onError: () -> Void = {} init( index: Int, context: AccountContext, presentationData: PresentationData, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction, requestDismiss: @escaping () -> Void, expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition, ContainedViewLayoutTransition) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, longPressEmoji: ((String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void)?, openMention: @escaping (String) -> Void, controller: StickerPackScreenImpl?) { self.index = index self.context = context self.controller = controller self.presentationData = presentationData self.stickerPacks = stickerPacks self.decideNextAction = decideNextAction self.requestDismiss = requestDismiss self.presentInGlobalOverlay = presentInGlobalOverlay self.expandProgressUpdated = expandProgressUpdated self.sendSticker = sendSticker self.sendEmoji = sendEmoji self.isEditing = controller?.initialIsEditing ?? false 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.titleBackgroundnode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) self.actionAreaBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor) self.actionAreaSeparatorNode = ASDisplayNode() self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor self.buttonNode = HighlightableButtonNode() self.titleNode = ImmediateTextNode() self.titleNode.textAlignment = .center self.titleNode.maximumNumberOfLines = 2 self.titleNode.highlightAttributeAction = { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention) } else { return nil } } self.titleNode.tapAttributeAction = { attributes, _ in if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String, mention.count > 1 { openMention(String(mention[mention.index(after: mention.startIndex)...])) } } self.titleContainer = ASDisplayNode() self.titleSeparatorNode = ASDisplayNode() self.titleSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.topContainerNode = ASDisplayNode() self.cancelButtonNode = HighlightableButtonNode() self.moreButtonNode = MoreButtonNode(theme: self.presentationData.theme) self.moreButtonNode.iconNode.enqueueState(.more, animated: false) var addStickerPackImpl: ((StickerPackCollectionInfo, [StickerPackItem]) -> Void)? var removeStickerPackImpl: ((StickerPackCollectionInfo) -> Void)? var emojiSelectedImpl: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? var emojiLongPressedImpl: ((String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void)? var addPressedImpl: (() -> Void)? self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: true, addStickerPack: { info, items in addStickerPackImpl?(info, items) }, removeStickerPack: { info in removeStickerPackImpl?(info) }, emojiSelected: { text, attribute in emojiSelectedImpl?(text, attribute) }, emojiLongPressed: { text, attribute, node, frame in emojiLongPressedImpl?(text, attribute, node, frame) }, addPressed: { addPressedImpl?() }) 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.addSubnode(self.topContainerNode) self.topContainerNode.addSubnode(self.cancelButtonNode) self.topContainerNode.addSubnode(self.moreButtonNode) self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) } self.gridNode.interactiveScrollingEnded = { [weak self] in guard let strongSelf = self, !strongSelf.isDismissed else { return } if let (layout, _, _, _) = strongSelf.validLayout, case .regular = layout.metrics.widthClass { return } let contentOffset = strongSelf.gridNode.scrollView.contentOffset let insets = strongSelf.gridNode.scrollView.contentInset if contentOffset.y <= -insets.top - 30.0 { strongSelf.isDismissed = true DispatchQueue.main.async { self?.requestDismiss() } } } self.gridNode.visibleContentOffsetChanged = { [weak self] _ in guard let strongSelf = self else { return } strongSelf.updateButtonBackgroundAlpha() } self.gridNode.interactiveScrollingWillBeEnded = { [weak self] contentOffset, velocity, targetOffset -> CGPoint in guard let strongSelf = self, !strongSelf.isDismissed else { return targetOffset } let insets = strongSelf.gridNode.scrollView.contentInset var modalProgress: CGFloat = 0.0 var updatedOffset = targetOffset var resetOffset = false if targetOffset.y < 0.0 && targetOffset.y >= -insets.top { if contentOffset.y > 0.0 { updatedOffset = CGPoint(x: 0.0, y: 0.0) modalProgress = 1.0 } else { if targetOffset.y > -insets.top / 2.0 || velocity.y <= -100.0 { modalProgress = 1.0 resetOffset = true } else { modalProgress = 0.0 if contentOffset.y > -insets.top { resetOffset = true } } } } else if targetOffset.y >= 0.0 { modalProgress = 1.0 } if abs(strongSelf.modalProgress - modalProgress) > CGFloat.ulpOfOne { if contentOffset.y > 0.0 && targetOffset.y > 0.0 { } else { resetOffset = true } strongSelf.modalProgress = modalProgress strongSelf.expandProgressUpdated(strongSelf, .animated(duration: 0.4, curve: .spring), .immediate) } if resetOffset { let offset: CGPoint let isVelocityAligned: Bool if modalProgress.isZero { offset = CGPoint(x: 0.0, y: -insets.top) isVelocityAligned = velocity.y < 0.0 } else { offset = CGPoint(x: 0.0, y: 0.0) isVelocityAligned = velocity.y > 0.0 } DispatchQueue.main.async { let duration: Double if isVelocityAligned { let minVelocity: CGFloat = 400.0 let maxVelocity: CGFloat = 1000.0 let clippedVelocity = max(minVelocity, min(maxVelocity, abs(velocity.y * 500.0))) let distance = abs(offset.y - contentOffset.y) duration = Double(distance / clippedVelocity) } else { duration = 0.5 } strongSelf.isAnimatingAutoscroll = true strongSelf.gridNode.autoscroll(toOffset: offset, duration: duration) strongSelf.isAnimatingAutoscroll = false } updatedOffset = contentOffset } return updatedOffset } let fetchedStickerPacks: Signal<[LoadedStickerPack], NoError> = combineLatest(stickerPacks.map { packReference in for pack in loadedStickerPacks { if case let .result(info, _, _) = pack, case let .id(id, _) = packReference, info.id.id == id { return .single(pack) } } return context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: true) }) self.itemsDisposable = combineLatest(queue: Queue.mainQueue(), fetchedStickerPacks, context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))).start(next: { [weak self] contents, peer in guard let strongSelf = self else { return } var hasPremium = false if let peer = peer, peer.isPremium { hasPremium = true } strongSelf.updateStickerPackContents(contents, hasPremium: hasPremium) }) 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) } } } self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) self.moreButtonNode.action = { [weak self] _, gesture in if let strongSelf = self { strongSelf.morePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture) } } self.titleNode.linkHighlightColor = self.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.2) addStickerPackImpl = { [weak self] info, items in guard let strongSelf = self else { return } if let index = strongSelf.currentStickerPacks.firstIndex(where: { $0.0.id == info.id }) { strongSelf.currentStickerPacks[index].2 = true var contents: [LoadedStickerPack] = [] for (info, items, isInstalled) in strongSelf.currentStickerPacks { contents.append(.result(info: info, items: items, installed: isInstalled)) } strongSelf.updateStickerPackContents(contents, hasPremium: false) let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: info, items: items).start() } } removeStickerPackImpl = { [weak self] info in guard let strongSelf = self else { return } if let index = strongSelf.currentStickerPacks.firstIndex(where: { $0.0.id == info.id }) { strongSelf.currentStickerPacks[index].2 = false var contents: [LoadedStickerPack] = [] for (info, items, isInstalled) in strongSelf.currentStickerPacks { contents.append(.result(info: info, items: items, installed: isInstalled)) } strongSelf.updateStickerPackContents(contents, hasPremium: false) let _ = strongSelf.context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete).start() } } emojiSelectedImpl = { text, attribute in sendEmoji?(text, attribute) } emojiLongPressedImpl = { text, attribute, node, frame in longPressEmoji?(text, attribute, node, frame) } addPressedImpl = { [weak self] in self?.presentAddStickerOptions() } } deinit { self.itemsDisposable?.dispose() } private var peekGestureRecognizer: PeekControllerGestureRecognizer? private var reorderingGestureRecognizer: ReorderingGestureRecognizer? override func didLoad() { super.didLoad() let peekGestureRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? in if let strongSelf = self { if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { var canEdit = false if let (info, _, _) = strongSelf.currentStickerPack, info.flags.contains(.isCreator) { canEdit = true } let accountPeerId = strongSelf.context.account.peerId return combineLatest( strongSelf.context.engine.stickers.isStickerSaved(id: item.file.fileId), strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId)) |> map { peer -> Bool in var hasPremium = false if case let .user(user) = peer, user.isPremium { hasPremium = true } return hasPremium } ) |> deliverOnMainQueue |> map { isStarred, hasPremium -> (UIView, CGRect, PeekControllerContent)? in if let strongSelf = self { var menuItems: [ContextMenuItem] = [] if let (info, _, _) = strongSelf.currentStickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { if strongSelf.sendSticker != nil { var iconName: String let actionTitle: String if let title = strongSelf.controller?.actionTitle { actionTitle = title iconName = "Chat/Context Menu/Add" } else { actionTitle = strongSelf.presentationData.strings.StickerPack_Send iconName = "Chat/Context Menu/Resend" } menuItems.append(.action(ContextMenuActionItem(text: actionTitle, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: iconName), color: theme.contextMenu.primaryColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode.view, imageNode.bounds) } } f(.default) }))) } menuItems.append(.action(ContextMenuActionItem(text: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) if let strongSelf = self { let _ = strongSelf.context.engine.stickers.toggleStickerSaved(file: item.file, saved: !isStarred).start(next: { _ in }) } }))) if canEdit { menuItems.append(.action(ContextMenuActionItem(text: "Edit Sticker", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) if let self { self.openEditSticker(item.file) } }))) if !strongSelf.isEditing { menuItems.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) if let self { self.updateIsEditing(true) } }))) } menuItems.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in if let _ = self { let contextItems: [ContextMenuItem] = [ .action(ContextMenuActionItem(text: "Back", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { c ,f in c.popItems() })), .separator, .action(ContextMenuActionItem(text: "Delete for Everyone", textColor: .destructive, icon: { _ in return nil }, action: { [weak self] _ ,f in f(.default) if let self, let (info, items, installed) = self.currentStickerPack { let updatedItems = items.filter { $0.file.fileId != item.file.fileId } self.currentStickerPack = (info, updatedItems, installed) self.updateEntries() let _ = self.context.engine.stickers.deleteStickerFromStickerSet(sticker: .stickerPack(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), media: item.file)).startStandalone() } })) ] c.pushItems(items: .single(ContextController.Items(content: .list(contextItems)))) } }))) } } return (itemNode.view, itemNode.bounds, StickerPreviewPeekContent(context: strongSelf.context, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item.file), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: { [weak self] in guard let strongSelf = self else { return } let controller = PremiumIntroScreen(context: strongSelf.context, source: .stickers) let navigationController = strongSelf.controller?.parentNavigationController strongSelf.controller?.dismiss(animated: false, completion: nil) navigationController?.pushViewController(controller) })) } else { return nil } } } else if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackEmojisItemNode, let targetItem = itemNode.targetItem(at: strongSelf.gridNode.view.convert(point, to: itemNode.view)) { return strongSelf.emojiSuggestionPeekContent(itemLayer: targetItem.1, file: targetItem.0) } } return nil }, present: { [weak self] content, sourceView, sourceRect in if let strongSelf = self { let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) controller.visibilityUpdated = { [weak self] visible in if let strongSelf = self { strongSelf.gridNode.forceHidden = visible } } strongSelf.peekController = controller 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) peekGestureRecognizer.longPressEnabled = !self.isEditing self.peekGestureRecognizer = peekGestureRecognizer self.gridNode.view.addGestureRecognizer(peekGestureRecognizer) let reorderingGestureRecognizer = ReorderingGestureRecognizer(animateOnTouch: false, shouldBegin: { [weak self] point in if let strongSelf = self, !strongSelf.gridNode.scrollView.isDragging && strongSelf.currentEntries.count > 1 { if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, !itemNode.isAdd { return (true, true, itemNode) } return (false, false, nil) } return (false, false, nil) }, willBegin: { _ in }, began: { [weak self] itemNode in self?.beginReordering(itemNode: itemNode) }, ended: { [weak self] point in if let strongSelf = self { if let point = point { strongSelf.endReordering(point: point) } else { strongSelf.endReordering(point: nil) } } }, moved: { [weak self] point, offset in self?.updateReordering(point: point, offset: offset) }) reorderingGestureRecognizer.isEnabled = self.isEditing self.reorderingGestureRecognizer = reorderingGestureRecognizer self.gridNode.view.addGestureRecognizer(reorderingGestureRecognizer) } private var reorderFeedback: HapticFeedback? private var reorderNode: ReorderingItemNode? private var isReordering = false private var reorderPosition: Int? private func beginReordering(itemNode: StickerPackPreviewGridItemNode) { self.isReordering = true if let reorderNode = self.reorderNode { reorderNode.removeFromSupernode() } self.interaction.reorderingFileId = itemNode.stickerPackItem?.file.fileId let reorderNode = ReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin) self.reorderNode = reorderNode self.gridNode.addSubnode(reorderNode) itemNode.isHidden = true if self.reorderFeedback == nil { self.reorderFeedback = HapticFeedback() } self.reorderFeedback?.impact() } private func endReordering(point: CGPoint?) { self.interaction.reorderingFileId = nil if let reorderNode = self.reorderNode { self.reorderNode = nil if let itemNode = reorderNode.itemNode, let _ = point { // var targetNode: StickerPackPreviewGridItemNode? // if let itemNode = self.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode { // targetNode = itemNode // } // let _ = itemNode // let _ = targetNode // if let targetNode = targetNode, let sourceItem = itemNode.asset as? TGMediaSelectableItem, let targetItem = targetNode.asset as? TGMediaSelectableItem, let targetIndex = self.interaction?.selectionState?.index(of: targetItem) { // self.interaction?.selectionState?.move(sourceItem, to: targetIndex) // } reorderNode.animateCompletion(completion: { [weak reorderNode] in reorderNode?.removeFromSupernode() }) self.reorderFeedback?.tap() if let reorderPosition = self.reorderPosition, let file = itemNode.stickerPackItem?.file { let _ = self.context.engine.stickers.reorderSticker(sticker: .standalone(media: file), position: reorderPosition).startStandalone() } } else { reorderNode.removeFromSupernode() reorderNode.itemNode?.isHidden = false } self.updateEntries(reload: true) } self.isReordering = false self.reorderPosition = nil } private func updateReordering(point: CGPoint, offset: CGPoint) { if let reorderNode = self.reorderNode { reorderNode.updateOffset(offset: offset) var targetNode: StickerPackPreviewGridItemNode? if let itemNode = self.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode { targetNode = itemNode } var reorderPosition: Int? if targetNode !== reorderNode.itemNode { var index = 0 for entry in self.currentEntries { if case let .sticker(_, _, item, _, _, _, _, _) = entry, item?.file.fileId == targetNode?.stickerPackItem?.file.fileId { reorderPosition = index break } index += 1 } } if self.reorderPosition != reorderPosition { self.reorderPosition = reorderPosition self.updateEntries() } } } private func emojiSuggestionPeekContent(itemLayer: CALayer, file: TelegramMediaFile) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError> { let context = self.context var collectionId: ItemCollectionId? for attribute in file.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute { switch packReference { case let .id(id, _): collectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id) default: break } } } var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] if let collectionId { bubbleUpEmojiOrStickersets.append(collectionId) } let accountPeerId = context.account.peerId return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId)) |> map { peer -> Bool in var hasPremium = false if case let .user(user) = peer, user.isPremium { hasPremium = true } return hasPremium } |> deliverOnMainQueue |> map { [weak self, weak itemLayer] hasPremium -> (UIView, CGRect, PeekControllerContent)? in guard let strongSelf = self, let itemLayer = itemLayer else { return nil } var menuItems: [ContextMenuItem] = [] menuItems.removeAll() let presentationData = context.sharedContext.currentPresentationData.with { $0 } var isLocked = false if !hasPremium { isLocked = file.isPremiumEmoji /*if isLocked && chatPeerId == context.account.peerId { isLocked = false }*/ } let sendEmoji: (TelegramMediaFile) -> Void = { file in guard let self else { return } var text = "." var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, displayText, stickerPackReference): text = displayText var packId: ItemCollectionId? if case let .id(id, _) = stickerPackReference { packId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id) } emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: packId, fileId: file.fileId.id, file: file) break loop default: break } } if let emojiAttribute { self.sendEmoji?(text, emojiAttribute) } } let setStatus: (TelegramMediaFile) -> Void = { file in guard let self else { return } guard let controller = self.controller else { return } let context = self.context let _ = context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).startStandalone() var animateInAsReplacement = false animateInAsReplacement = false let presentationData = context.sharedContext.currentPresentationData.with { $0 } let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false }) controller.present(undoController, in: .window(.root)) } let copyEmoji: (TelegramMediaFile) -> Void = { file in var text = "." var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, displayText, _): text = displayText emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop default: break } } if let _ = emojiAttribute { storeMessageTextInPasteboard(text, entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))]) } } if strongSelf.sendEmoji != nil { menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SendEmoji, icon: { theme in if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) { return generateImage(image.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) if let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) } }) } else { return nil } }, action: { _, f in sendEmoji(file) f(.default) }))) } menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SetAsStatus, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Smile"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) guard let strongSelf = self else { return } if hasPremium { setStatus(file) } else { var replaceImpl: ((ViewController) -> Void)? let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: { let controller = PremiumIntroScreen(context: context, source: .animatedEmoji) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in controller?.replace(with: c) } strongSelf.controller?.push(controller) } }))) menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_CopyEmoji, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in copyEmoji(file) f(.default) }))) if menuItems.isEmpty { return nil } let content = StickerPreviewPeekContent(context: context, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked, menu: menuItems, openPremiumIntro: { [weak self] in guard let self else { return } guard let controller = self.controller else { return } let premiumController = PremiumIntroScreen(context: context, source: .stickers) controller.push(premiumController) }) return (strongSelf.view, itemLayer.convert(itemLayer.bounds, to: strongSelf.view.layer), content) } } func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) self.titleBackgroundnode.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.actionAreaBackgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor self.titleSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) self.moreButtonNode.theme = self.presentationData.theme self.titleNode.linkHighlightColor = self.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.5) if let currentContents = self.currentContents?.first { let buttonColor: UIColor var buttonFont: UIFont = Font.semibold(17.0) if let controller = self.controller, let _ = controller.mainActionTitle { buttonColor = self.presentationData.theme.list.itemCheckColors.foregroundColor } else { switch currentContents { case .fetching: buttonColor = .clear case .none: buttonColor = self.presentationData.theme.list.itemAccentColor case let .result(info, _, installed): if info.flags.contains(.isCreator) { buttonColor = installed ? self.presentationData.theme.list.itemAccentColor : self.presentationData.theme.list.itemCheckColors.foregroundColor } else { buttonColor = installed ? self.presentationData.theme.list.itemDestructiveColor : self.presentationData.theme.list.itemCheckColors.foregroundColor } if installed { buttonFont = Font.regular(17.0) } } } self.buttonNode.setTitle(self.buttonNode.attributedTitle(for: .normal)?.string ?? "", with: buttonFont, with: buttonColor, for: .normal) } if !self.currentEntries.isEmpty { self.updateEntries() } let titleFont = Font.semibold(17.0) let title = self.updatedTitle ?? (self.titleNode.attributedText?.string ?? "") let entities = generateTextEntities(title, enabledTypes: [.mention]) self.titleNode.attributedText = stringWithAppliedEntities(title, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil) if let (layout, _, _, _) = self.validLayout { let _ = self.titleNode.updateLayout(CGSize(width: layout.size.width - max(12.0, self.cancelButtonNode.frame.width) * 2.0 - 40.0, height: .greatestFiniteMagnitude)) self.updateLayout(layout: layout, transition: .immediate) } } private var isEditing = false func updateEntries(reload: Bool = false) { guard let controller = self.controller else { return } var isEditable = false if let info = self.currentStickerPack?.0, info.flags.contains(.isCreator) { isEditable = true } let transaction: StickerPackPreviewGridTransaction if reload { transaction = StickerPackPreviewGridTransaction(list: self.currentEntries, context: self.context, interaction: self.interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, animationCache: controller.animationCache, animationRenderer: controller.animationRenderer, scrollToItem: nil, isEditable: isEditable, isEditing: self.isEditing) } else { transaction = StickerPackPreviewGridTransaction(previousList: self.currentEntries, list: self.currentEntries, context: self.context, interaction: self.interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, animationCache: controller.animationCache, animationRenderer: controller.animationRenderer, scrollToItem: nil, isEditable: isEditable, isEditing: self.isEditing) } self.enqueueTransaction(transaction) } private func updateIsEditing(_ isEditing: Bool) { self.isEditing = isEditing self.updateEntries(reload: true) self.updateButton() self.peekGestureRecognizer?.longPressEnabled = !isEditing self.reorderingGestureRecognizer?.isEnabled = isEditing if let (layout, _, _, _) = self.validLayout { self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .easeInOut)) } } @objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { guard let controller = self.controller else { return } let strings = self.presentationData.strings let text: String let shareSubject: ShareControllerSubject if !self.currentStickerPacks.isEmpty { var links: String = "" for (info, _, _) in self.currentStickerPacks { if !links.isEmpty { links += "\n" } if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { links += "https://t.me/addemoji/\(info.shortName)" } else { links += "https://t.me/addstickers/\(info.shortName)" } } text = links shareSubject = .text(text) } else if let (info, _, _) = self.currentStickerPack { if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { text = "https://t.me/addemoji/\(info.shortName)" } else { text = "https://t.me/addstickers/\(info.shortName)" } shareSubject = .url(text) } else { return } var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: strings.StickerPack_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) if let strongSelf = self { let parentNavigationController = strongSelf.controller?.parentNavigationController let shareController = ShareController(context: strongSelf.context, subject: shareSubject) shareController.actionCompleted = { [weak parentNavigationController] in if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) } } strongSelf.controller?.present(shareController, in: .window(.root)) } }))) let copyTitle = self.currentStickerPacks.count > 1 ? strings.StickerPack_CopyLinks : strings.StickerPack_CopyLink let copyText = self.currentStickerPacks.count > 1 ? strings.Conversation_LinksCopied : strings.Conversation_LinkCopied items.append(.action(ContextMenuActionItem(text: copyTitle, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) UIPasteboard.general.string = text if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: copyText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) } }))) if let (info, packItems, _) = self.currentStickerPack, info.flags.contains(.isCreator) { //TODO:localize items.append(.separator) if packItems.count > 0 { items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) self?.updateIsEditing(true) }))) } items.append(.action(ContextMenuActionItem(text: "Edit Name", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) self?.presentEditPackTitle() }))) items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in if let self, let (_, _, isInstalled) = self.currentStickerPack { if isInstalled { let contextItems: [ContextMenuItem] = [ .action(ContextMenuActionItem(text: "Delete for Everyone", textColor: .destructive, icon: { _ in return nil }, action: { [weak self] _ ,f in f(.default) self?.presentDeletePack() })), .action(ContextMenuActionItem(text: "Remove for Me", icon: { _ in return nil }, action: { [weak self] _ ,f in f(.default) self?.togglePackInstalled() })) ] c.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true) } else { f(.default) self.presentDeletePack() } } }))) items.append(.separator) items.append(.action(ContextMenuActionItem(text: "Check [@stickers]() bot for more options.", textLayout: .multiline, textFont: .small, parseMarkdown: true, icon: { _ in return nil }, action: { [weak self] _, f in f(.default) guard let self, let controller = self.controller else { return } controller.controllerNode.openMention("stickers") }))) } let contextController = ContextController(presentationData: self.presentationData, source: .reference(StickerPackContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.presentInGlobalOverlay(contextController, nil) } private let stickerPickerInputData = Promise() private func presentAddStickerOptions() { //TODO:localize let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: "Create a New Sticker", color: .accent, action: { [weak actionSheet, weak self] in actionSheet?.dismissAnimated() guard let self, let controller = self.controller else { return } self.presentCreateSticker() controller.controllerNode.dismiss() })) items.append(ActionSheetButtonItem(title: "Add an Existing Sticker", color: .accent, action: { [weak actionSheet, weak self] in actionSheet?.dismissAnimated() guard let self, let controller = self.controller else { return } self.presentAddExistingSticker() controller.controllerNode.dismiss() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) self.presentInGlobalOverlay(actionSheet, nil) let stickerItems = EmojiPagerContentComponent.stickerInputData( context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], chatPeerId: self.context.account.peerId, hasSearch: true, hasTrending: false, forceHasPremium: true ) let signal = stickerItems |> deliverOnMainQueue |> map { stickers -> StickerPickerInput in return StickerPickerInputData(emoji: nil, stickers: stickers, gifs: nil) } self.stickerPickerInputData.set(signal) } private func presentCreateSticker() { guard let (info, _, _) = self.currentStickerPack else { return } let context = self.context let presentationData = self.presentationData let updatedPresentationData = self.controller?.updatedPresentationData let navigationController = self.controller?.parentNavigationController as? NavigationController var dismissImpl: (() -> Void)? let mainController = context.sharedContext.makeStickerMediaPickerScreen( context: context, getSourceRect: { return .zero }, completion: { result, transitionView, transitionRect, transitionImage, completion, dismissed in let editorController = context.sharedContext.makeStickerEditorScreen( context: context, source: result, transitionArguments: transitionView.flatMap { ($0, transitionRect, transitionImage) }, completion: { file, commit in dismissImpl?() let sticker = ImportSticker( resource: file.resource, emojis: ["😀"], dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), mimeType: file.mimeType, keywords: "" ) let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let _ = (context.engine.stickers.addStickerToStickerSet(packReference: packReference, sticker: sticker) |> deliverOnMainQueue).start(completed: { commit() let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: navigationController, sendSticker: nil, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil) (navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root)) Queue.mainQueue().after(0.1) { packController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: "Sticker added to **\(info.title)** sticker set.", undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } }) } ) navigationController?.pushViewController(editorController) }, dismissed: {} ) dismissImpl = { [weak mainController] in mainController?.dismiss() } navigationController?.pushViewController(mainController) } private func presentAddExistingSticker() { guard let (info, _, _) = self.currentStickerPack else { return } let presentationData = self.presentationData let updatedPresentationData = self.controller?.updatedPresentationData let navigationController = self.controller?.parentNavigationController as? NavigationController let context = self.context let controller = self.context.sharedContext.makeStickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData, completion: { file in let sticker = ImportSticker( resource: file.resource, emojis: ["😀"], dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), mimeType: file.mimeType, keywords: "" ) let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let _ = (context.engine.stickers.addStickerToStickerSet(packReference: packReference, sticker: sticker) |> deliverOnMainQueue).start(completed: { let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: navigationController, sendSticker: nil, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil) (navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root)) Queue.mainQueue().after(0.1) { packController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: "Sticker added to **\(info.title)** sticker set.", undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } }) }) navigationController?.pushViewController(controller) } private func openEditSticker(_ initialFile: TelegramMediaFile) { guard let (info, _, _) = self.currentStickerPack else { return } let context = self.context let updatedPresentationData = self.controller?.updatedPresentationData let navigationController = self.controller?.parentNavigationController as? NavigationController self.controller?.dismiss() let controller = context.sharedContext.makeStickerEditorScreen( context: context, source: initialFile, transitionArguments: nil, completion: { file, commit in let sticker = ImportSticker( resource: file.resource, emojis: ["😀"], dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), mimeType: file.mimeType, keywords: "" ) let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let _ = (context.engine.stickers.replaceSticker(previousSticker: .stickerPack(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), media: initialFile), sticker: sticker) |> deliverOnMainQueue).start(completed: { commit() let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: navigationController, sendSticker: nil, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil) (navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root)) }) } ) navigationController?.pushViewController(controller) } private func presentEditPackTitle() { guard let (info, _, _) = self.currentStickerPack else { return } let context = self.context //TODO:localize var dismissImpl: (() -> Void)? let controller = stickerPackEditTitleController(context: context, title: "Edit Sticker Set Name", text: "Choose a new name for your set.", placeholder: self.presentationData.strings.ImportStickerPack_NamePlaceholder, actionTitle: presentationData.strings.Common_Done, value: self.updatedTitle ?? info.title, maxLength: 64, apply: { [weak self] title in guard let self, let title else { return } let _ = (context.engine.stickers.renameStickerSet(packReference: .id(id: info.id.id, accessHash: info.accessHash), title: title) |> deliverOnMainQueue).startStandalone() self.updatedTitle = title self.updatePresentationData(self.presentationData) dismissImpl?() }, cancel: {}) dismissImpl = { [weak controller] in controller?.dismiss() } self.controller?.present(controller, in: .window(.root)) } private func presentDeletePack() { guard let controller = self.controller, let (info, _, _) = self.currentStickerPack else { return } let context = self.context controller.present(textAlertController(context: context, updatedPresentationData: controller.updatedPresentationData, title: "Delete Sticker Set", text: "This will delete the sticker set for all users.", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: "Delete", action: { [weak self] in let _ = (context.engine.stickers.deleteStickerSet(packReference: .id(id: info.id.id, accessHash: info.accessHash)) |> deliverOnMainQueue).startStandalone() self?.controller?.controllerNode.dismiss() })]), in: .window(.root)) } @objc func cancelPressed() { self.requestDismiss() } @objc func buttonPressed() { if !self.currentStickerPacks.isEmpty { var installedCount = 0 for (_, _, isInstalled) in self.currentStickerPacks { if isInstalled { installedCount += 1 } } if installedCount == self.currentStickerPacks.count { var removedPacks: Signal<[(info: ItemCollectionInfo, index: Int, items: [ItemCollectionItem])], NoError> = .single([]) for (info, _, _) in self.currentStickerPacks { removedPacks = removedPacks |> mapToSignal { current -> Signal<[(info: ItemCollectionInfo, index: Int, items: [ItemCollectionItem])], NoError> in return self.context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete) |> map { result -> [(info: ItemCollectionInfo, index: Int, items: [ItemCollectionItem])] in if let result = result { return current + [(info, result.0, result.1)] } else { return current } } } } let _ = (removedPacks |> deliverOnMainQueue).start(next: { [weak self] results in if !results.isEmpty { self?.controller?.actionPerformed?(results.map { result -> (StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) in return (result.0 as! StickerPackCollectionInfo, result.2.map { $0 as! StickerPackItem }, .remove(positionInList: result.1)) }) } }) } else { var installedPacks: [(StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction)] = [] for (info, items, isInstalled) in self.currentStickerPacks { if !isInstalled { installedPacks.append((info, items, .add)) let _ = self.context.engine.stickers.addStickerPackInteractively(info: info, items: items).start() } } self.controller?.actionPerformed?(installedPacks) } self.requestDismiss() } else if let (info, _, installed) = self.currentStickerPack { if installed, info.flags.contains(.isCreator) { self.updateIsEditing(!self.isEditing) return } self.togglePackInstalled() } else { self.requestDismiss() } } private func togglePackInstalled() { if let (info, items, installed) = self.currentStickerPack { var dismissed = false switch self.decideNextAction(self, installed ? .remove : .add) { case .dismiss: self.requestDismiss() dismissed = true case .navigatedNext, .ignored: self.updateStickerPackContents([.result(info: info, items: items, installed: !installed)], hasPremium: false) } let actionPerformed = self.controller?.actionPerformed if installed { let _ = (self.context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete) |> deliverOnMainQueue).start(next: { indexAndItems in guard let (positionInList, _) = indexAndItems else { return } if dismissed { actionPerformed?([(info, items, .remove(positionInList: positionInList))]) } }) } else { let _ = self.context.engine.stickers.addStickerPackInteractively(info: info, items: items).start() if dismissed { actionPerformed?([(info, items, .add)]) } } } } private func updateButtonBackgroundAlpha() { let offset = self.gridNode.visibleContentOffset() let backgroundAlpha: CGFloat switch offset { case .known: let topPosition = self.view.convert(self.topContainerNode.frame, to: self.view).minY let bottomPosition = self.actionAreaBackgroundNode.view.convert(self.actionAreaBackgroundNode.bounds, to: self.view).minY let bottomEdgePosition = topPosition + self.topContainerNode.frame.height + self.gridNode.scrollView.contentSize.height let bottomOffset = bottomPosition - bottomEdgePosition backgroundAlpha = min(10.0, max(0.0, -1.0 * bottomOffset)) / 10.0 case .unknown, .none: backgroundAlpha = 1.0 } let transition: ContainedViewLayoutTransition var delay: Double = 0.0 if backgroundAlpha >= self.actionAreaBackgroundNode.alpha || abs(backgroundAlpha - self.actionAreaBackgroundNode.alpha) < 0.01 { transition = .immediate } else { transition = .animated(duration: 0.2, curve: .linear) if abs(backgroundAlpha - self.actionAreaBackgroundNode.alpha) > 0.9 { delay = 0.2 } } transition.updateAlpha(node: self.actionAreaBackgroundNode, alpha: backgroundAlpha, delay: delay) transition.updateAlpha(node: self.actionAreaSeparatorNode, alpha: backgroundAlpha, delay: delay) } private func updateButton(count: Int32 = 0) { if let currentContents = self.currentContents, currentContents.count == 1, let content = currentContents.first, case let .result(info, _, installed) = content { if installed { let text: String if info.flags.contains(.isCreator) { if self.isEditing { var updated = false if let current = self.buttonNode.attributedTitle(for: .normal)?.string, !current.isEmpty && current != self.presentationData.strings.Common_Done { updated = true } if updated, let snapshotView = self.buttonNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = self.buttonNode.view.frame self.buttonNode.view.superview?.insertSubview(snapshotView, belowSubview: self.buttonNode.view) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) self.buttonNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } self.buttonNode.setTitle(self.presentationData.strings.Common_Done, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) self.buttonNode.setBackgroundImage(generateStretchableFilledCircleImage(radius: 11, color: self.presentationData.theme.list.itemCheckColors.fillColor), for: []) } else { var updated = false if let current = self.buttonNode.attributedTitle(for: .normal)?.string, !current.isEmpty && current != "Edit Stickers" { updated = true } if updated, let snapshotView = self.buttonNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = self.buttonNode.view.frame self.buttonNode.view.superview?.insertSubview(snapshotView, belowSubview: self.buttonNode.view) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) self.buttonNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } //TODO:localize text = "Edit Stickers" self.buttonNode.setTitle(text, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal) self.buttonNode.setBackgroundImage(nil, for: []) } } else { if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { text = self.presentationData.strings.StickerPack_RemoveStickerCount(count) } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { text = self.presentationData.strings.StickerPack_RemoveEmojiCount(count) } else { text = self.presentationData.strings.StickerPack_RemoveMaskCount(count) } self.buttonNode.setTitle(text, with: Font.regular(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(count) } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { text = self.presentationData.strings.StickerPack_AddEmojiCount(count) } else { text = self.presentationData.strings.StickerPack_AddMaskCount(count) } self.buttonNode.setTitle(text, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) self.buttonNode.setBackgroundImage(generateStretchableFilledCircleImage(radius: 11, color: self.presentationData.theme.list.itemCheckColors.fillColor), for: []) } } } private func updateStickerPackContents(_ contents: [LoadedStickerPack], hasPremium: Bool) { self.currentContents = contents self.didReceiveStickerPackResult = true var entries: [StickerPackPreviewGridEntry] = [] var updateLayout = false var scrollToItem: GridNodeScrollToItem? let titleFont = Font.semibold(17.0) var isEditable = false if contents.count > 1 { self.onLoading() var loadedCount = 0 var error = false for content in contents { if case .result = content { loadedCount += 1 } else if case .none = content { error = true } } if error { self.onError() } else if loadedCount == contents.count { self.onReady() if !contents.isEmpty && self.currentStickerPacks.isEmpty { if let _ = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .immediate, directionHint: .up, adjustForSection: false) } } if self.titleNode.attributedText == nil { if let titlePlaceholderNode = self.titlePlaceholderNode { self.titlePlaceholderNode = nil titlePlaceholderNode.removeFromSupernode() } } self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.EmojiPack_Title, font: titleFont, textColor: self.presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) updateLayout = true var currentStickerPacks: [(StickerPackCollectionInfo, [StickerPackItem], Bool)] = [] var index = 0 var installedCount = 0 for content in contents { if case let .result(info, items, isInstalled) = content { entries.append(.emojis(index: index, stableId: index, info: info, items: items, title: info.title, isInstalled: isInstalled)) if isInstalled { installedCount += 1 } currentStickerPacks.append((info, items, isInstalled)) } index += 1 } self.currentStickerPacks = currentStickerPacks if installedCount == contents.count { let text = self.presentationData.strings.StickerPack_RemoveEmojiPacksCount(Int32(contents.count)) self.buttonNode.setTitle(text, with: Font.regular(17.0), with: self.presentationData.theme.list.itemDestructiveColor, for: .normal) self.buttonNode.setBackgroundImage(nil, for: []) } else { let text = self.presentationData.strings.StickerPack_AddEmojiPacksCount(Int32(contents.count - installedCount)) self.buttonNode.setTitle(text, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) let roundedAccentBackground = generateImage(CGSize(width: 22.0, height: 22.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: 11, topCapHeight: 11) self.buttonNode.setBackgroundImage(roundedAccentBackground, for: []) } } } else if let contents = contents.first { switch contents { case .fetching: self.onLoading() entries = [] self.buttonNode.setTitle(self.presentationData.strings.Channel_NotificationLoading, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemDisabledTextColor, for: .normal) self.buttonNode.setBackgroundImage(nil, for: []) for _ in 0 ..< 16 { var stableId: Int? inner: for entry in self.currentEntries { if case let .sticker(index, currentStableId, stickerItem, _, _, _, _, _) = entry, stickerItem == nil, index == entries.count { stableId = currentStableId break inner } } let resolvedStableId: Int if let stableId = stableId { resolvedStableId = stableId } else { resolvedStableId = self.nextStableId self.nextStableId += 1 } self.nextStableId += 1 entries.append(.sticker(index: entries.count, stableId: resolvedStableId, stickerItem: nil, isEmpty: false, isPremium: false, isLocked: false, isEditing: false, isAdd: false)) } if self.titlePlaceholderNode == nil { let titlePlaceholderNode = ShimmerEffectNode() self.titlePlaceholderNode = titlePlaceholderNode self.titleContainer.addSubnode(titlePlaceholderNode) } case .none: self.onError() self.controller?.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.StickerPack_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) self.controller?.dismiss(animated: true, completion: nil) case let .result(info, items, installed): isEditable = info.flags.contains(.isCreator) self.onReady() if !items.isEmpty && self.currentStickerPack == nil { if let _ = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .immediate, directionHint: .up, adjustForSection: false) } } self.currentStickerPack = (info, items, installed) if self.titleNode.attributedText == nil { if let titlePlaceholderNode = self.titlePlaceholderNode { self.titlePlaceholderNode = nil titlePlaceholderNode.removeFromSupernode() } } let entities = generateTextEntities(info.title, enabledTypes: [.mention]) self.titleNode.attributedText = stringWithAppliedEntities(info.title, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil) updateLayout = true if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { entries.append(.emojis(index: 0, stableId: 0, info: info, items: items, title: nil, isInstalled: nil)) } else { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) var generalItems: [StickerPackItem] = [] var premiumItems: [StickerPackItem] = [] for item in items { if item.file.isPremiumSticker { premiumItems.append(item) } else { generalItems.append(item) } } let addItem: (StickerPackItem, Bool, Bool) -> Void = { item, isPremium, isLocked in var stableId: Int? inner: for entry in self.currentEntries { if case let .sticker(_, currentStableId, stickerItem, _, _, _, _, _) = entry, let stickerItem = stickerItem, stickerItem.file.fileId == item.file.fileId { stableId = currentStableId break inner } } let resolvedStableId: Int if let stableId = stableId { resolvedStableId = stableId } else { resolvedStableId = self.nextStableId self.nextStableId += 1 } entries.append(.sticker(index: entries.count, stableId: resolvedStableId, stickerItem: item, isEmpty: false, isPremium: isPremium, isLocked: isLocked, isEditing: false, isAdd: false)) } for item in generalItems { addItem(item, false, false) } if !premiumConfiguration.isPremiumDisabled { if !premiumItems.isEmpty { for item in premiumItems { addItem(item, true, !hasPremium) } } } } if let mainActionTitle = self.controller?.mainActionTitle { self.buttonNode.setTitle(mainActionTitle, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) self.buttonNode.setBackgroundImage(generateStretchableFilledCircleImage(radius: 11, color: self.presentationData.theme.list.itemCheckColors.fillColor), for: []) } else { let count: Int32 if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { count = Int32(entries.count) } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { count = Int32(items.count) } else { count = Int32(entries.count) } self.updateButton(count: count) } if info.flags.contains(.isCreator) { entries.append(.add) } } } let previousEntries = self.currentEntries self.currentEntries = entries if let titlePlaceholderNode = self.titlePlaceholderNode { let fakeTitleSize = CGSize(width: 160.0, height: 22.0) let titlePlaceholderFrame = CGRect(origin: CGPoint(x: floor((-fakeTitleSize.width) / 2.0), y: floor((-fakeTitleSize.height) / 2.0)), size: fakeTitleSize) titlePlaceholderNode.frame = titlePlaceholderFrame let theme = self.presentationData.theme titlePlaceholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.roundedRect(rect: CGRect(origin: CGPoint(), size: titlePlaceholderFrame.size), cornerRadius: 4.0)], size: titlePlaceholderFrame.size) updateLayout = true } if updateLayout, let (layout, _, _, _) = self.validLayout { self.updateLayout(layout: layout, transition: .immediate) } if let controller = self.controller { let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, context: self.context, interaction: self.interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, animationCache: controller.animationCache, animationRenderer: controller.animationRenderer, scrollToItem: scrollToItem, isEditable: isEditable, isEditing: self.isEditing) self.enqueueTransaction(transaction) } } func updateEntries() { guard let (info, items, _) = self.currentStickerPack else { return } let hasPremium = self.context.isPremium let previousEntries = self.currentEntries var entries: [StickerPackPreviewGridEntry] = [] let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) var generalItems: [StickerPackItem] = [] var premiumItems: [StickerPackItem] = [] for item in items { if item.file.isPremiumSticker { premiumItems.append(item) } else { generalItems.append(item) } } let addItem: (StickerPackItem, Bool, Bool) -> Void = { item, isPremium, isLocked in var stableId: Int? inner: for entry in self.currentEntries { if case let .sticker(_, currentStableId, stickerItem, _, _, _, _, _) = entry, let stickerItem = stickerItem, stickerItem.file.fileId == item.file.fileId { stableId = currentStableId break inner } } let resolvedStableId: Int if let stableId = stableId { resolvedStableId = stableId } else { resolvedStableId = self.nextStableId self.nextStableId += 1 } entries.append(.sticker(index: entries.count, stableId: resolvedStableId, stickerItem: item, isEmpty: false, isPremium: isPremium, isLocked: isLocked, isEditing: false, isAdd: false)) } var currentIndex: Int = 0 for item in generalItems { if self.isReordering, let reorderNode = self.reorderNode, let reorderItem = reorderNode.itemNode?.stickerPackItem, let reorderPosition { if currentIndex == reorderPosition { addItem(reorderItem, false, false) currentIndex += 1 } if item.file.fileId == reorderItem.file.fileId { } else { addItem(item, false, false) currentIndex += 1 } } else { addItem(item, false, false) currentIndex += 1 } } if !premiumConfiguration.isPremiumDisabled { if !premiumItems.isEmpty { for item in premiumItems { addItem(item, true, !hasPremium) currentIndex += 1 } } } entries.append(.add) self.currentEntries = entries if let controller = self.controller { let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, context: self.context, interaction: self.interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, animationCache: controller.animationCache, animationRenderer: controller.animationRenderer, scrollToItem: nil, isEditable: info.flags.contains(.isCreator), isEditing: self.isEditing) self.enqueueTransaction(transaction) } } var topContentInset: CGFloat { guard let (_, gridFrame, titleAreaInset, gridInsets) = self.validLayout else { return 0.0 } return min(self.backgroundNode.frame.minY, gridFrame.minY + gridInsets.top - titleAreaInset) } func syncExpandProgress(expandScrollProgress: CGFloat, expandProgress: CGFloat, modalProgress: CGFloat, transition: ContainedViewLayoutTransition) { guard let (_, _, _, gridInsets) = self.validLayout else { return } let contentOffset = (1.0 - expandScrollProgress) * (-gridInsets.top) if case let .animated(duration, _) = transition { self.gridNode.autoscroll(toOffset: CGPoint(x: 0.0, y: contentOffset), duration: duration) } else { if expandScrollProgress.isZero { } self.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false) } self.expandScrollProgress = expandScrollProgress self.expandProgress = expandProgress self.modalProgress = modalProgress } func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { var insets = layout.insets(options: [.statusBar]) if case .regular = layout.metrics.widthClass { insets.top = 0.0 } else if case .compact = layout.metrics.widthClass, layout.size.width > layout.size.height { insets.top = 0.0 } else { insets.top += 10.0 } var buttonHeight: CGFloat = 50.0 var actionAreaTopInset: CGFloat = 8.0 var actionAreaBottomInset: CGFloat = 16.0 if let _ = self.controller?.mainActionTitle { } else { if !self.currentStickerPacks.isEmpty { var installedCount = 0 for (_, _, isInstalled) in self.currentStickerPacks { if isInstalled { installedCount += 1 } } if installedCount == self.currentStickerPacks.count { buttonHeight = 42.0 actionAreaTopInset = 1.0 actionAreaBottomInset = 2.0 } } if let (info, _, isInstalled) = self.currentStickerPack, isInstalled, !info.flags.contains(.isCreator) { buttonHeight = 42.0 actionAreaTopInset = 1.0 actionAreaBottomInset = 2.0 } } let buttonSideInset: CGFloat = 16.0 let titleAreaInset: CGFloat = 56.0 var actionAreaHeight: CGFloat = buttonHeight actionAreaHeight += insets.bottom + actionAreaBottomInset transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonSideInset, y: layout.size.height - actionAreaHeight + actionAreaTopInset), size: CGSize(width: layout.size.width - buttonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: buttonHeight))) 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))) self.actionAreaBackgroundNode.update(size: CGSize(width: layout.size.width, height: actionAreaHeight), transition: .immediate) 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 = 5 let fillingWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) let itemWidth = floor(fillingWidth / CGFloat(itemsPerRow)) let gridLeftInset = floor((layout.size.width - fillingWidth) / 2.0) let contentHeight: CGFloat if !self.currentStickerPacks.isEmpty { var packsHeight = 0.0 for stickerPack in currentStickerPacks { let layout = ItemLayout(width: fillingWidth, itemsCount: stickerPack.1.count) packsHeight += layout.height + 61.0 } contentHeight = packsHeight + 8.0 } else if let (info, items, _) = self.currentStickerPack { if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { let layout = ItemLayout(width: fillingWidth, itemsCount: items.count) contentHeight = layout.height } else { let rowCount = items.count / itemsPerRow + ((items.count % itemsPerRow) == 0 ? 0 : 1) contentHeight = itemWidth * CGFloat(rowCount) } } else { contentHeight = gridFrame.size.height } let initialRevealedRowCount: CGFloat = 4.5 let topInset: CGFloat if case .regular = layout.metrics.widthClass { topInset = 0.0 } else { topInset = insets.top + max(0.0, layout.size.height - floor(initialRevealedRowCount * itemWidth) - insets.top - actionAreaHeight - titleAreaInset) } let additionalGridBottomInset = max(0.0, gridFrame.size.height - actionAreaHeight - contentHeight) let gridInsets = UIEdgeInsets(top: topInset, left: gridLeftInset, bottom: actionAreaHeight + additionalGridBottomInset, 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.didReceiveStickerPackResult { if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf.isReadyValue.set(.single(true)) } } }) if let titlePlaceholderNode = self.titlePlaceholderNode { titlePlaceholderNode.updateAbsoluteRect(titlePlaceholderNode.frame.offsetBy(dx: self.titleContainer.frame.minX, dy: self.titleContainer.frame.minY - gridInsets.top - gridFrame.minY), within: gridFrame.size) } let cancelSize = self.cancelButtonNode.measure(CGSize(width: layout.size.width, height: .greatestFiniteMagnitude)) self.cancelButtonNode.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: 18.0), size: cancelSize) let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - cancelSize.width * 2.0 - 40.0, height: .greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: floor((-titleSize.width) / 2.0), y: floor((-titleSize.height) / 2.0)), size: titleSize) self.moreButtonNode.frame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - 46.0, y: 5.0), size: CGSize(width: 44.0, height: 44.0)) transition.updateAlpha(node: self.cancelButtonNode, alpha: self.isEditing ? 0.0 : 1.0) transition.updateAlpha(node: self.moreButtonNode, alpha: self.isEditing ? 0.0 : 1.0) 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)) let expandScrollProgress = 1.0 - max(0.0, min(1.0, presentationLayout.contentOffset.y / (-gridInsets.top))) let modalProgress = max(0.0, min(1.0, expandScrollProgress)) let expandProgressTransition = transition var expandUpdated = false if abs(self.expandScrollProgress - expandScrollProgress) > CGFloat.ulpOfOne { self.expandScrollProgress = expandScrollProgress expandUpdated = true } if abs(self.expandProgress - expandProgress) > CGFloat.ulpOfOne { self.expandProgress = expandProgress expandUpdated = true } if abs(self.modalProgress - modalProgress) > CGFloat.ulpOfOne { self.modalProgress = modalProgress expandUpdated = true } if expandUpdated { self.expandProgressUpdated(self, expandProgressTransition, self.isAnimatingAutoscroll ? transition : .immediate) } if !transition.isAnimated { self.backgroundNode.layer.removeAllAnimations() self.titleContainer.layer.removeAllAnimations() self.titleSeparatorNode.layer.removeAllAnimations() } var backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: max(minBackgroundY, unclippedBackgroundY)), size: CGSize(width: layout.size.width, height: layout.size.height)) var titleContainerFrame: CGRect if case .regular = layout.metrics.widthClass { backgroundFrame.origin.y = min(0.0, backgroundFrame.origin.y) titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: floor((56.0) / 2.0)), size: CGSize()) } else { titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((56.0) / 2.0)), size: CGSize()) } transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) transition.updateFrame(node: self.titleContainer, frame: titleContainerFrame) transition.updateFrame(node: self.titleSeparatorNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 56.0 - UIScreenPixel), size: CGSize(width: backgroundFrame.width, height: UIScreenPixel))) transition.updateFrame(node: self.titleBackgroundnode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 56.0))) self.titleBackgroundnode.update(size: CGSize(width: layout.size.width, height: 56.0), transition: .immediate) transition.updateFrame(node: self.topContainerNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 56.0))) let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) transition.updateAlpha(node: 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: transaction.scrollToItem, 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 titlePoint = self.view.convert(point, to: self.titleNode.view) if self.titleNode.bounds.contains(titlePoint) { return self.titleNode.view } } 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 weak var controller: StickerPackScreenImpl? private var presentationData: PresentationData private let stickerPacks: [StickerPackReference] private let modalProgressUpdated: (CGFloat, ContainedViewLayoutTransition) -> Void private let dismissed: () -> Void private let presentInGlobalOverlay: (ViewController, Any?) -> Void private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? private let longPressEmoji: ((String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void)? fileprivate let openMention: (String) -> Void private let dimNode: ASDisplayNode private let shadowNode: ASImageNode private let arrowNode: ASImageNode private let containerContainingNode: ASDisplayNode private var containers: [Int: 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 } var onLoading: () -> Void = {} var onReady: () -> Void = {} var onError: () -> Void = {} init( context: AccountContext, controller: StickerPackScreenImpl, stickerPacks: [StickerPackReference], initialSelectedStickerPackIndex: Int, modalProgressUpdated: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, longPressEmoji: ((String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void)?, openMention: @escaping (String) -> Void) { self.context = context self.controller = controller self.presentationData = controller.presentationData self.stickerPacks = stickerPacks self.selectedStickerPackIndex = initialSelectedStickerPackIndex self.modalProgressUpdated = modalProgressUpdated self.dismissed = dismissed self.presentInGlobalOverlay = presentInGlobalOverlay self.sendSticker = sendSticker self.sendEmoji = sendEmoji self.longPressEmoji = longPressEmoji self.openMention = openMention self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.25) self.dimNode.alpha = 0.0 self.shadowNode = ASImageNode() self.shadowNode.displaysAsynchronously = false self.shadowNode.isUserInteractionEnabled = false self.arrowNode = ASImageNode() self.arrowNode.displaysAsynchronously = false self.arrowNode.isUserInteractionEnabled = false self.arrowNode.image = generateArrowImage(color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) self.containerContainingNode = ASDisplayNode() self.containerContainingNode.clipsToBounds = true super.init() self.addSubnode(self.dimNode) self.addSubnode(self.shadowNode) self.addSubnode(self.arrowNode) self.addSubnode(self.containerContainingNode) } override func didLoad() { super.didLoad() self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapGesture(_:)))) } func updatePresentationData(_ presentationData: PresentationData) { for (_, container) in self.containers { container.updatePresentationData(presentationData) } self.arrowNode.image = generateArrowImage(color: presentationData.theme.actionSheet.opaqueItemBackgroundColor) } 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)) let containerContainingFrame: CGRect let containerInsets: UIEdgeInsets if case .regular = layout.metrics.widthClass { self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.01) self.containerContainingNode.cornerRadius = 10.0 let size = CGSize(width: 390.0, height: min(560.0, layout.size.height - 60.0)) var contentRect: CGRect if let sourceRect = self.controller?.getSourceRect?() { let sideSpacing: CGFloat = 10.0 let margin: CGFloat = 64.0 contentRect = CGRect(origin: CGPoint(x: sourceRect.maxX + sideSpacing, y: floor(sourceRect.midY - size.height / 2.0)), size: size) contentRect.origin.y = min(layout.size.height - margin - size.height - layout.intrinsicInsets.bottom, max(margin, contentRect.origin.y)) let arrowSize = CGSize(width: 23.0, height: 12.0) let arrowFrame: CGRect if contentRect.maxX > layout.size.width { contentRect.origin.x = sourceRect.minX - size.width - sideSpacing arrowFrame = CGRect(origin: CGPoint(x: contentRect.maxX - (arrowSize.width - arrowSize.height) / 2.0, y: floor(sourceRect.midY - arrowSize.height / 2.0)), size: arrowSize) self.arrowNode.transform = CATransform3DMakeRotation(-.pi / 2.0, 0.0, 0.0, 1.0) } else { arrowFrame = CGRect(origin: CGPoint(x: contentRect.minX - arrowSize.width + (arrowSize.width - arrowSize.height) / 2.0, y: floor(sourceRect.midY - arrowSize.height / 2.0)), size: arrowSize) self.arrowNode.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0) } self.arrowNode.frame = arrowFrame self.arrowNode.isHidden = false } else { let masterWidth = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) let detailWidth = layout.size.width - masterWidth contentRect = CGRect(origin: CGPoint(x: masterWidth + floor((detailWidth - size.width) / 2.0), y: floor((layout.size.height - size.height) / 2.0)), size: size) self.arrowNode.isHidden = true } containerContainingFrame = contentRect containerInsets = .zero self.shadowNode.alpha = 1.0 if self.shadowNode.image == nil { self.shadowNode.image = generateShadowImage() } } else { self.containerContainingNode.cornerRadius = 0.0 self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.25) containerContainingFrame = CGRect(origin: CGPoint(), size: layout.size) containerInsets = layout.intrinsicInsets self.arrowNode.isHidden = true self.shadowNode.alpha = 0.0 } transition.updateFrame(node: self.containerContainingNode, frame: containerContainingFrame) let shadowFrame = containerContainingFrame.insetBy(dx: -60.0, dy: -60.0) transition.updateFrame(node: self.shadowNode, frame: shadowFrame) let expandProgress: CGFloat = 1.0 let scaledInset: CGFloat = 12.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 let i = 0 let indexOffset = i - self.selectedStickerPackIndex var scaledOffset: CGFloat = 0.0 scaledOffset = -CGFloat(indexOffset) * (1.0 - expandProgress) * (scaledInset * 2.0) + CGFloat(indexOffset) * scaledDistance if abs(indexOffset) <= 1 { let containerTransition: ContainedViewLayoutTransition let container: StickerPackContainer var wasAdded = false if let current = self.containers[i] { containerTransition = transition container = current } else { wasAdded = true containerTransition = .immediate let index = i container = StickerPackContainer(index: index, context: self.context, presentationData: self.presentationData, stickerPacks: self.stickerPacks, loadedStickerPacks: self.controller?.loadedStickerPacks ?? [], decideNextAction: { [weak self] container, action in guard let strongSelf = self, let layout = strongSelf.validLayout else { return .dismiss } if index == strongSelf.stickerPacks.count - 1 { return .dismiss } else { switch action { case .add: var allAdded = true for _ in index + 1 ..< strongSelf.stickerPacks.count { if let container = strongSelf.containers[index], let (_, _, installed) = container.currentStickerPack { if !installed { allAdded = false } } else { allAdded = false } } if allAdded { return .dismiss } case .remove: if strongSelf.stickerPacks.count == 1 { return .dismiss } else { return .ignored } } } 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, expandTransition in guard let strongSelf = self, let layout = strongSelf.validLayout else { return } if index == strongSelf.selectedStickerPackIndex, let container = strongSelf.containers[strongSelf.selectedStickerPackIndex] { let modalProgress = container.modalProgress strongSelf.modalProgressUpdated(modalProgress, transition) strongSelf.containerLayoutUpdated(layout, transition: expandTransition) for (_, otherContainer) in strongSelf.containers { if otherContainer !== container { otherContainer.syncExpandProgress(expandScrollProgress: container.expandScrollProgress, expandProgress: container.expandProgress, modalProgress: container.modalProgress, transition: expandTransition) } } } }, presentInGlobalOverlay: presentInGlobalOverlay, sendSticker: self.sendSticker, sendEmoji: self.sendEmoji, longPressEmoji: self.longPressEmoji, openMention: self.openMention, controller: self.controller) container.onReady = { [weak self] in self?.onReady() } container.onLoading = { [weak self] in self?.onLoading() } container.onError = { [weak self] in self?.onError() } self.containerContainingNode.addSubnode(container) self.containers[i] = container } let containerFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * containerContainingFrame.size.width + self.relativeToSelectedStickerPackTransition + scaledOffset, y: containerVerticalOffset), size: containerContainingFrame.size) containerTransition.updateFrame(node: container, frame: containerFrame, beginWithCurrentState: true) containerTransition.updateSublayerTransformScaleAndOffset(node: container, scale: containerScale, offset: CGPoint(), beginWithCurrentState: true) var containerLayout = layout containerLayout.size = containerFrame.size containerLayout.intrinsicInsets = containerInsets if container.validLayout?.0 != layout { container.updateLayout(layout: containerLayout, transition: containerTransition) } if wasAdded { if let selectedContainer = self.containers[self.selectedStickerPackIndex] { if selectedContainer !== container { container.syncExpandProgress(expandScrollProgress: selectedContainer.expandScrollProgress, expandProgress: selectedContainer.expandProgress, modalProgress: selectedContainer.modalProgress, transition: .immediate) } } } } else { if let container = self.containers[i] { container.removeFromSupernode() self.containers.removeValue(forKey: i) } } if firstTime { if !self.containers.isEmpty { self._ready.set(combineLatest(self.containers.map { (_, container) in container.isReady }) |> map { values -> Bool in for value in values { if !value { return false } } return true }) } else { self._ready.set(.single(true)) } } } @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.stickerPacks.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.stickerPacks.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.stickerPacks.count - 1, Int(self.selectedStickerPackIndex + deltaIndex))) } let deltaOffset = self.relativeToSelectedStickerPackTransition self.relativeToSelectedStickerPackTransition = 0.0 if let layout = self.validLayout { var previousFrames: [Int: CGRect] = [:] for (key, container) in self.containers { previousFrames[key] = container.frame } let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) self.containerLayoutUpdated(layout, transition: .immediate) for (key, container) in self.containers { if let previousFrame = previousFrames[key] { transition.animatePositionAdditive(node: container, offset: CGPoint(x: previousFrame.minX - container.frame.minX, y: 0.0)) } else { transition.animatePositionAdditive(node: container, offset: CGPoint(x: -deltaOffset, y: 0.0)) } } } default: break } } func animateIn() { guard let layout = self.validLayout else { return } self.dimNode.alpha = 1.0 self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) if case .regular = layout.metrics.widthClass { } else { 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.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } } func animateOut(completion: @escaping () -> Void) { guard let layout = self.validLayout else { return } self.dimNode.alpha = 0.0 self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) if case .regular = layout.metrics.widthClass { self.layer.allowsGroupOpacity = true self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in completion() }) } else { 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() }) self.modalProgressUpdated(0.0, .animated(duration: 0.2, curve: .easeInOut)) } } func dismiss() { if self.isDismissed { return } self.isDismissed = true self.animateOut(completion: { [weak self] in self?.dismissed() }) self.dismissAllTooltips() } private func dismissAllTooltips() { guard let controller = self.controller else { return } controller.window?.forEachController({ controller in if let controller = controller as? UndoOverlayController, !controller.keepOnParentDismissal { controller.dismissWithCommitAction() } }) controller.forEachController({ controller in if let controller = controller as? UndoOverlayController, !controller.keepOnParentDismissal { controller.dismissWithCommitAction() } return true }) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } if let selectedContainer = self.containers[self.selectedStickerPackIndex] { if selectedContainer.hitTest(self.view.convert(point, to: selectedContainer.view), with: event) == nil { return self.dimNode.view } } if let result = super.hitTest(point, with: event) { for (index, container) in self.containers { if result.isDescendant(of: container.view) { if index != self.selectedStickerPackIndex { return self.containerContainingNode.view } } } return result } else { return nil } } @objc private func dimNodeTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.dismiss() } } } public final class StickerPackScreenImpl: ViewController, StickerPackScreen { private let context: AccountContext fileprivate var presentationData: PresentationData fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? private var presentationDataDisposable: Disposable? private let stickerPacks: [StickerPackReference] fileprivate let loadedStickerPacks: [LoadedStickerPack] private let initialSelectedStickerPackIndex: Int fileprivate weak var parentNavigationController: NavigationController? private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? fileprivate var controllerNode: StickerPackScreenNode { return self.displayNode as! StickerPackScreenNode } public var dismissed: (() -> Void)? public var actionPerformed: (([(StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction)]) -> Void)? public var getSourceRect: (() -> CGRect?)? private let _ready = Promise() override public var ready: Promise { return self._ready } private let openMentionDisposable = MetaDisposable() private var alreadyDidAppear: Bool = false private var animatedIn: Bool = false fileprivate var initialIsEditing: Bool = false let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer let mainActionTitle: String? let actionTitle: String? public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], selectedStickerPackIndex: Int = 0, mainActionTitle: String? = nil, actionTitle: String? = nil, isEditing: Bool = false, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, actionPerformed: (([(StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction)]) -> Void)? = nil ) { self.context = context self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.updatedPresentationData = updatedPresentationData self.stickerPacks = stickerPacks self.loadedStickerPacks = loadedStickerPacks self.initialSelectedStickerPackIndex = selectedStickerPackIndex self.mainActionTitle = mainActionTitle self.actionTitle = actionTitle self.initialIsEditing = isEditing self.parentNavigationController = parentNavigationController self.sendSticker = sendSticker self.sendEmoji = sendEmoji self.actionPerformed = actionPerformed self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Ignore self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self, strongSelf.isNodeLoaded { strongSelf.presentationData = presentationData strongSelf.controllerNode.updatePresentationData(presentationData) } }) } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.presentationDataDisposable?.dispose() self.openMentionDisposable.dispose() } override public func loadDisplayNode() { self.displayNode = StickerPackScreenNode(context: self.context, controller: self, 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?.dismissed?() 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 } } }, sendEmoji: self.sendEmoji.flatMap { [weak self] sendEmoji in return { text, attribute in sendEmoji(text, attribute) self?.controllerNode.dismiss() } }, longPressEmoji: { [weak self] text, attribute, node, frame in guard let strongSelf = self else { return } var actions: [ContextMenuAction] = [] actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: strongSelf.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in storeMessageTextInPasteboard( text, entities: [ MessageTextEntity( range: 0 ..< (text as NSString).length, type: .CustomEmoji( stickerPack: nil, fileId: attribute.fileId ) ) ] ) if let strongSelf = self, let file = attribute.file { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_EmojiCopied, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) } })) let contextMenuController = makeContextMenuController(actions: actions) strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { return (node, frame.insetBy(dx: -40.0, dy: 0.0), strongSelf.controllerNode, strongSelf.controllerNode.view.bounds) } else { return nil } })) }, openMention: { [weak self] mention in guard let strongSelf = self else { return } strongSelf.openMentionDisposable.set((strongSelf.context.engine.peers.resolvePeerByName(name: mention) |> mapToSignal { result -> Signal in guard case let .result(result) = result else { return .complete() } return .single(result) } |> mapToSignal { peer -> Signal in if let peer = peer { return .single(peer._asPeer()) } else { return .single(nil) } } |> deliverOnMainQueue).start(next: { peer in guard let strongSelf = self else { return } if let peer { if let parentNavigationController = strongSelf.parentNavigationController { strongSelf.controllerNode.dismiss() strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: parentNavigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .always, animated: true)) } } else { strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) }) var loaded = false var dismissed = false var overlayStatusController: ViewController? let cancelImpl: (() -> Void)? = { [weak self] in dismissed = true overlayStatusController?.dismiss() self?.dismiss() } self.controllerNode.onReady = { [weak self] in loaded = true if let strongSelf = self { if !dismissed { if let overlayStatusController = overlayStatusController { overlayStatusController.dismiss() } if strongSelf.alreadyDidAppear { } else { strongSelf.isReady = true } strongSelf.controllerNode.isHidden = false if !strongSelf.animatedIn { strongSelf.animatedIn = true strongSelf.controllerNode.animateIn() } } } } let presentationData = self.presentationData self.controllerNode.onLoading = { [weak self] in Queue.mainQueue().after(0.15, { if !loaded { let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) self?.present(controller, in: .window(.root)) overlayStatusController = controller } }) } self.controllerNode.onError = { loaded = true if let overlayStatusController = overlayStatusController { overlayStatusController.dismiss() } } self.controllerNode.isHidden = true self._ready.set(self.controllerNode.ready.get()) super.displayNodeDidLoad() } private var isReady = false override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !self.alreadyDidAppear { self.alreadyDidAppear = true if self.isReady { self.animatedIn = true self.controllerNode.animateIn() } } } private var validLayout: ContainerViewLayout? override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { let previousSize = self.validLayout?.size super.containerLayoutUpdated(layout, transition: transition) self.validLayout = layout if let previousSize, previousSize != layout.size { Queue.mainQueue().after(0.1) { self.controllerNode.containerLayoutUpdated(layout, transition: transition) } } self.controllerNode.containerLayoutUpdated(layout, transition: transition) } } public enum StickerPackScreenPerformedAction { case add case remove(positionInList: Int) } public func StickerPackScreen( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: StickerPackPreviewControllerMode = .default, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack] = [], mainActionTitle: String? = nil, actionTitle: String? = nil, isEditing: Bool = false, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? = nil, actionPerformed: (([(StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction)]) -> Void)? = nil, dismissed: (() -> Void)? = nil, getSourceRect: (() -> CGRect?)? = nil ) -> ViewController { let controller = StickerPackScreenImpl( context: context, updatedPresentationData: updatedPresentationData, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, selectedStickerPackIndex: stickerPacks.firstIndex(of: mainStickerPack) ?? 0, mainActionTitle: mainActionTitle, actionTitle: actionTitle, isEditing: isEditing, parentNavigationController: parentNavigationController, sendSticker: sendSticker, sendEmoji: sendEmoji, actionPerformed: actionPerformed ) controller.dismissed = dismissed controller.getSourceRect = getSourceRect return controller } private final class StickerPackContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceNode: ContextReferenceContentNode init(controller: ViewController, sourceNode: ContextReferenceContentNode) { self.controller = controller self.sourceNode = sourceNode } func transitionInfo() -> ContextControllerReferenceViewInfo? { return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) } } private func generateShadowImage() -> UIImage? { return generateImage(CGSize(width: 140.0, height: 140.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.saveGState() context.setShadow(offset: CGSize(), blur: 60.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 20.0, height: 20.0), cornerRadius: 10.0).cgPath context.addPath(path) context.fillPath() context.restoreGState() context.setBlendMode(.clear) context.addPath(path) context.fillPath() })?.stretchableImage(withLeftCapWidth: 70, topCapHeight: 70) } private func generateArrowImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 23.0, height: 12.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(color.cgColor) context.translateBy(x: -183.0, y: -209.0) try? drawSvgPath(context, path: "M183.219,208.89 H206.781 C205.648,208.89 204.567,209.371 203.808,210.214 L197.23,217.523 C196.038,218.848 193.962,218.848 192.77,217.523 L186.192,210.214 C185.433,209.371 184.352,208.89 183.219,208.89 Z ") }) } private class ReorderingGestureRecognizer: UIGestureRecognizer { private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: StickerPackPreviewGridItemNode?) private let willBegin: (CGPoint) -> Void private let began: (StickerPackPreviewGridItemNode) -> Void private let ended: (CGPoint?) -> Void private let moved: (CGPoint, CGPoint) -> Void private var initialLocation: CGPoint? private var longPressTimer: SwiftSignalKit.Timer? var animateOnTouch = true private var itemNode: StickerPackPreviewGridItemNode? public init(animateOnTouch: Bool, shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: StickerPackPreviewGridItemNode?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (StickerPackPreviewGridItemNode) -> Void, ended: @escaping (CGPoint?) -> Void, moved: @escaping (CGPoint, CGPoint) -> Void) { self.animateOnTouch = animateOnTouch self.shouldBegin = shouldBegin self.willBegin = willBegin self.began = began self.ended = ended self.moved = moved super.init(target: nil, action: nil) } deinit { self.longPressTimer?.invalidate() } private func startLongPressTimer() { self.longPressTimer?.invalidate() let longPressTimer = SwiftSignalKit.Timer(timeout: 0.3, repeat: false, completion: { [weak self] in self?.longPressTimerFired() }, queue: Queue.mainQueue()) self.longPressTimer = longPressTimer longPressTimer.start() } private func stopLongPressTimer() { self.itemNode = nil self.longPressTimer?.invalidate() self.longPressTimer = nil } override public func reset() { super.reset() self.itemNode = nil self.stopLongPressTimer() self.initialLocation = nil } private func longPressTimerFired() { guard let _ = self.initialLocation else { return } self.state = .began self.longPressTimer?.invalidate() self.longPressTimer = nil if let itemNode = self.itemNode { self.began(itemNode) } } override public func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) if self.numberOfTouches > 1 { self.state = .failed self.ended(nil) return } if self.state == .possible { if let location = touches.first?.location(in: self.view) { let (allowed, requiresLongPress, itemNode) = self.shouldBegin(location) if allowed { if let itemNode = itemNode, self.animateOnTouch { itemNode.layer.animateScale(from: 1.0, to: 0.98, duration: 0.2, delay: 0.1) } self.itemNode = itemNode self.initialLocation = location if requiresLongPress { self.startLongPressTimer() } else { self.state = .began if let itemNode = self.itemNode { self.began(itemNode) } } } else { self.state = .failed } } else { self.state = .failed } } } override public func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) self.initialLocation = nil if self.longPressTimer != nil { self.stopLongPressTimer() self.state = .failed } if self.state == .began || self.state == .changed { if let location = touches.first?.location(in: self.view) { self.ended(location) } else { self.ended(nil) } self.state = .failed } } override public func touchesCancelled(_ touches: Set, with event: UIEvent) { super.touchesCancelled(touches, with: event) self.initialLocation = nil if self.longPressTimer != nil { self.stopLongPressTimer() self.state = .failed } if self.state == .began || self.state == .changed { self.ended(nil) self.state = .failed } } override public func touchesMoved(_ touches: Set, with event: UIEvent) { super.touchesMoved(touches, with: event) if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { self.state = .changed self.moved(location, CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)) } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil { let touchLocation = touch.location(in: self.view) let dX = touchLocation.x - initialTapLocation.x let dY = touchLocation.y - initialTapLocation.y if dX * dX + dY * dY > 3.0 * 3.0 { self.itemNode?.layer.removeAllAnimations() self.stopLongPressTimer() self.initialLocation = nil self.state = .failed } } } } private func generateShadowImage(corners: CACornerMask, radius: CGFloat) -> UIImage? { return generateImage(CGSize(width: 120.0, height: 120), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) // context.saveGState() context.setShadow(offset: CGSize(), blur: 28.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) var rectCorners: UIRectCorner = [] if corners.contains(.layerMinXMinYCorner) { rectCorners.insert(.topLeft) } if corners.contains(.layerMaxXMinYCorner) { rectCorners.insert(.topRight) } if corners.contains(.layerMinXMaxYCorner) { rectCorners.insert(.bottomLeft) } if corners.contains(.layerMaxXMaxYCorner) { rectCorners.insert(.bottomRight) } let path = UIBezierPath(roundedRect: CGRect(x: 30.0, y: 30.0, width: 60.0, height: 60.0), byRoundingCorners: rectCorners, cornerRadii: CGSize(width: radius, height: radius)).cgPath context.addPath(path) context.fillPath() // context.restoreGState() // context.setBlendMode(.clear) // context.addPath(path) // context.fillPath() })?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60) } private final class CopyView: UIView { let shadow: UIImageView var snapshotView: UIView? init(frame: CGRect, corners: CACornerMask, radius: CGFloat) { self.shadow = UIImageView() self.shadow.contentMode = .scaleToFill super.init(frame: frame) self.addSubview(self.shadow) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } private final class ReorderingItemNode: ASDisplayNode { weak var itemNode: StickerPackPreviewGridItemNode? var currentState: (Int, Int)? private let copyView: CopyView private let initialLocation: CGPoint init(itemNode: StickerPackPreviewGridItemNode, initialLocation: CGPoint) { self.itemNode = itemNode self.copyView = CopyView(frame: CGRect(), corners: [], radius: 0.0) let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false) self.initialLocation = initialLocation super.init() if let snapshotView = snapshotView { snapshotView.frame = CGRect(origin: CGPoint(), size: itemNode.bounds.size) snapshotView.bounds.origin = itemNode.bounds.origin snapshotView.layer.shadowRadius = 10.0 snapshotView.layer.shadowColor = UIColor.black.cgColor self.copyView.addSubview(snapshotView) self.copyView.snapshotView = snapshotView } self.view.addSubview(self.copyView) self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: itemNode.bounds.size) self.copyView.shadow.frame = CGRect(origin: CGPoint(x: -30.0, y: -30.0), size: CGSize(width: itemNode.bounds.size.width + 60.0, height: itemNode.bounds.size.height + 60.0)) self.copyView.shadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) self.copyView.snapshotView?.layer.animateScale(from: 1.0, to: 1.1, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) self.copyView.shadow.layer.animateScale(from: 1.0, to: 1.1, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } func updateOffset(offset: CGPoint) { self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x + offset.x, y: initialLocation.y + offset.y), size: copyView.bounds.size) } func currentOffset() -> CGFloat? { return self.copyView.center.y } func animateCompletion(completion: @escaping () -> Void) { if let itemNode = self.itemNode { itemNode.view.superview?.bringSubviewToFront(itemNode.view) itemNode.layer.animateScale(from: 1.1, to: 1.0, duration: 0.25, removeOnCompletion: false) // let sourceFrame = self.view.convert(self.copyView.frame, to: itemNode.supernode?.view) // let targetFrame = itemNode.frame // itemNode.updateLayout(size: sourceFrame.size, transition: .immediate) // itemNode.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in // completion() // }) // itemNode.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.3, curve: .spring)) itemNode.isHidden = false self.copyView.isHidden = true completion() } else { completion() } } }