import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import AccountContext import ChatPresentationInterfaceState import ComponentFlow import EntityKeyboard import AnimationCache import MultiAnimationRenderer import Postbox import TelegramCore import ComponentDisplayAdapters import TextFormat import PagerComponent import AppBundle import PremiumUI import AudioToolbox import UndoUI import ContextUI import GalleryUI import TelegramPresentationData import TelegramNotices import StickerPeekUI import ChatInputNode import TelegramUIPreferences import MultiplexedVideoNode import ChatControllerInteraction import FeaturedStickersScreen import Pasteboard import StickerPackPreviewUI import EntityKeyboardGifContent import LegacyMessageInputPanelInputView import AttachmentTextInputPanelNode public final class EmptyInputView: UIView, UIInputViewAudioFeedback { public var enableInputClicksWhenVisible: Bool { return true } } public struct ChatMediaInputPaneScrollState { let absoluteOffset: CGFloat? let relativeChange: CGFloat } public final class ChatEntityKeyboardInputNode: ChatInputNode { public final class Interaction { let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool, Bool) -> Bool let updateChoosingSticker: (Bool) -> Void let switchToTextInput: () -> Void let dismissTextInput: () -> Void let insertText: (NSAttributedString) -> Void let backwardsDeleteText: () -> Void let openStickerEditor: () -> Void let presentController: (ViewController, Any?) -> Void let presentGlobalOverlayController: (ViewController, Any?) -> Void let getNavigationController: () -> NavigationController? let requestLayout: (ContainedViewLayoutTransition) -> Void public var forceTheme: PresentationTheme? public init( sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool, sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void, sendGif: @escaping (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool, sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool, Bool) -> Bool, updateChoosingSticker: @escaping (Bool) -> Void, switchToTextInput: @escaping () -> Void, dismissTextInput: @escaping () -> Void, insertText: @escaping (NSAttributedString) -> Void, backwardsDeleteText: @escaping () -> Void, openStickerEditor: @escaping () -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void ) { self.sendSticker = sendSticker self.sendEmoji = sendEmoji self.sendGif = sendGif self.sendBotContextResultAsGif = sendBotContextResultAsGif self.updateChoosingSticker = updateChoosingSticker self.switchToTextInput = switchToTextInput self.dismissTextInput = dismissTextInput self.insertText = insertText self.backwardsDeleteText = backwardsDeleteText self.openStickerEditor = openStickerEditor self.presentController = presentController self.presentGlobalOverlayController = presentGlobalOverlayController self.getNavigationController = getNavigationController self.requestLayout = requestLayout } public init(chatControllerInteraction: ChatControllerInteraction, panelInteraction: ChatPanelInterfaceInteraction) { self.sendSticker = chatControllerInteraction.sendSticker self.sendEmoji = chatControllerInteraction.sendEmoji self.sendGif = chatControllerInteraction.sendGif self.sendBotContextResultAsGif = chatControllerInteraction.sendBotContextResultAsGif self.updateChoosingSticker = chatControllerInteraction.updateChoosingSticker self.switchToTextInput = { [weak chatControllerInteraction] in chatControllerInteraction?.updateInputMode { _ in return .text } } self.dismissTextInput = chatControllerInteraction.dismissTextInput self.insertText = panelInteraction.insertText self.backwardsDeleteText = panelInteraction.backwardsDeleteText self.openStickerEditor = chatControllerInteraction.openStickerEditor self.presentController = chatControllerInteraction.presentController self.presentGlobalOverlayController = chatControllerInteraction.presentGlobalOverlayController self.getNavigationController = chatControllerInteraction.navigationController self.requestLayout = panelInteraction.requestLayout } } public struct InputData: Equatable { public var emoji: EmojiPagerContentComponent? public var stickers: EmojiPagerContentComponent? public var gifs: EntityKeyboardGifContent? public var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] public init( emoji: EmojiPagerContentComponent?, stickers: EmojiPagerContentComponent?, gifs: EntityKeyboardGifContent?, availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] ) { self.emoji = emoji self.stickers = stickers self.gifs = gifs self.availableGifSearchEmojies = availableGifSearchEmojies } } public final class StateContext { let emojiState = EmojiPagerContentComponent.StateContext() public init() { } } public static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal { let hasPremium: Signal if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId { hasPremium = .single(true) } else { hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in guard case let .user(user) = peer else { return false } return user.isPremium } |> distinctUntilChanged } return hasPremium } public static func inputData(context: AccountContext, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool, hasTrending: Bool = true, hasSearch: Bool = true, hideBackground: Bool = false, sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)?) -> Signal { let animationCache = context.animationCache let animationRenderer = context.animationRenderer let emojiItems = EmojiPagerContentComponent.emojiInputData( context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, subject: .emoji, hasTrending: hasTrending, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId, hasSearch: hasSearch, hideBackground: hideBackground ) let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: hasSearch, hasTrending: hasTrending, forceHasPremium: false, hasEdit: true, hideBackground: hideBackground) let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) |> map { appConfiguration -> [String] in let defaultReactions: [String] = ["👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"] guard let data = appConfiguration.data, let emojis = data["gif_search_emojies"] as? [String] else { return defaultReactions } return emojis } |> distinctUntilChanged let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) |> map { animatedEmoji -> [String: [StickerPackItem]] in var animatedEmojiStickers: [String: [StickerPackItem]] = [:] switch animatedEmoji { case let .result(_, items, _): for item in items { if let emoji = item.getStringRepresentationsOfIndexKeys().first { animatedEmojiStickers[emoji.basicEmoji.0] = [item] let strippedEmoji = emoji.basicEmoji.0.strippedEmoji if animatedEmojiStickers[strippedEmoji] == nil { animatedEmojiStickers[strippedEmoji] = [item] } } } default: break } return animatedEmojiStickers } let gifInputInteraction = GifPagerContentComponent.InputInteraction( performItemAction: { item, view, rect in if let sendGif { let _ = sendGif(item.file, view, rect, false, false) } }, openGifContextMenu: { _, _, _, _, _ in }, loadMore: { _ in }, openSearch: { }, updateSearchQuery: { _ in }, hideBackground: hideBackground, hasSearch: hasSearch ) // We are going to subscribe to the actual data when the view is loaded let gifItems: Signal = .single(EntityKeyboardGifContent( hasRecentGifs: true, component: GifPagerContentComponent( context: context, inputInteraction: gifInputInteraction, subject: .recent, items: [], isLoading: false, loadMoreToken: nil, displaySearchWithPlaceholder: nil, searchCategories: nil, searchInitiallyHidden: true, searchState: .empty(hasResults: false), hideBackground: hideBackground ) )) return combineLatest(queue: .mainQueue(), emojiItems, stickerItems, gifItems, reactions, animatedEmojiStickers ) |> map { emoji, stickers, gifs, reactions, animatedEmojiStickers -> InputData in var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] = [] for reaction in reactions { if let file = animatedEmojiStickers[reaction]?.first?.file { var title: String? switch reaction { case "😡": title = strings.Gif_Emotion_Angry case "😮": title = strings.Gif_Emotion_Surprised case "😂": title = strings.Gif_Emotion_Joy case "😘": title = strings.Gif_Emotion_Kiss case "😍": title = strings.Gif_Emotion_Hearts case "👍": title = strings.Gif_Emotion_ThumbsUp case "👎": title = strings.Gif_Emotion_ThumbsDown case "🙄": title = strings.Gif_Emotion_RollEyes case "😎": title = strings.Gif_Emotion_Cool case "🥳": title = strings.Gif_Emotion_Party default: break } guard let title = title else { continue } availableGifSearchEmojies.append(EntityKeyboardComponent.GifSearchEmoji(emoji: reaction, file: file, title: title)) } } return InputData( emoji: emoji, stickers: stickers, gifs: gifs, availableGifSearchEmojies: availableGifSearchEmojies ) } } private let context: AccountContext private let stateContext: StateContext? private let entityKeyboardView: ComponentHostView private let defaultToEmojiTab: Bool private var stableReorderableGroupOrder: [EntityKeyboardComponent.ReorderCategory: [ItemCollectionId]] = [:] private var currentInputData: InputData private var inputDataDisposable: Disposable? private var hasRecentGifsDisposable: Disposable? private let opaqueTopPanelBackground: Bool private struct EmojiSearchResult { var groups: [EmojiPagerContentComponent.ItemGroup] var id: AnyHashable var version: Int var isPreset: Bool } private struct EmojiSearchState { var result: EmojiSearchResult? var isSearching: Bool init(result: EmojiSearchResult?, isSearching: Bool) { self.result = result self.isSearching = isSearching } } private let emojiSearchDisposable = MetaDisposable() private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { didSet { self.emojiSearchState.set(.single(self.emojiSearchStateValue)) } } private let stickerSearchDisposable = MetaDisposable() private let stickerSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) private var stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { didSet { self.stickerSearchState.set(.single(self.stickerSearchStateValue)) } } private let interaction: ChatEntityKeyboardInputNode.Interaction? private var inputNodeInteraction: ChatMediaInputNodeInteraction? private let trendingGifsPromise = Promise(nil) private var isMarkInputCollapsed: Bool = false private var isEmojiSearchActive: Bool = false { didSet { self.followsDefaultHeight = !self.isEmojiSearchActive } } fileprivate var clipContentToTopPanel: Bool = false public var externalTopPanelContainerImpl: PagerExternalTopPanelContainer? public override var externalTopPanelContainer: UIView? { return self.externalTopPanelContainerImpl } public var switchToTextInput: (() -> Void)? private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)? private var scheduledContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private var scheduledInnerTransition: Transition? private var gifMode: GifPagerContentComponent.Subject? { didSet { if let gifMode = self.gifMode, gifMode != oldValue { self.reloadGifContext() } } } public var canSwitchToTextInputAutomatically: Bool { if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { if centralId == AnyHashable("emoji") { return false } } return true } public var useExternalSearchContainer: Bool = false private var gifContext: GifContext? { didSet { if let gifContext = self.gifContext { self.gifComponent.set(gifContext.component) } } } private let gifComponent = Promise() private var gifInputInteraction: GifPagerContentComponent.InputInteraction? fileprivate var emojiInputInteraction: EmojiPagerContentComponent.InputInteraction? private var stickerInputInteraction: EmojiPagerContentComponent.InputInteraction? private weak var currentUndoOverlayController: UndoOverlayController? private var choosingStickerDisposable: Disposable? private var scrollingStickersGridPromise = Promise(false) private var previewingStickersPromise = ValuePromise(false) private var choosingSticker: Signal { return combineLatest(self.scrollingStickersGridPromise.get(), self.previewingStickersPromise.get()) |> map { scrollingStickersGrid, previewingStickers -> Bool in return scrollingStickersGrid || previewingStickers } |> distinctUntilChanged } public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?) { self.context = context self.currentInputData = currentInputData self.defaultToEmojiTab = defaultToEmojiTab self.opaqueTopPanelBackground = opaqueTopPanelBackground self.stateContext = stateContext self.interaction = interaction self.entityKeyboardView = ComponentHostView() super.init() self.currentInputData = self.processInputData(inputData: self.currentInputData) self.topBackgroundExtension = 34.0 self.followsDefaultHeight = true self.view.addSubview(self.entityKeyboardView) self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer() var stickerPeekBehavior: EmojiContentPeekBehaviorImpl? if let interaction { let context = self.context stickerPeekBehavior = EmojiContentPeekBehaviorImpl( context: self.context, interaction: EmojiContentPeekBehaviorImpl.Interaction( sendSticker: interaction.sendSticker, sendEmoji: { file in 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 { interaction.sendEmoji(text, emojiAttribute, true) } }, setStatus: { [weak self] file in guard let strongSelf = self else { return } let _ = strongSelf.context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).start() var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() strongSelf.currentUndoOverlayController = nil animateInAsReplacement = true } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = 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 }) strongSelf.currentUndoOverlayController = controller interaction.presentController(controller, nil) }, copyEmoji: { [weak self] file in guard let strongSelf = self else { return } 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))]) var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() strongSelf.currentUndoOverlayController = nil animateInAsReplacement = true } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_EmojiCopied, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false }) strongSelf.currentUndoOverlayController = controller interaction.presentController(controller, nil) } }, presentController: interaction.presentController, presentGlobalOverlayController: interaction.presentGlobalOverlayController, navigationController: interaction.getNavigationController, updateIsPreviewing: { [weak self] value in self?.previewingStickersPromise.set(value) } ), chatPeerId: chatPeerId, present: { c, a in interaction.presentGlobalOverlayController(c, a) } ) } var premiumToastCounter = 0 self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self, weak interaction] groupId, item, _, _, _, _ in let _ = ( combineLatest( ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false) ) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium, hasGlobalPremium in guard let strongSelf = self, let interaction else { return } if groupId == AnyHashable("featuredTop"), let file = item.itemFile { let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) let _ = (combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), context.account.postbox.combinedView(keys: [viewKey]) ) |> take(1) |> deliverOnMainQueue).start(next: { [weak interaction] emojiPacksView, views in guard let interaction else { return } guard let view = views.views[viewKey] as? OrderedItemListView else { return } guard let self else { return } let _ = interaction var installedCollectionIds = Set() for (id, _, _) in emojiPacksView.collectionInfos { installedCollectionIds.insert(id) } let stickerPacks = view.items.map({ $0.contents.get(FeaturedStickerPackItem.self)! }).filter({ !installedCollectionIds.contains($0.info.id) }) for featuredStickerPack in stickerPacks { if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) { if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiInputInteraction { pagerView.openCustomSearch(content: EmojiSearchContent( context: self.context, forceTheme: self.interaction?.forceTheme, items: stickerPacks, initialFocusId: featuredStickerPack.info.id, hasPremiumForUse: hasPremium, hasPremiumForInstallation: hasGlobalPremium, parentInputInteraction: emojiInputInteraction )) } /*let controller = StickerPackScreen( context: context, updatedPresentationData: controllerInteraction.updatedPresentationData, mode: .default, mainStickerPack: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), stickerPacks: [.id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash)], loadedStickerPacks: [.result(info: featuredStickerPack.info, items: featuredStickerPack.topItems, installed: false)], parentNavigationController: controllerInteraction.navigationController(), sendSticker: nil, sendEmoji: { [weak interfaceInteraction] text, emojiAttribute in guard let interfaceInteraction else { return } interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) } ) controllerInteraction.presentController(controller, nil)*/ break } } }) } else if let file = item.itemFile { var text = "." var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, displayText, _): text = displayText var packId: ItemCollectionId? if let id = groupId.base as? ItemCollectionId { packId = id } emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: packId, fileId: file.fileId.id, file: file) break loop default: break } } if file.isPremiumEmoji && !hasPremium && groupId != AnyHashable("peerSpecific") { var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() strongSelf.currentUndoOverlayController = nil animateInAsReplacement = true } let presentationData = context.sharedContext.currentPresentationData.with { $0 } premiumToastCounter += 1 let suggestSavedMessages = premiumToastCounter % 2 == 0 let text: String let actionTitle: String if suggestSavedMessages { text = presentationData.strings.EmojiInput_PremiumEmojiToast_TryText actionTitle = presentationData.strings.EmojiInput_PremiumEmojiToast_TryAction } else { text = presentationData.strings.EmojiInput_PremiumEmojiToast_Text actionTitle = presentationData.strings.EmojiInput_PremiumEmojiToast_Action } let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: text, undoText: actionTitle, customAction: { [weak interaction] in guard let interaction else { return } if suggestSavedMessages { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer = peer, let navigationController = interaction.getNavigationController() else { return } context.sharedContext.navigateToChatController(NavigateToChatControllerParams( navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, updateTextInputState: nil, activateInput: .entityInput, keepStack: .always, completion: { _ in }) ) }) } 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) } interaction.getNavigationController()?.pushViewController(controller) } }), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false }) strongSelf.currentUndoOverlayController = controller interaction.presentController(controller, nil) return } if let emojiAttribute = emojiAttribute { AudioServicesPlaySystemSound(0x450) interaction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) } } else if case let .staticEmoji(staticEmoji) = item.content { AudioServicesPlaySystemSound(0x450) interaction.insertText(NSAttributedString(string: staticEmoji, attributes: [:])) } }) }, deleteBackwards: { [weak interaction] in if let interaction { interaction.backwardsDeleteText() } }, openStickerSettings: { }, openFeatured: { }, openSearch: { }, addGroupAction: { [weak self, weak interaction] groupId, isPremiumLocked, scrollToGroup in guard let interaction, let collectionId = groupId.base as? ItemCollectionId else { return } if isPremiumLocked { 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) } interaction.getNavigationController()?.pushViewController(controller) return } let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) |> deliverOnMainQueue).start(next: { views in guard let view = views.views[viewKey] as? OrderedItemListView else { return } for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { if featuredEmojiPack.info.id == collectionId { if let strongSelf = self { strongSelf.scheduledContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId, scrollToGroup: scrollToGroup)) } let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() break } } }) }, clearGroup: { [weak interaction] groupId in guard let interaction else { return } if groupId == AnyHashable("recent") { interaction.dismissTextInput() var presentationData = context.sharedContext.currentPresentationData.with { $0 } if let forceTheme = interaction.forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: presentationData.strings.Emoji_ClearRecent, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let _ = context.engine.stickers.clearRecentlyUsedEmoji().start() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) interaction.presentController(actionSheet, nil) } else if groupId == AnyHashable("featuredTop") { let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) |> deliverOnMainQueue).start(next: { views in guard let view = views.views[viewKey] as? OrderedItemListView else { return } var emojiPackIds: [Int64] = [] for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { emojiPackIds.append(featuredEmojiPack.info.id.id) } let _ = ApplicationSpecificNotice.setDismissedTrendingEmojiPacks(accountManager: context.sharedContext.accountManager, values: emojiPackIds).start() }) } }, editAction: { _ in }, pushController: { [weak interaction] controller in guard let interaction else { return } interaction.getNavigationController()?.pushViewController(controller) }, presentController: { [weak interaction] controller in guard let interaction else { return } interaction.presentController(controller, nil) }, presentGlobalOverlayController: { [weak interaction] controller in guard let interaction else { return } interaction.presentGlobalOverlayController(controller, nil) }, navigationController: { [weak interaction] in return interaction?.getNavigationController() }, requestUpdate: { [weak self] transition in guard let strongSelf = self else { return } if !transition.animation.isImmediate { strongSelf.interaction?.requestLayout(transition.containedViewLayoutTransition) } }, updateSearchQuery: { [weak self] query in guard let self = self else { return } switch query { case .none: self.emojiSearchDisposable.set(nil) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) case let .text(rawQuery, languageCode): let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) if query.isEmpty { self.emojiSearchDisposable.set(nil) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) } else { let context = self.context var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { signal = signal |> mapToSignal { keywords in return .single(keywords) |> then( context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) |> map { englishKeywords in return keywords + englishKeywords } ) } } let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in guard case let .user(user) = peer else { return false } return user.isPremium } |> distinctUntilChanged let resultSignal = combineLatest( signal, hasPremium ) |> mapToSignal { keywords, hasPremium -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { allEmoticons[emoticon] = keyword.keyword } } let remoteSignal: Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> if hasPremium { remoteSignal = context.engine.stickers.searchEmoji(emojiString: Array(allEmoticons.keys)) remotePacksSignal = context.engine.stickers.searchEmojiSets(query: query) |> mapToSignal { localResult in return .single((localResult, false)) |> then( context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map { remoteResult in return (localResult.merge(with: remoteResult), true) } ) } } else { remoteSignal = .single(([], true)) remotePacksSignal = .single((FoundStickerSets(), true)) } return combineLatest(remoteSignal, remotePacksSignal) |> mapToSignal { foundEmoji, foundPacks -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in if foundEmoji.items.isEmpty && !foundEmoji.isFinalResult { return .complete() } var items: [EmojiPagerContentComponent.Item] = [] let appendUnicodeEmoji = { for (_, list) in EmojiPagerContentComponent.staticEmojiMapping { for emojiString in list { if allEmoticons[emojiString] != nil { let item = EmojiPagerContentComponent.Item( animationData: nil, content: .staticEmoji(emojiString), itemFile: nil, subgroupId: nil, icon: .none, tintMode: .none ) items.append(item) } } } } if !hasPremium { appendUnicodeEmoji() } var existingIds = Set() for itemFile in foundEmoji.items { if existingIds.contains(itemFile.fileId) { continue } existingIds.insert(itemFile.fileId) if itemFile.isPremiumEmoji && !hasPremium { continue } let animationData = EntityKeyboardAnimationData(file: itemFile) let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: itemFile, subgroupId: nil, icon: .none, tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) } if hasPremium { appendUnicodeEmoji() } var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] resultGroups.append(EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, subtitle: nil, badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, fillWithLoadingPlaceholders: false, items: items )) for (collectionId, info, _, _) in foundPacks.sets.infos { if let info = info as? StickerPackCollectionInfo { var topItems: [StickerPackItem] = [] for e in foundPacks.sets.entries { if let item = e.item as? StickerPackItem { if e.index.collectionId == collectionId { topItems.append(item) } } } var groupItems: [EmojiPagerContentComponent.Item] = [] for item in topItems { var tintMode: EmojiPagerContentComponent.Item.TintMode = .none if item.file.isCustomTemplateEmoji { tintMode = .primary } let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: .none, tintMode: tintMode ) groupItems.append(resultItem) } resultGroups.append(EmojiPagerContentComponent.ItemGroup( supergroupId: AnyHashable(info.id), groupId: AnyHashable(info.id), title: info.title, subtitle: nil, badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, hasEdit: false, collapsedLineCount: 3, displayPremiumBadges: false, headerItem: nil, fillWithLoadingPlaceholders: false, items: groupItems )) } } return .single(resultGroups) } } var version = 0 self.emojiSearchStateValue.isSearching = true self.emojiSearchDisposable.set((resultSignal |> delay(0.15, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) version += 1 })) } case let .category(value): let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] var existingIds = Set() for itemFile in files { if existingIds.contains(itemFile.fileId) { continue } existingIds.insert(itemFile.fileId) let animationData = EntityKeyboardAnimationData(file: itemFile) let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: itemFile, subgroupId: nil, icon: .none, tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) } return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, subtitle: nil, badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, fillWithLoadingPlaceholders: false, items: items )], isFinalResult)) } var version = 0 self.emojiSearchDisposable.set((resultSignal |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } guard let group = result.items.first else { return } if group.items.isEmpty && !result.isFinalResult { self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [ EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, subtitle: nil, badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, fillWithLoadingPlaceholders: true, items: [] ) ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) return } self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) version += 1 })) } }, updateScrollingToItemGroup: { }, onScroll: {}, chatPeerId: chatPeerId, peekBehavior: stickerPeekBehavior, customLayout: nil, externalBackground: nil, externalExpansionView: nil, customContentView: nil, useOpaqueTheme: false, hideBackground: false, stateContext: self.stateContext?.emojiState, addImage: nil ) self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak interaction] groupId, item, view, rect, layer, _ in let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in guard let interaction else { return } guard let file = item.itemFile else { if groupId == AnyHashable("recent"), case .icon(.add) = item.content { interaction.openStickerEditor() } return } if groupId == AnyHashable("featuredTop") { let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) |> deliverOnMainQueue).start(next: { [weak interaction] views in guard let interaction else { return } guard let view = views.views[viewKey] as? OrderedItemListView else { return } for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) { interaction.getNavigationController()?.pushViewController(FeaturedStickersScreen( context: context, highlightedPackId: featuredStickerPack.info.id, forceTheme: interaction.forceTheme, sendSticker: { [weak interaction] fileReference, sourceNode, sourceRect in guard let interaction else { return false } return interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) } )) break } } }) } else { if file.isPremiumSticker && !hasPremium { let controller = PremiumIntroScreen(context: context, source: .stickers) interaction.getNavigationController()?.pushViewController(controller) return } var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] if let id = groupId.base as? ItemCollectionId, context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { bubbleUpEmojiOrStickersets.append(id) } let _ = interaction.sendSticker(.standalone(media: file), false, false, nil, false, view, rect, layer, bubbleUpEmojiOrStickersets) } }) }, deleteBackwards: { [weak interaction] in if let interaction { interaction.backwardsDeleteText() } }, openStickerSettings: { [weak interaction] in guard let interaction else { return } let controller = context.sharedContext.makeInstalledStickerPacksController(context: context, mode: .modal, forceTheme: interaction.forceTheme) controller.navigationPresentation = .modal interaction.getNavigationController()?.pushViewController(controller) }, openFeatured: { [weak interaction] in guard let interaction else { return } interaction.getNavigationController()?.pushViewController(FeaturedStickersScreen( context: context, highlightedPackId: nil, forceTheme: interaction.forceTheme, sendSticker: { [weak interaction] fileReference, sourceNode, sourceRect in guard let interaction else { return false } return interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) } )) }, openSearch: { [weak self] in if let strongSelf = self, let pagerView = strongSelf.entityKeyboardView.componentView as? EntityKeyboardComponent.View { pagerView.openSearch() } }, addGroupAction: { [weak interaction] groupId, isPremiumLocked, _ in guard let interaction, let collectionId = groupId.base as? ItemCollectionId else { return } if isPremiumLocked { let controller = PremiumIntroScreen(context: context, source: .stickers) interaction.getNavigationController()?.pushViewController(controller) return } let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) |> deliverOnMainQueue).start(next: { views in guard let view = views.views[viewKey] as? OrderedItemListView else { return } for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { if featuredStickerPack.info.id == collectionId { let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false) |> mapToSignal { result -> Signal in switch result { case let .result(info, items, installed): if installed { return .complete() } else { return context.engine.stickers.addStickerPackInteractively(info: info, items: items) } case .fetching: break case .none: break } return .complete() } |> deliverOnMainQueue).start(completed: { }) break } } }) }, clearGroup: { [weak interaction] groupId in guard let interaction else { return } if groupId == AnyHashable("recent") { interaction.dismissTextInput() var presentationData = context.sharedContext.currentPresentationData.with { $0 } if let forceTheme = interaction.forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: presentationData.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let _ = context.engine.stickers.clearRecentlyUsedStickers().start() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) interaction.presentController(actionSheet, nil) } else if groupId == AnyHashable("featuredTop") { let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) |> deliverOnMainQueue).start(next: { views in guard let view = views.views[viewKey] as? OrderedItemListView else { return } var stickerPackIds: [Int64] = [] for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { stickerPackIds.append(featuredStickerPack.info.id.id) } let _ = ApplicationSpecificNotice.setDismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager, values: stickerPackIds).start() }) } else if groupId == AnyHashable("peerSpecific") { } }, editAction: { [weak interaction] groupId in guard let collectionId = groupId.base as? ItemCollectionId else { return } let viewKey = PostboxViewKey.itemCollectionInfo(id: collectionId) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) |> deliverOnMainQueue).start(next: { [weak interaction] views in guard let interaction, let view = views.views[viewKey] as? ItemCollectionInfoView, let info = view.info as? StickerPackCollectionInfo else { return } let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let controller = context.sharedContext.makeStickerPackScreen( context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], isEditing: true, parentNavigationController: nil, sendSticker: { [weak interaction] fileReference, sourceView, sourceRect in return interaction?.sendSticker(fileReference, false, false, nil, false, sourceView, sourceRect, nil, []) ?? false } ) interaction.presentController(controller, nil) }) }, pushController: { [weak interaction] controller in guard let interaction else { return } interaction.getNavigationController()?.pushViewController(controller) }, presentController: { [weak interaction] controller in guard let interaction else { return } interaction.presentController(controller, nil) }, presentGlobalOverlayController: { [weak interaction] controller in guard let interaction else { return } interaction.presentGlobalOverlayController(controller, nil) }, navigationController: { [weak interaction] in return interaction?.getNavigationController() }, requestUpdate: { _ in }, updateSearchQuery: { [weak self] query in guard let strongSelf = self else { return } switch query { case .none: strongSelf.stickerSearchDisposable.set(nil) strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case .text: strongSelf.stickerSearchDisposable.set(nil) strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .category(value): let resultSignal = strongSelf.context.engine.stickers.searchStickers(query: value, scope: [.installed, .remote]) |> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] var existingIds = Set() for item in files.items { let itemFile = item.file if existingIds.contains(itemFile.fileId) { continue } existingIds.insert(itemFile.fileId) let animationData = EntityKeyboardAnimationData(file: itemFile) let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: itemFile, subgroupId: nil, icon: itemFile.isPremiumSticker ? .premium : .none, tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) } return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, subtitle: nil, badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, fillWithLoadingPlaceholders: false, items: items )], files.isFinalResult)) } var version = 0 strongSelf.stickerSearchDisposable.set((resultSignal |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } guard let group = result.items.first else { return } if group.items.isEmpty && !result.isFinalResult { //strongSelf.stickerSearchStateValue.isSearching = true strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [ EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, subtitle: nil, badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, fillWithLoadingPlaceholders: true, items: [] ) ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) return } strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) version += 1 })) } }, updateScrollingToItemGroup: { }, onScroll: {}, chatPeerId: chatPeerId, peekBehavior: stickerPeekBehavior, customLayout: nil, externalBackground: nil, externalExpansionView: nil, customContentView: nil, useOpaqueTheme: false, hideBackground: false, stateContext: nil, addImage: nil ) self.inputDataDisposable = (combineLatest(queue: .mainQueue(), updatedInputData, .single(self.currentInputData.gifs) |> then(self.gifComponent.get() |> map(Optional.init)), self.emojiSearchState.get(), self.stickerSearchState.get() ) |> deliverOnMainQueue).start(next: { [weak self] inputData, gifs, emojiSearchState, stickerSearchState in guard let strongSelf = self else { return } var inputData = inputData inputData.gifs = gifs let presentationData = context.sharedContext.currentPresentationData.with { $0 } if let emojiSearchResult = emojiSearchState.result { var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult, iconFile: nil ) } if let emoji = inputData.emoji { let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty(hasResults: true) inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState) } } else if emojiSearchState.isSearching { if let emoji = inputData.emoji { inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emoji.contentItemGroups, itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults, searchState: .searching) } } if let stickerSearchResult = stickerSearchState.result { var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults? if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { stickerSearchResults = EmojiPagerContentComponent.EmptySearchResults( text: presentationData.strings.EmojiSearch_SearchStickersEmptyResult, iconFile: nil ) } if let stickers = inputData.stickers { let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty(hasResults: true) inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: stickerSearchState.isSearching ? .searching : defaultSearchState) } } else if stickerSearchState.isSearching { if let stickers = inputData.stickers { inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickers.contentItemGroups, itemContentUniqueId: stickers.itemContentUniqueId, emptySearchResults: stickers.emptySearchResults, searchState: .searching) } } var transition: Transition = .immediate var useAnimation = false if let pagerView = strongSelf.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { if centralId == AnyHashable("emoji") { useAnimation = strongSelf.currentInputData.emoji != inputData.emoji } else if centralId == AnyHashable("stickers"), strongSelf.currentInputData.stickers != nil, inputData.stickers != nil { useAnimation = strongSelf.currentInputData.stickers != inputData.stickers } } if useAnimation { let contentAnimation: EmojiPagerContentComponent.ContentAnimation if let scheduledContentAnimationHint = strongSelf.scheduledContentAnimationHint { strongSelf.scheduledContentAnimationHint = nil contentAnimation = scheduledContentAnimationHint } else { contentAnimation = EmojiPagerContentComponent.ContentAnimation(type: .generic) } transition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) } strongSelf.currentInputData = strongSelf.processInputData(inputData: inputData) strongSelf.performLayout(transition: transition) }) self.inputNodeInteraction = ChatMediaInputNodeInteraction( navigateToCollectionId: { _ in }, navigateBackToStickers: { }, setGifMode: { _ in }, openSettings: { }, openTrending: { _ in }, dismissTrendingPacks: { _ in }, toggleSearch: { _, _, _ in }, openPeerSpecificSettings: { }, dismissPeerSpecificSettings: { }, clearRecentlyUsedStickers: { } ) self.trendingGifsPromise.set(.single(nil)) self.trendingGifsPromise.set(paneGifSearchForQuery(context: context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil) |> map { items -> ChatMediaInputGifPaneTrendingState? in if let items = items { return ChatMediaInputGifPaneTrendingState(files: items.files, nextOffset: items.nextOffset) } else { return nil } }) self.gifInputInteraction = GifPagerContentComponent.InputInteraction( performItemAction: { [weak interaction] item, view, rect in guard let interaction else { return } if let (collection, result) = item.contextResult { let _ = interaction.sendBotContextResultAsGif(collection, result, view, rect, false, false) } else { let _ = interaction.sendGif(item.file, view, rect, false, false) } }, openGifContextMenu: { [weak self] item, sourceView, sourceRect, gesture, isSaved in guard let strongSelf = self else { return } strongSelf.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) }, loadMore: { [weak self] token in guard let strongSelf = self, let gifContext = strongSelf.gifContext else { return } gifContext.loadMore(token: token) }, openSearch: { [weak self] in if let strongSelf = self, let pagerView = strongSelf.entityKeyboardView.componentView as? EntityKeyboardComponent.View { pagerView.openSearch() } }, updateSearchQuery: { [weak self] query in guard let self else { return } if let query { self.gifMode = .emojiSearch(query) } else { self.gifMode = .recent } }, hideBackground: currentInputData.gifs?.component.hideBackground ?? false, hasSearch: currentInputData.gifs?.component.inputInteraction.hasSearch ?? false ) self.switchToTextInput = { [weak self] in if let self { self.interaction?.switchToTextInput() } } if self.currentInputData.gifs != nil { let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) |> map { savedGifs -> Bool in return !savedGifs.isEmpty } self.hasRecentGifsDisposable = (hasRecentGifs |> deliverOnMainQueue).start(next: { [weak self] hasRecentGifs in guard let strongSelf = self else { return } if let gifMode = strongSelf.gifMode { if !hasRecentGifs, case .recent = gifMode { strongSelf.gifMode = .trending } } else { strongSelf.gifMode = hasRecentGifs ? .recent : .trending } }) } self.choosingStickerDisposable = (self.choosingSticker |> deliverOnMainQueue).start(next: { [weak self] value in if let self { self.interaction?.updateChoosingSticker(value) } }) } deinit { self.inputDataDisposable?.dispose() self.hasRecentGifsDisposable?.dispose() self.emojiSearchDisposable.dispose() self.stickerSearchDisposable.dispose() self.choosingStickerDisposable?.dispose() } private func reloadGifContext() { if let gifInputInteraction = self.gifInputInteraction, let gifMode = self.gifMode { self.gifContext = GifContext(context: self.context, subject: gifMode, gifInputInteraction: gifInputInteraction, trendingGifs: self.trendingGifsPromise.get()) } } public func markInputCollapsed() { self.isMarkInputCollapsed = true } private func performLayout(transition: Transition) { guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, isVisible, isExpanded) = self.currentState else { return } self.scheduledInnerTransition = transition let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, layoutMetrics: layoutMetrics, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } public func simulateUpdateLayout(isVisible: Bool) { guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, _, isExpanded) = self.currentState else { return } let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, layoutMetrics: layoutMetrics, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } public override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, isVisible, isExpanded) let innerTransition: Transition if let scheduledInnerTransition = self.scheduledInnerTransition { self.scheduledInnerTransition = nil innerTransition = scheduledInnerTransition } else { innerTransition = Transition(transition) } let wasMarkedInputCollapsed = self.isMarkInputCollapsed self.isMarkInputCollapsed = false var expandedHeight = standardInputHeight if self.isEmojiSearchActive && !isExpanded { expandedHeight += 118.0 } var hiddenInputHeight: CGFloat = 0.0 if self.hideInput && !self.adjustLayoutForHiddenInput { hiddenInputHeight = inputPanelHeight } let context = self.context let interaction = self.interaction let inputNodeInteraction = self.inputNodeInteraction! let trendingGifsPromise = self.trendingGifsPromise var mappedTransition = innerTransition if wasMarkedInputCollapsed || !isExpanded { mappedTransition = mappedTransition.withUserData(EntityKeyboardComponent.MarkInputCollapsed()) } var emojiContent: EmojiPagerContentComponent? = self.currentInputData.emoji var stickerContent: EmojiPagerContentComponent? = self.currentInputData.stickers var gifContent: EntityKeyboardGifContent? = self.currentInputData.gifs var stickersEnabled = true var emojiEnabled = true if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel { if let boostsToUnrestrict = interfaceState.boostsToUnrestrict, boostsToUnrestrict > 0 { } else { if peer.hasBannedPermission(.banSendStickers) != nil { stickersEnabled = false } if peer.hasBannedPermission(.banSendText) != nil { emojiEnabled = false } } } else if let peer = interfaceState.renderedPeer?.peer as? TelegramGroup { if peer.hasBannedPermission(.banSendStickers) { stickersEnabled = false } if peer.hasBannedPermission(.banSendText) { emojiEnabled = false } } if !stickersEnabled || interfaceState.interfaceState.editMessage != nil { stickerContent = nil gifContent = nil } if !emojiEnabled && interfaceState.interfaceState.editMessage == nil { emojiContent = nil } stickerContent?.inputInteractionHolder.inputInteraction = self.stickerInputInteraction self.currentInputData.emoji?.inputInteractionHolder.inputInteraction = self.emojiInputInteraction if let stickerInputInteraction = self.stickerInputInteraction { self.scrollingStickersGridPromise.set(stickerInputInteraction.scrollingStickersGridPromise.get()) } let startTime = CFAbsoluteTimeGetCurrent() var keyboardBottomInset = bottomInset if case .regular = layoutMetrics.widthClass, inputHeight > 0.0 && inputHeight < 100.0 { keyboardBottomInset = inputHeight + 15.0 } let entityKeyboardSize = self.entityKeyboardView.update( transition: mappedTransition, component: AnyComponent(EntityKeyboardComponent( theme: interfaceState.theme, strings: interfaceState.strings, isContentInFocus: isVisible, containerInsets: UIEdgeInsets(top: self.isEmojiSearchActive ? -34.0 : 0.0, left: leftInset, bottom: keyboardBottomInset, right: rightInset), topPanelInsets: UIEdgeInsets(), emojiContent: emojiContent, stickerContent: stickerContent, maskContent: nil, gifContent: gifContent?.component, hasRecentGifs: gifContent?.hasRecentGifs ?? false, availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies, defaultToEmojiTab: self.defaultToEmojiTab, externalTopPanelContainer: self.externalTopPanelContainerImpl, externalBottomPanelContainer: nil, displayTopPanelBackground: self.opaqueTopPanelBackground ? .opaque : .none, topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in guard let strongSelf = self else { return } if strongSelf.topBackgroundExtension != topPanelExtension { strongSelf.topBackgroundExtension = topPanelExtension strongSelf.topBackgroundExtensionUpdated?(transition.containedViewLayoutTransition) } }, topPanelScrollingOffset: { _, _ in }, hideInputUpdated: { [weak self] hideInput, adjustLayout, transition in guard let strongSelf = self else { return } if strongSelf.hideInput != hideInput || strongSelf.adjustLayoutForHiddenInput != adjustLayout { strongSelf.hideInput = hideInput strongSelf.adjustLayoutForHiddenInput = adjustLayout strongSelf.hideInputUpdated?(transition.containedViewLayoutTransition) } }, hideTopPanelUpdated: { [weak self] hideTopPanel, transition in guard let strongSelf = self else { return } strongSelf.isEmojiSearchActive = hideTopPanel strongSelf.performLayout(transition: transition) }, switchToTextInput: { [weak self] in self?.switchToTextInput?() }, switchToGifSubject: { [weak self] subject in guard let strongSelf = self else { return } strongSelf.gifMode = subject }, reorderItems: { [weak self] category, items in guard let strongSelf = self else { return } strongSelf.reorderItems(category: category, items: items) }, makeSearchContainerNode: { [weak self, weak interaction] content in guard let self, let interaction else { return nil } let mappedMode: ChatMediaInputSearchMode switch content { case .stickers: mappedMode = .sticker case .gifs: mappedMode = .gif } let searchContainerNode = PaneSearchContainerNode( context: context, theme: interfaceState.theme, strings: interfaceState.strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction, mode: mappedMode, trendingGifsPromise: trendingGifsPromise, cancel: { }, peekBehavior: self.emojiInputInteraction?.peekBehavior ) searchContainerNode.openGifContextMenu = { [weak self] item, sourceNode, sourceRect, gesture, isSaved in guard let self else { return } self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceNode.view, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) } return searchContainerNode }, contentIdUpdated: { _ in }, deviceMetrics: deviceMetrics, hiddenInputHeight: hiddenInputHeight, inputHeight: inputHeight, displayBottomPanel: true, isExpanded: isExpanded && !self.isEmojiSearchActive, clipContentToTopPanel: self.clipContentToTopPanel, useExternalSearchContainer: self.useExternalSearchContainer )), environment: {}, containerSize: CGSize(width: width, height: expandedHeight) ) transition.updateFrame(view: self.entityKeyboardView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: entityKeyboardSize)) let layoutTime = CFAbsoluteTimeGetCurrent() - startTime if layoutTime > 0.1 { #if DEBUG print("EntityKeyboard layout in \(layoutTime * 1000.0) ms") #endif } return (expandedHeight, 0.0) } private func processStableItemGroupList(category: EntityKeyboardComponent.ReorderCategory, itemGroups: [EmojiPagerContentComponent.ItemGroup]) -> [EmojiPagerContentComponent.ItemGroup] { let nextIds: [ItemCollectionId] = itemGroups.compactMap { group -> ItemCollectionId? in if group.isEmbedded { return nil } if group.isFeatured { return nil } if let collectionId = group.groupId.base as? ItemCollectionId { return collectionId } else { return nil } } let stableOrder = self.stableReorderableGroupOrder[category] ?? nextIds var updatedGroups: [EmojiPagerContentComponent.ItemGroup] = [] var staticIsFirst = false let topStaticGroups: [String] = [ "static", "recent", "featuredTop" ] for group in itemGroups { var found = false for topStaticGroup in topStaticGroups { if group.groupId == AnyHashable(topStaticGroup) { if group.groupId == AnyHashable("static") { staticIsFirst = true } found = true break } } if !found { break } } for group in itemGroups { if !(group.groupId.base is ItemCollectionId) { if group.groupId != AnyHashable("static") || staticIsFirst { updatedGroups.append(group) } } else { if group.isEmbedded { continue } if group.isFeatured { continue } if !stableOrder.contains(where: { AnyHashable($0) == group.groupId }) { updatedGroups.append(group) } } } for id in stableOrder { if let group = itemGroups.first(where: { $0.groupId == AnyHashable(id) }) { updatedGroups.append(group) } } for group in itemGroups { if !updatedGroups.contains(where: { $0.groupId == group.groupId }) { updatedGroups.append(group) } } let updatedIds = updatedGroups.compactMap { group -> ItemCollectionId? in if group.isEmbedded { return nil } if group.isFeatured { return nil } if let collectionId = group.groupId.base as? ItemCollectionId { return collectionId } else { return nil } } self.stableReorderableGroupOrder[category] = updatedIds return updatedGroups } private func processInputData(inputData: InputData) -> InputData { return InputData( emoji: inputData.emoji.flatMap { emoji in return emoji.withUpdatedItemGroups(panelItemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: emoji.panelItemGroups), contentItemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: emoji.contentItemGroups), itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults, searchState: emoji.searchState) }, stickers: inputData.stickers.flatMap { stickers in return stickers.withUpdatedItemGroups(panelItemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.panelItemGroups), contentItemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.contentItemGroups), itemContentUniqueId: stickers.itemContentUniqueId, emptySearchResults: nil, searchState: stickers.searchState) }, gifs: inputData.gifs, availableGifSearchEmojies: inputData.availableGifSearchEmojies ) } private func reorderItems(category: EntityKeyboardComponent.ReorderCategory, items: [EntityKeyboardTopPanelComponent.Item]) { var currentIds: [ItemCollectionId] = [] for item in items { guard let id = item.id.base as? ItemCollectionId else { continue } currentIds.append(id) } let namespace: ItemCollectionId.Namespace switch category { case .stickers: namespace = Namespaces.ItemCollection.CloudStickerPacks case .emoji: namespace = Namespaces.ItemCollection.CloudEmojiPacks case .masks: namespace = Namespaces.ItemCollection.CloudMaskPacks } self.stableReorderableGroupOrder.removeValue(forKey: category) let _ = (self.context.engine.stickers.reorderStickerPacks(namespace: namespace, itemIds: currentIds) |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { return } strongSelf.performLayout(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) }) if self.context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.interaction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: presentationData.strings.StickerPacksSettings_DynamicOrderOff, text: presentationData.strings.StickerPacksSettings_DynamicOrderOffInfo, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in return false }), nil) let _ = updateStickerSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { return $0.withUpdatedDynamicPackOrder(false) }).start() } } private func openGifContextMenu(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { let canSaveGif: Bool if file.media.fileId.namespace == Namespaces.Media.CloudFile { canSaveGif = true } else { canSaveGif = false } let _ = (self.context.engine.stickers.isGifSaved(id: file.media.fileId) |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in guard let strongSelf = self else { return } var isGifSaved = isGifSaved if !canSaveGif { isGifSaved = false } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in }, baseNavigationController: nil) gallery.setHintWillBePresentedInPreviewingContext(true) var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Send, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) if let self { if isSaved { let _ = self.interaction?.sendGif(file, sourceView, sourceRect, false, false) } else if let (collection, result) = contextResult { let _ = self.interaction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, false, false) } } }))) if let currentState = strongSelf.currentState { let interfaceState = currentState.interfaceState var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true } if !isScheduledMessages { if case let .peer(peerId) = interfaceState.chatLocation { if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) if let self { if isSaved { let _ = self.interaction?.sendGif(file, sourceView, sourceRect, true, false) } else if let (collection, result) = contextResult { let _ = self.interaction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, true, false) } } }))) } if isSaved { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) if let self { let _ = self.interaction?.sendGif(file, sourceView, sourceRect, false, true) } }))) } } } } if isSaved || isGifSaved { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) if let self { let _ = removeSavedGif(postbox: self.context.account.postbox, mediaId: file.media.fileId).start() } }))) } else if canSaveGif && !isGifSaved { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_SaveGif, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) guard let strongSelf = self else { return } let context = strongSelf.context let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (toggleGifSaved(account: context.account, fileReference: file, saved: true) |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } switch result { case .generic: strongSelf.interaction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { text = presentationData.strings.Premium_MaxSavedGifsFinalText } else { text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string } strongSelf.interaction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in guard let strongSelf = self else { return false } if case .info = action { let controller = PremiumIntroScreen(context: context, source: .savedGifs) strongSelf.interaction?.getNavigationController()?.pushViewController(controller) return true } return false }), nil) } }) }))) } let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.interaction?.presentGlobalOverlayController(contextController, nil) }) } public func scrollToGroupEmoji() { if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View { pagerView.scrollToItemGroup(contentId: "emoji", groupId: "peerSpecific", subgroupId: nil) } } } private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController weak var sourceView: UIView? let sourceRect: CGRect let navigationController: NavigationController? = nil let passthroughTouches: Bool = false init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect) { self.controller = controller self.sourceView = sourceView self.sourceRect = sourceRect } func transitionInfo() -> ContextControllerTakeControllerInfo? { let sourceView = self.sourceView let sourceRect = self.sourceRect return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceView] in if let sourceView = sourceView { return (sourceView, sourceRect) } else { return nil } }) } func animatedIn() { if let controller = self.controller as? GalleryController { controller.viewDidAppear(false) } } } public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputView, LegacyMessageInputPanelInputView, UIInputViewAudioFeedback { private let context: AccountContext public var insertText: ((NSAttributedString) -> Void)? public var deleteBackwards: (() -> Void)? public var switchToKeyboard: (() -> Void)? public var presentController: ((ViewController) -> Void)? private var presentationData: PresentationData private var inputNode: ChatEntityKeyboardInputNode? private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer public init( context: AccountContext, isDark: Bool, areCustomEmojiEnabled: Bool, hideBackground: Bool = false, forceHasPremium: Bool = false ) { self.context = context self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer self.presentationData = context.sharedContext.currentPresentationData.with { $0 } if isDark { self.presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme) } super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default) self.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.clipsToBounds = true let inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] groupId, item, _, _, _, _ in let hasPremium: Signal if forceHasPremium { hasPremium = .single(true) } else { hasPremium = ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue } let _ = hasPremium.start(next: { hasPremium in guard let strongSelf = self else { return } if groupId == AnyHashable("featuredTop") { } else { if let file = item.itemFile { var text = "." var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, displayText, _): text = displayText var packId: ItemCollectionId? if let id = groupId.base as? ItemCollectionId { packId = id } emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: packId, fileId: file.fileId.id, file: file) break loop default: break } } if file.isPremiumEmoji && !hasPremium { let presentationData = context.sharedContext.currentPresentationData.with { $0 } strongSelf.presentController?(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { guard let strongSelf = self else { return } var replaceImpl: ((ViewController) -> Void)? let controller = PremiumDemoScreen(context: strongSelf.context, subject: .animatedEmoji, action: { let controller = PremiumIntroScreen(context: strongSelf.context, source: .animatedEmoji) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in guard let controller else { return } if controller.navigationController != nil { controller.replace(with: c) } else { controller.dismiss() if let self { self.presentController?(c) } } } strongSelf.presentController?(controller) }), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) return } if let emojiAttribute = emojiAttribute { AudioServicesPlaySystemSound(0x450) strongSelf.insertText?(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) } } else if case let .staticEmoji(staticEmoji) = item.content { AudioServicesPlaySystemSound(0x450) strongSelf.insertText?(NSAttributedString(string: staticEmoji, attributes: [:])) } } }) }, deleteBackwards: { [weak self] in guard let strongSelf = self else { return } strongSelf.deleteBackwards?() }, openStickerSettings: { }, openFeatured: { }, openSearch: { }, addGroupAction: { _, _, _ in }, clearGroup: { [weak self] groupId in guard let strongSelf = self else { return } if groupId == AnyHashable("recent") { strongSelf.window?.endEditing(true) let presentationData = context.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: presentationData.strings.Emoji_ClearRecent, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let _ = context.engine.stickers.clearRecentlyUsedEmoji().start() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.presentController?(actionSheet) } }, editAction: { _ in }, pushController: { _ in }, presentController: { _ in }, presentGlobalOverlayController: { _ in }, navigationController: { return nil }, requestUpdate: { _ in }, updateSearchQuery: { _ in }, updateScrollingToItemGroup: { }, onScroll: {}, chatPeerId: nil, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, customContentView: nil, useOpaqueTheme: false, hideBackground: hideBackground, stateContext: nil, addImage: nil ) let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? let _ = EmojiPagerContentComponent.emojiInputData( context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, subject: .generic, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium ).start(next: { value in emojiComponent = value semaphore.signal() }) semaphore.wait() if let emojiComponent = emojiComponent { let inputNode = ChatEntityKeyboardInputNode( context: self.context, currentInputData: ChatEntityKeyboardInputNode.InputData( emoji: emojiComponent, stickers: nil, gifs: nil, availableGifSearchEmojies: [] ), updatedInputData: EmojiPagerContentComponent.emojiInputData( context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, subject: .generic, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium, hideBackground: hideBackground ) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in return ChatEntityKeyboardInputNode.InputData( emoji: emojiComponent, stickers: nil, gifs: nil, availableGifSearchEmojies: [] ) }, defaultToEmojiTab: true, opaqueTopPanelBackground: !hideBackground, interaction: nil, chatPeerId: nil, stateContext: nil ) self.inputNode = inputNode inputNode.clipContentToTopPanel = true inputNode.emojiInputInteraction = inputInteraction inputNode.externalTopPanelContainerImpl = nil inputNode.switchToTextInput = { [weak self] in self?.switchToKeyboard?() } if !hideBackground { inputNode.backgroundColor = self.presentationData.theme.chat.inputMediaPanel.backgroundColor } self.addSubnode(inputNode) } } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func layoutSubviews() { super.layoutSubviews() guard let inputNode = self.inputNode else { return } for view in self.subviews { if view !== inputNode.view { view.isHidden = true } } let bottomInset: CGFloat if #available(iOS 11.0, *) { bottomInset = max(0.0, UIScreen.main.bounds.height - (self.window?.safeAreaLayoutGuide.layoutFrame.maxY ?? 10000.0)) } else { bottomInset = 0.0 } let presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: self.context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil ) let _ = inputNode.updateLayout( width: self.bounds.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: self.bounds.height, inputHeight: self.bounds.height, maximumHeight: self.bounds.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: DeviceMetrics.iPhone12, isVisible: true, isExpanded: false ) inputNode.frame = self.bounds } } public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { public class Interaction { public let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool public let sendEmoji: (TelegramMediaFile) -> Void public let setStatus: (TelegramMediaFile) -> Void public let copyEmoji: (TelegramMediaFile) -> Void public let presentController: (ViewController, Any?) -> Void public let presentGlobalOverlayController: (ViewController, Any?) -> Void public let navigationController: () -> NavigationController? public let updateIsPreviewing: (Bool) -> Void public init(sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool, sendEmoji: @escaping (TelegramMediaFile) -> Void, setStatus: @escaping (TelegramMediaFile) -> Void, copyEmoji: @escaping (TelegramMediaFile) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, updateIsPreviewing: @escaping (Bool) -> Void) { self.sendSticker = sendSticker self.sendEmoji = sendEmoji self.setStatus = setStatus self.copyEmoji = copyEmoji self.presentController = presentController self.presentGlobalOverlayController = presentGlobalOverlayController self.navigationController = navigationController self.updateIsPreviewing = updateIsPreviewing } } private final class ViewRecord { weak var view: UIView? let peekRecognizer: PeekControllerGestureRecognizer init(view: UIView, peekRecognizer: PeekControllerGestureRecognizer) { self.view = view self.peekRecognizer = peekRecognizer } } private let context: AccountContext private let forceTheme: PresentationTheme? private let interaction: Interaction? private let chatPeerId: EnginePeer.Id? private let present: (ViewController, Any?) -> Void private var viewRecords: [ViewRecord] = [] private weak var peekController: PeekController? public init(context: AccountContext, forceTheme: PresentationTheme? = nil, interaction: Interaction?, chatPeerId: EnginePeer.Id?, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.forceTheme = forceTheme self.interaction = interaction self.chatPeerId = chatPeerId self.present = present } public func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, CALayer, TelegramMediaFile)?) { self.viewRecords = self.viewRecords.filter({ $0.view != nil }) let viewRecord = self.viewRecords.first(where: { $0.view === view }) if let viewRecord = viewRecord { viewRecord.peekRecognizer.isEnabled = isEnabled } else { let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self, weak view] point in guard let strongSelf = self else { return nil } guard let (groupId, itemLayer, file) = itemAtPoint(point) else { return nil } let context = strongSelf.context var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] if let id = groupId.base as? ItemCollectionId { if file.isCustomEmoji || context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { bubbleUpEmojiOrStickersets.append(id) } } let accountPeerId = context.account.peerId let chatPeerId = strongSelf.chatPeerId if file.isCustomEmoji { 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 itemLayer] hasPremium -> (UIView, CGRect, PeekControllerContent)? in guard let strongSelf = self, let itemLayer = itemLayer else { return nil } var menuItems: [ContextMenuItem] = [] let presentationData = context.sharedContext.currentPresentationData.with { $0 } var isLocked = false if !hasPremium { isLocked = file.isPremiumEmoji if isLocked && chatPeerId == context.account.peerId { isLocked = false } } if let interaction = strongSelf.interaction { let sendEmoji: (TelegramMediaFile) -> Void = { file in interaction.sendEmoji(file) } let setStatus: (TelegramMediaFile) -> Void = { file in interaction.setStatus(file) } let copyEmoji: (TelegramMediaFile) -> Void = { file in interaction.copyEmoji(file) } if let _ = strongSelf.chatPeerId { 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.interaction?.navigationController()?.pushViewController(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 } guard let view = view else { return nil } return (view, itemLayer.convert(itemLayer.bounds, to: view.layer), StickerPreviewPeekContent(context: context, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked, menu: menuItems, openPremiumIntro: { guard let strongSelf = self, let interaction = strongSelf.interaction else { return } let controller = PremiumIntroScreen(context: context, source: .stickers) interaction.navigationController()?.pushViewController(controller) })) } } else { return combineLatest( context.engine.stickers.isStickerSaved(id: file.fileId), 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 itemLayer] isStarred, hasPremium -> (UIView, CGRect, PeekControllerContent)? in guard let strongSelf = self, let itemLayer = itemLayer else { return nil } var menuItems: [ContextMenuItem] = [] let presentationData = context.sharedContext.currentPresentationData.with { $0 } let isLocked = file.isPremiumSticker && !hasPremium if let interaction = strongSelf.interaction { let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Void = { fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in let _ = interaction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets) } if let chatPeerId = strongSelf.chatPeerId, !isLocked { if chatPeerId != strongSelf.context.account.peerId && chatPeerId.namespace != Namespaces.Peer.SecretChat { menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { sendSticker(.standalone(media: file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { sendSticker(.standalone(media: file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) }))) } menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { let _ = sendSticker(.standalone(media: file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { let _ = sendSticker(.standalone(media: file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) }))) } menuItems.append( .action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : 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: { _, f in f(.default) let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) |> deliverOnMainQueue).start(next: { result in switch result { case .generic: interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { text = presentationData.strings.Premium_MaxFavedStickersFinalText } else { text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { action in if case .info = action { let controller = PremiumIntroScreen(context: context, source: .savedStickers) interaction.navigationController()?.pushViewController(controller) return true } return false }), nil) } }) })) ) loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _): if let packReference = packReference { menuItems.append( .action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) guard let strongSelf = self else { return } let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], isEditing: false, parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) return true }) interaction.navigationController()?.view.window?.endEditing(true) interaction.presentController(controller, nil) })) ) } break loop default: break } } } guard let view = view else { return nil } return (view, itemLayer.convert(itemLayer.bounds, to: view.layer), StickerPreviewPeekContent(context: context, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked && !isStarred, menu: menuItems, openPremiumIntro: { guard let strongSelf = self, let interaction = strongSelf.interaction else { return } let controller = PremiumIntroScreen(context: context, source: .stickers) interaction.navigationController()?.pushViewController(controller) })) } } }, present: { [weak self] content, sourceView, sourceRect in guard let strongSelf = self else { return nil } var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } if let forceTheme = strongSelf.forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } let controller = PeekController(presentationData: presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) controller.visibilityUpdated = { [weak self] visible in guard let strongSelf = self, let interaction = strongSelf.interaction else { return } interaction.updateIsPreviewing(visible) } strongSelf.peekController = controller strongSelf.present(controller, nil) return controller }, updateContent: { [weak self] content in guard let strongSelf = self else { return } let _ = strongSelf }) self.viewRecords.append(ViewRecord(view: view, peekRecognizer: peekRecognizer)) view.addGestureRecognizer(peekRecognizer) peekRecognizer.isEnabled = isEnabled } } }