import Foundation import UIKit import Display import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import AnimationCache import MultiAnimationRenderer import EntityKeyboard import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import PagerComponent import TelegramCore import Lottie import EmojiTextAttachmentView import TextFormat import AppBundle import GZip import EmojiStatusComponent import Postbox import TelegramNotices private func randomGenericReactionEffect(context: AccountContext) -> Signal { return context.engine.stickers.loadedStickerPack(reference: .emojiGenericAnimations, forceActualized: false) |> map { result -> TelegramMediaFile? in switch result { case let .result(_, items, _): return items.randomElement()?.file._parse() default: return nil } } |> take(1) |> mapToSignal { file -> Signal in guard let file else { return .single(nil) } return Signal { subscriber in let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file)).start() let dataDisposable = (context.account.postbox.mediaBox.resourceData(file.resource) |> filter(\.complete) |> take(1)).start(next: { data in subscriber.putNext(data.path) subscriber.putCompletion() }) return ActionDisposable { fetchDisposable.dispose() dataDisposable.dispose() } } } } public final class EmojiStatusSelectionComponent: Component { public typealias EnvironmentType = Empty public let theme: PresentationTheme public let strings: PresentationStrings public let deviceMetrics: DeviceMetrics public let emojiContent: EmojiPagerContentComponent public let backgroundColor: UIColor public let separatorColor: UIColor public let color: UIColor? public let hideTopPanel: Bool public let disableTopPanel: Bool public let hideTopPanelUpdated: (Bool, ComponentTransition) -> Void public init( theme: PresentationTheme, strings: PresentationStrings, deviceMetrics: DeviceMetrics, emojiContent: EmojiPagerContentComponent, color: UIColor?, backgroundColor: UIColor, separatorColor: UIColor, hideTopPanel: Bool, disableTopPanel: Bool, hideTopPanelUpdated: @escaping (Bool, ComponentTransition) -> Void ) { self.theme = theme self.strings = strings self.deviceMetrics = deviceMetrics self.emojiContent = emojiContent self.color = color self.backgroundColor = backgroundColor self.separatorColor = separatorColor self.hideTopPanel = hideTopPanel self.disableTopPanel = disableTopPanel self.hideTopPanelUpdated = hideTopPanelUpdated } public static func ==(lhs: EmojiStatusSelectionComponent, rhs: EmojiStatusSelectionComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings != rhs.strings { return false } if lhs.deviceMetrics != rhs.deviceMetrics { return false } if lhs.emojiContent != rhs.emojiContent { return false } if lhs.color != rhs.color { return false } if lhs.backgroundColor != rhs.backgroundColor { return false } if lhs.separatorColor != rhs.separatorColor { return false } if lhs.hideTopPanel != rhs.hideTopPanel { return false } if lhs.disableTopPanel != rhs.disableTopPanel { return false } return true } public final class View: UIView { private let keyboardView: ComponentView private let keyboardClippingView: UIView private let panelHostView: PagerExternalTopPanelContainer private let panelBackgroundView: BlurredBackgroundView private let panelSeparatorView: UIView private var component: EmojiStatusSelectionComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { self.keyboardView = ComponentView() self.keyboardClippingView = UIView() self.panelHostView = PagerExternalTopPanelContainer() self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.panelSeparatorView = UIView() super.init(frame: frame) self.addSubview(self.keyboardClippingView) self.addSubview(self.panelBackgroundView) self.addSubview(self.panelSeparatorView) self.addSubview(self.panelHostView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } func update(component: EmojiStatusSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundColor = component.backgroundColor let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) self.panelSeparatorView.backgroundColor = component.separatorColor self.component = component self.state = state let topPanelHeight: CGFloat = component.hideTopPanel ? 0.0 : 42.0 var forceUpdate = false if let _ = transition.userData(PagerComponentForceUpdate.self) { forceUpdate = true } let keyboardSize = self.keyboardView.update( transition: transition,//.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), component: AnyComponent(EntityKeyboardComponent( theme: component.theme, strings: component.strings, isContentInFocus: true, containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: 0.0, right: 0.0), topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), emojiContent: component.emojiContent.withCustomTintColor(component.color), stickerContent: nil, maskContent: nil, gifContent: nil, hasRecentGifs: false, availableGifSearchEmojies: [], defaultToEmojiTab: true, externalTopPanelContainer: self.panelHostView, externalBottomPanelContainer: nil, displayTopPanelBackground: .blur, topPanelExtensionUpdated: { _, _ in }, topPanelScrollingOffset: { _, _ in }, hideInputUpdated: { _, _, _ in }, hideTopPanelUpdated: { [weak self] hideTopPanel, transition in guard let strongSelf = self else { return } strongSelf.component?.hideTopPanelUpdated(hideTopPanel, transition) }, switchToTextInput: {}, switchToGifSubject: { _ in }, reorderItems: { _, _ in }, makeSearchContainerNode: { _ in return nil }, contentIdUpdated: { _ in }, deviceMetrics: component.deviceMetrics, hiddenInputHeight: 0.0, inputHeight: 0.0, displayBottomPanel: false, isExpanded: false, clipContentToTopPanel: false, useExternalSearchContainer: false, hidePanels: component.disableTopPanel, customTintColor: component.color )), environment: {}, forceUpdate: forceUpdate, containerSize: availableSize ) if let keyboardComponentView = self.keyboardView.view { if keyboardComponentView.superview == nil { self.keyboardClippingView.addSubview(keyboardComponentView) } if panelBackgroundColor.alpha < 0.01 { self.keyboardClippingView.clipsToBounds = true } else { self.keyboardClippingView.clipsToBounds = false } transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight))) transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight), size: keyboardSize)) transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0))) transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: keyboardSize.width, height: topPanelHeight))) self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: component.hideTopPanel ? -UIScreenPixel : topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel))) transition.setAlpha(view: self.panelSeparatorView, alpha: component.hideTopPanel ? 0.0 : 1.0) } return availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public final class EmojiStatusSelectionController: ViewController { private final class Node: ViewControllerTracingNode { 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 weak var controller: EmojiStatusSelectionController? private let context: AccountContext private weak var sourceView: UIView? private var globalSourceRect: CGRect? private let componentHost: ComponentView private let componentShadowLayer: SimpleLayer private let cloudLayer0: SimpleLayer private let cloudShadowLayer0: SimpleLayer private let cloudLayer1: SimpleLayer private let cloudShadowLayer1: SimpleLayer private var presentationData: PresentationData private var validLayout: ContainerViewLayout? private let currentSelection: Int64? private let color: UIColor? private var emojiContentDisposable: Disposable? private var emojiContent: EmojiPagerContentComponent? private var freezeUpdates: Bool = false private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? 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 var emptyResultEmojis: [TelegramMediaFile] = [] private var stableEmptyResultEmoji: TelegramMediaFile? private let stableEmptyResultEmojiDisposable = MetaDisposable() private var previewItem: (groupId: AnyHashable, item: EmojiPagerContentComponent.Item)? private var dismissedPreviewItem: (groupId: AnyHashable, item: EmojiPagerContentComponent.Item)? private var previewScreenView: ComponentView? private var availableReactions: AvailableReactions? private var availableReactionsDisposable: Disposable? private var genericReactionEffectDisposable: Disposable? private var genericReactionEffect: String? private var hapticFeedback: HapticFeedback? private var isAnimatingOut: Bool = false private var isDismissed: Bool = false private var isReactionSearchActive: Bool = false init(controller: EmojiStatusSelectionController, context: AccountContext, sourceView: UIView?, emojiContent: Signal, currentSelection: Int64?, color: UIColor?) { self.controller = controller self.context = context self.currentSelection = currentSelection self.color = color if let sourceView = sourceView { self.globalSourceRect = sourceView.convert(sourceView.bounds, to: nil) } self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.componentHost = ComponentView() self.componentShadowLayer = SimpleLayer() self.componentShadowLayer.shadowOpacity = 0.12 self.componentShadowLayer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor self.componentShadowLayer.shadowOffset = CGSize(width: 0.0, height: 2.0) self.componentShadowLayer.shadowRadius = 16.0 self.cloudLayer0 = SimpleLayer() self.cloudShadowLayer0 = SimpleLayer() self.cloudShadowLayer0.shadowOpacity = 0.12 self.cloudShadowLayer0.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor self.cloudShadowLayer0.shadowOffset = CGSize(width: 0.0, height: 2.0) self.cloudShadowLayer0.shadowRadius = 16.0 self.cloudLayer1 = SimpleLayer() self.cloudShadowLayer1 = SimpleLayer() self.cloudShadowLayer1.shadowOpacity = 0.12 self.cloudShadowLayer1.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor self.cloudShadowLayer1.shadowOffset = CGSize(width: 0.0, height: 2.0) self.cloudShadowLayer1.shadowRadius = 16.0 super.init() self.layer.addSublayer(self.componentShadowLayer) self.layer.addSublayer(self.cloudShadowLayer0) self.layer.addSublayer(self.cloudShadowLayer1) self.layer.addSublayer(self.cloudLayer0) self.layer.addSublayer(self.cloudLayer1) self.stableEmptyResultEmojiDisposable.set((self.context.engine.data.get( TelegramEngine.EngineData.Item.Collections.FeaturedEmojiPacks() ) |> deliverOnMainQueue).start(next: { [weak self] featuredEmojiPacks in guard let strongSelf = self else { return } var filteredFiles: [TelegramMediaFile] = [] let filterList: [String] = ["😖", "😫", "🫠", "😨", "❓"] for featuredEmojiPack in featuredEmojiPacks { for item in featuredEmojiPack.topItems { if let alt = item.file.customEmojiAlt { if filterList.contains(alt) { filteredFiles.append(item.file._parse()) } } } } strongSelf.emptyResultEmojis = filteredFiles })) self.emojiContentDisposable = (combineLatest(queue: .mainQueue(), emojiContent, self.emojiSearchState.get() ) |> deliverOnMainQueue).start(next: { [weak self] emojiContent, emojiSearchState in guard let strongSelf = self else { return } strongSelf.controller?._ready.set(.single(true)) var emojiContent = emojiContent if let emojiSearchResult = emojiSearchState.result { var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { if strongSelf.stableEmptyResultEmoji == nil { strongSelf.stableEmptyResultEmoji = strongSelf.emptyResultEmojis.randomElement() } emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( text: strongSelf.presentationData.strings.EmojiSearch_SearchStatusesEmptyResult, iconFile: strongSelf.stableEmptyResultEmoji ) } else { strongSelf.stableEmptyResultEmoji = nil } emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active) } else { strongSelf.stableEmptyResultEmoji = nil } if strongSelf.emojiContent == nil || !strongSelf.freezeUpdates { strongSelf.emojiContent = emojiContent } emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { groupId, item, _, _, _, isPreview in guard let strongSelf = self else { return } strongSelf.applyItem(groupId: groupId, item: item, isPreview: isPreview) }, deleteBackwards: { }, openStickerSettings: { }, openFeatured: { }, openSearch: { }, addGroupAction: { groupId, isPremiumLocked, _ in guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else { return } let _ = (strongSelf.context.engine.data.get( TelegramEngine.EngineData.Item.Collections.FeaturedEmojiPacks() ) |> deliverOnMainQueue).start(next: { featuredEmojiPacks in guard let strongSelf = self else { return } for featuredEmojiPack in featuredEmojiPacks { if featuredEmojiPack.info.id == collectionId { if let strongSelf = self { strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId, scrollToGroup: true)) } let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() break } } }) }, clearGroup: { groupId in }, editAction: { _ in }, pushController: { c in }, presentController: { c in }, presentGlobalOverlayController: { c in }, navigationController: { return nil }, requestUpdate: { _ in }, updateSearchQuery: { 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 = signal |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in return combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), context.engine.stickers.availableReactions(), hasPremium ) |> take(1) |> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in var result: [(String, TelegramMediaFile.Accessor?, String)] = [] var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { allEmoticons[emoticon] = keyword.keyword } } for entry in view.entries { guard let item = entry.item as? StickerPackItem else { continue } if let alt = item.file.customEmojiAlt { if !item.file.isPremiumEmoji || hasPremium { if !alt.isEmpty, let keyword = allEmoticons[alt] { result.append((alt, item.file, keyword)) } else if alt == query { result.append((alt, item.file, alt)) } } } } var items: [EmojiPagerContentComponent.Item] = [] var existingIds = Set() for item in result { if let itemFile = item.1 { 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 [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 )] } } 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(category: 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: TelegramMediaFile.Accessor(itemFile)) let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: TelegramMediaFile.Accessor(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.isSearching = true 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.id), version: version, isPreset: true), isSearching: false) return } self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) version += 1 })) } }, updateScrollingToItemGroup: { }, onScroll: {}, chatPeerId: nil, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, customContentView: nil, useOpaqueTheme: true, hideBackground: false, stateContext: nil, addImage: nil ) strongSelf.refreshLayout(transition: .immediate) }) self.availableReactionsDisposable = (context.engine.stickers.availableReactions() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] availableReactions in guard let strongSelf = self else { return } strongSelf.availableReactions = availableReactions }) self.genericReactionEffectDisposable = (randomGenericReactionEffect(context: context) |> deliverOnMainQueue).start(next: { [weak self] path in self?.genericReactionEffect = path }) } deinit { self.emojiContentDisposable?.dispose() self.availableReactionsDisposable?.dispose() self.genericReactionEffectDisposable?.dispose() self.emojiSearchDisposable.dispose() } private func refreshLayout(transition: ComponentTransition) { guard let layout = self.validLayout else { return } self.containerLayoutUpdated(layout: layout, transition: transition) } func animateOut(completion: @escaping () -> Void, fromBackground: Bool) { if self.isAnimatingOut { return } self.isAnimatingOut = true let duration: Double = fromBackground ? 0.1 : 0.25 self.componentShadowLayer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) self.componentHost.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { _ in completion() }) self.cloudLayer0.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) self.cloudShadowLayer0.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) self.cloudLayer1.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) self.cloudShadowLayer1.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) } func animateOutToStatus(item: EmojiPagerContentComponent.Item, sourceLayer: CALayer, customEffectFile: String?, destinationView: UIView, fromBackground: Bool) { self.isUserInteractionEnabled = false destinationView.isHidden = true let hapticFeedback: HapticFeedback if let current = self.hapticFeedback { hapticFeedback = current } else { hapticFeedback = HapticFeedback() self.hapticFeedback = hapticFeedback } hapticFeedback.prepareTap() var itemCompleted = false var contentCompleted = false var effectCompleted = false let completion: () -> Void = { [weak self] in guard let strongSelf = self, itemCompleted, contentCompleted, effectCompleted else { return } strongSelf.controller?.dismissNow() } var effectView: AnimationView? if let customEffectFile = customEffectFile, let data = try? Data(contentsOf: URL(fileURLWithPath: customEffectFile)), let composition = try? Animation.from(data: TGGUnzipData(data, 2 * 1024 * 1024) ?? data) { let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) view.animationSpeed = 1.0 view.backgroundColor = nil view.isOpaque = false effectView = view } else if let itemFile = item.itemFile { var useCleanEffect = false if itemFile.isCustomTemplateEmoji { useCleanEffect = true } for attribute in itemFile._parse().attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute { switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { useCleanEffect = true } default: break } } } var effectData: Data? if useCleanEffect { if let url = getAppBundle().url(forResource: "generic_reaction_avatar_effect", withExtension: "json") { effectData = try? Data(contentsOf: url) } } else if let genericReactionEffect = self.genericReactionEffect, let data = try? Data(contentsOf: URL(fileURLWithPath: genericReactionEffect)) { effectData = TGGUnzipData(data, 5 * 1024 * 1024) ?? data } else { if let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json") { effectData = try? Data(contentsOf: url) } } if let effectData = effectData, let composition = try? Animation.from(data: effectData) { let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) view.animationSpeed = 1.0 view.backgroundColor = nil view.isOpaque = false let animationCache = self.context.animationCache let animationRenderer = self.context.animationRenderer for i in 1 ... 7 { let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "placeholder_\(i)")) for animationLayer in allLayers { let baseItemLayer = InlineStickerItemLayer( context: self.context, userLocation: .other, attemptSynchronousLoad: false, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: itemFile.fileId.id, file: itemFile._parse()), file: item.itemFile?._parse(), cache: animationCache, renderer: animationRenderer, placeholderColor: UIColor(white: 0.0, alpha: 0.0), pointSize: CGSize(width: 32.0, height: 32.0), dynamicColor: self.color ?? self.presentationData.theme.list.itemAccentColor ) if let color = self.color { switch item.tintMode { case .none: break default: baseItemLayer.contentTintColor = color } } else { switch item.tintMode { case let .custom(color): baseItemLayer.contentTintColor = color case .accent: baseItemLayer.contentTintColor = self.presentationData.theme.list.itemAccentColor case .primary: baseItemLayer.contentTintColor = self.presentationData.theme.list.itemPrimaryTextColor case .none: break } } if let sublayers = animationLayer.sublayers { for sublayer in sublayers { sublayer.isHidden = true } } baseItemLayer.isVisibleForAnimations = true baseItemLayer.frame = CGRect(origin: CGPoint(x: -0.0, y: -0.0), size: CGSize(width: 500.0, height: 500.0)) animationLayer.addSublayer(baseItemLayer) } } effectView = view } } if let sourceCopyLayer = sourceLayer.snapshotContentTree() { self.layer.addSublayer(sourceCopyLayer) sourceCopyLayer.frame = sourceLayer.convert(sourceLayer.bounds, to: self.layer) sourceLayer.isHidden = true let previousSourceCopyFrame = sourceCopyLayer.frame let localDestinationFrame = destinationView.convert(destinationView.bounds, to: self.view) let destinationSize = max(localDestinationFrame.width, localDestinationFrame.height) let effectFrame = localDestinationFrame.insetBy(dx: -destinationSize * 2.0, dy: -destinationSize * 2.0) let destinationNormalScale = localDestinationFrame.width / previousSourceCopyFrame.width let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear) sourceCopyLayer.position = localDestinationFrame.center var midPointY: CGFloat = localDestinationFrame.center.y - 30.0 if let layout = self.validLayout { if midPointY < layout.safeInsets.top + 8.0 { midPointY = max(localDestinationFrame.center.y, layout.safeInsets.top + 20.0) } } transition.animatePositionWithKeyframes(layer: sourceCopyLayer, keyframes: generateParabollicMotionKeyframes(from: previousSourceCopyFrame.center, to: localDestinationFrame.center, midPointY: midPointY), completion: { [weak self, weak sourceCopyLayer, weak destinationView] _ in guard let strongSelf = self else { return } itemCompleted = true sourceCopyLayer?.isHidden = true if let destinationView = destinationView { destinationView.isHidden = false destinationView.layer.animateScale(from: 0.3, to: 1.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) } hapticFeedback.tap() if let effectView = effectView { effectView.frame = effectFrame strongSelf.view.addSubview(effectView) effectView.play(completion: { _ in effectCompleted = true completion() }) } else { effectCompleted = true } completion() }) let scaleKeyframes: [CGFloat] = [ 1.0, 1.4, 1.0, destinationNormalScale * 0.3 ] sourceCopyLayer.transform = CATransform3DMakeScale(scaleKeyframes[scaleKeyframes.count - 1], scaleKeyframes[scaleKeyframes.count - 1], 1.0) sourceCopyLayer.animateKeyframes(values: scaleKeyframes.map({ $0 as NSNumber }), duration: 0.2, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.linear.rawValue) } else { itemCompleted = true destinationView.isHidden = false } if let previewScreenView = self.previewScreenView { self.previewItem = nil self.dismissedPreviewItem = nil self.previewScreenView = nil if let previewScreenComponentView = previewScreenView.view as? EmojiStatusPreviewScreenComponent.View { previewScreenComponentView.animateOut(targetLayer: nil, completion: { [weak previewScreenComponentView] in previewScreenComponentView?.removeFromSuperview() }) } else { previewScreenView.view?.removeFromSuperview() } } self.animateOut(completion: { contentCompleted = true completion() }, fromBackground: fromBackground) } func containerLayoutUpdated(layout: ContainerViewLayout, transition: ComponentTransition) { self.validLayout = layout var transition = transition guard let emojiContent = self.emojiContent else { return } let listBackgroundColor: UIColor let separatorColor: UIColor if self.presentationData.theme.overallDarkAppearance { listBackgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor separatorColor = self.presentationData.theme.list.itemBlocksSeparatorColor self.componentShadowLayer.shadowOpacity = 0.32 self.cloudShadowLayer0.shadowOpacity = 0.32 self.cloudShadowLayer1.shadowOpacity = 0.32 } else { listBackgroundColor = self.presentationData.theme.list.plainBackgroundColor separatorColor = self.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5) self.componentShadowLayer.shadowOpacity = 0.12 self.cloudShadowLayer0.shadowOpacity = 0.12 self.cloudShadowLayer1.shadowOpacity = 0.12 } self.cloudLayer0.backgroundColor = listBackgroundColor.cgColor self.cloudLayer1.backgroundColor = listBackgroundColor.cgColor let sideInset: CGFloat = 16.0 if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { self.scheduledEmojiContentAnimationHint = nil let contentAnimation = scheduledEmojiContentAnimationHint transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) } var componentWidth = layout.size.width - sideInset * 2.0 let nativeItemSize: CGFloat = 40.0 let minSpacing: CGFloat = 9.0 let itemsPerRow = Int((componentWidth + minSpacing) / (nativeItemSize + minSpacing)) if itemsPerRow > 8 { componentWidth = min(componentWidth, 480.0) } let componentSize = self.componentHost.update( transition: transition, component: AnyComponent(EmojiStatusSelectionComponent( theme: self.presentationData.theme, strings: self.presentationData.strings, deviceMetrics: layout.deviceMetrics, emojiContent: emojiContent, color: self.color, backgroundColor: listBackgroundColor, separatorColor: separatorColor, hideTopPanel: self.isReactionSearchActive, disableTopPanel: false, hideTopPanelUpdated: { [weak self] hideTopPanel, transition in guard let strongSelf = self else { return } strongSelf.isReactionSearchActive = hideTopPanel strongSelf.refreshLayout(transition: transition) } )), environment: {}, containerSize: CGSize(width: componentWidth, height: min(308.0, layout.size.height)) ) if let componentView = self.componentHost.view { var animateIn = false if componentView.superview == nil { self.view.addSubview(componentView) animateIn = true componentView.clipsToBounds = true componentView.layer.cornerRadius = 24.0 } var sourceOrigin: CGPoint if let sourceView = self.sourceView { let sourceRect = sourceView.convert(sourceView.bounds, to: self.view) sourceOrigin = CGPoint(x: sourceRect.midX, y: sourceRect.maxY) } else if let globalSourceRect = self.globalSourceRect { let sourceRect = self.view.convert(globalSourceRect, from: nil) sourceOrigin = CGPoint(x: sourceRect.midX, y: sourceRect.maxY) } else { sourceOrigin = CGPoint(x: layout.size.width / 2.0, y: floor(layout.size.height / 2.0 - componentSize.height)) } var componentFrame: CGRect let pointsToTop: Bool if sourceOrigin.y + 5.0 + componentSize.height > layout.size.height - layout.insets(options: []).bottom { componentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - componentSize.width) / 2.0), y: sourceOrigin.y - 25.0 - componentSize.height), size: componentSize) pointsToTop = false } else { componentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - componentSize.width) / 2.0), y: sourceOrigin.y + 5.0), size: componentSize) pointsToTop = true } if componentFrame.minY < layout.insets(options: [.statusBar]).top { componentFrame.origin.y = layout.insets(options: [.statusBar]).top } if self.componentShadowLayer.bounds.size != componentFrame.size { let componentShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: componentFrame.size), cornerRadius: 24.0).cgPath self.componentShadowLayer.shadowPath = componentShadowPath } transition.setFrame(layer: self.componentShadowLayer, frame: componentFrame) let cloudOffset0: CGFloat = 30.0 let cloudSize0: CGFloat = 16.0 var cloudFrame0: CGRect if pointsToTop { cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.minY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0)) } else { cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.maxY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0)) } var invertX = false if cloudFrame0.maxX >= layout.size.width - layout.safeInsets.right - 32.0 { cloudFrame0.origin.x = floor(sourceOrigin.x - cloudSize0 - cloudOffset0 + cloudSize0 / 2.0) invertX = true } transition.setFrame(layer: self.cloudLayer0, frame: cloudFrame0) if self.cloudShadowLayer0.bounds.size != cloudFrame0.size { let cloudShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: cloudFrame0.size), cornerRadius: 24.0).cgPath self.cloudShadowLayer0.shadowPath = cloudShadowPath } transition.setFrame(layer: self.cloudShadowLayer0, frame: cloudFrame0) transition.setCornerRadius(layer: self.cloudLayer0, cornerRadius: cloudFrame0.width / 2.0) let cloudOffset1 = CGPoint(x: -9.0, y: -14.0) let cloudSize1: CGFloat = 8.0 var cloudFrame1: CGRect if pointsToTop { cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY + cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1)) } else { cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY - cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1)) } if invertX { cloudFrame1.origin.x = floor(cloudFrame0.midX - cloudSize1 - cloudOffset1.x + cloudSize1 / 2.0) } transition.setFrame(layer: self.cloudLayer1, frame: cloudFrame1) if self.cloudShadowLayer1.bounds.size != cloudFrame1.size { let cloudShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: cloudFrame1.size), cornerRadius: 24.0).cgPath self.cloudShadowLayer1.shadowPath = cloudShadowPath } transition.setFrame(layer: self.cloudShadowLayer1, frame: cloudFrame1) transition.setCornerRadius(layer: self.cloudLayer1, cornerRadius: cloudFrame1.width / 2.0) transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) if animateIn { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self] _ in self?.allowsGroupOpacity = false }) let contentDuration: Double = 0.3 let contentDelay: Double = 0.14 let initialContentFrame = CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: pointsToTop ? componentFrame.minY : (componentFrame.maxY - 24.0 * 2.0)), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)) if let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View { emojiView.animateIn(fromLocation: self.view.convert(initialContentFrame.center, to: emojiView)) } componentView.layer.animatePosition(from: initialContentFrame.center, to: componentFrame.center, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) componentView.layer.animateBounds(from: CGRect(origin: CGPoint(x: -(componentFrame.minX - initialContentFrame.minX), y: -(componentFrame.minY - initialContentFrame.minY)), size: initialContentFrame.size), to: CGRect(origin: CGPoint(), size: componentFrame.size), duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) self.componentShadowLayer.animateFrame(from: CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: pointsToTop ? componentFrame.minY : (componentFrame.maxY - 24.0 * 2.0)), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)), to: componentView.frame, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: contentDelay) self.componentShadowLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: contentDelay) let initialComponentShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: initialContentFrame.size), cornerRadius: 24.0).cgPath self.componentShadowLayer.animate(from: initialComponentShadowPath, to: self.componentShadowLayer.shadowPath!, keyPath: "shadowPath", timingFunction: kCAMediaTimingFunctionSpring, duration: contentDuration, delay: contentDelay) self.cloudLayer0.animateScale(from: 0.01, to: 1.0, duration: 0.4, delay: 0.05, timingFunction: kCAMediaTimingFunctionSpring) self.cloudShadowLayer0.animateScale(from: 0.01, to: 1.0, duration: 0.4, delay: 0.05, timingFunction: kCAMediaTimingFunctionSpring) self.cloudLayer1.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) self.cloudShadowLayer1.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) } } if let previewItem = self.previewItem, let itemFile = previewItem.item.displayFile?._parse() { let previewScreenView: ComponentView var previewScreenTransition = transition if let current = self.previewScreenView { previewScreenView = current } else { previewScreenTransition = ComponentTransition(animation: .none) if let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View, let sourceLayer = emojiView.layerForItem(groupId: previewItem.groupId, item: previewItem.item) { previewScreenTransition = previewScreenTransition.withUserData(EmojiStatusPreviewScreenComponent.TransitionAnimation( transitionType: .animateIn(sourceLayer: sourceLayer) )) } previewScreenView = ComponentView() self.previewScreenView = previewScreenView } let _ = previewScreenView.update( transition: previewScreenTransition, component: AnyComponent(EmojiStatusPreviewScreenComponent( theme: self.presentationData.theme, strings: self.presentationData.strings, bottomInset: layout.insets(options: []).bottom, item: EmojiStatusComponent( context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, content: .animation( content: .file(file: itemFile), size: CGSize(width: 128.0, height: 128.0), placeholderColor: self.presentationData.theme.list.plainBackgroundColor.withMultipliedAlpha(0.1), themeColor: self.presentationData.theme.list.itemAccentColor, loopMode: .forever ), isVisibleForAnimations: true, useSharedAnimation: false, action: nil ), dismiss: { [weak self] result in guard let strongSelf = self else { return } if let result, let previewItem = strongSelf.previewItem { let expirationDate: Int32? = result.timestamp if let itemGift = previewItem.item.itemGift { let _ = (strongSelf.context.engine.accountData.setStarGiftStatus(starGift: itemGift, expirationDate: expirationDate) |> deliverOnMainQueue).start() if let destinationView = strongSelf.controller?.destinationItemView() { strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: nil, destinationView: destinationView, fromBackground: true) } } else { var emojiString: String? if let itemFile = previewItem.item.itemFile { if let alt = itemFile.customEmojiAlt { emojiString = alt } } let context = strongSelf.context let _ = (context.engine.stickers.availableReactions() |> take(1) |> mapToSignal { availableReactions -> Signal in guard let emojiString = emojiString, let availableReactions = availableReactions else { return .single(nil) } for reaction in availableReactions.reactions { if case let .builtin(value) = reaction.value, value == emojiString { if let aroundAnimation = reaction.aroundAnimation?._parse() { return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource) |> take(1) |> map { data -> String? in if data.complete { return data.path } else { return nil } } } else { return .single(nil) } } } return .single(nil) } |> deliverOnMainQueue).start(next: { filePath in guard let strongSelf = self, let previewItem = strongSelf.previewItem, let destinationView = strongSelf.controller?.destinationItemView() else { return } let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile?._parse(), expirationDate: expirationDate) |> deliverOnMainQueue).start() strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: filePath, destinationView: destinationView, fromBackground: true) }) } } else { strongSelf.dismissedPreviewItem = strongSelf.previewItem strongSelf.previewItem = nil strongSelf.refreshLayout(transition: .immediate) } } )), environment: {}, containerSize: layout.size ) if let view = previewScreenView.view { if view.superview == nil { self.view.addSubview(view) } transition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: layout.size)) } } else if let previewScreenView = self.previewScreenView { self.previewScreenView = nil if let previewScreenComponentView = previewScreenView.view as? EmojiStatusPreviewScreenComponent.View { var targetLayer: CALayer? if let previewItem = self.dismissedPreviewItem, let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View, let sourceLayer = emojiView.layerForItem(groupId: previewItem.groupId, item: previewItem.item) { targetLayer = sourceLayer } previewScreenComponentView.animateOut(targetLayer: targetLayer, completion: { [weak previewScreenComponentView] in previewScreenComponentView?.removeFromSuperview() }) } else { previewScreenView.view?.removeFromSuperview() } } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let result = super.hitTest(point, with: event) { if self.isDismissed { return self.view } if result === self.view { self.isDismissed = true self.controller?.dismiss() } return result } return nil } private func applyItem(groupId: AnyHashable, item: EmojiPagerContentComponent.Item?, isPreview: Bool) { guard let controller = self.controller else { return } if isPreview { guard let item = item else { return } self.previewItem = (groupId, item) self.view.endEditing(true) self.refreshLayout(transition: .immediate) } else { self.freezeUpdates = true if case .statusSelection = controller.mode, let item = item, let currentSelection = self.currentSelection, item.itemFile?.fileId.id == currentSelection { let _ = (self.context.engine.accountData.setEmojiStatus(file: nil, expirationDate: nil) |> deliverOnMainQueue).start() controller.dismiss() return } var animateOutToView = false switch controller.mode { case .backgroundSelection, .customStatusSelection, .quickReactionSelection: if let itemFile = item?.itemFile, itemFile.fileId.id != 0 { animateOutToView = true } case .statusSelection: animateOutToView = true } switch controller.mode { case .statusSelection: if let gift = item?.itemGift { animateOutToView = false let _ = (ApplicationSpecificNotice.getStarGiftWearTips(accountManager: self.context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] count in guard let self else { return } if !self.context.isPremium || count < 3, let pushController = controller.pushController { let controller = self.context.sharedContext.makeGiftWearPreviewScreen(context: self.context, gift: gift) pushController(controller) } else { let _ = (self.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: nil) |> deliverOnMainQueue).start() } }) } else { let _ = (self.context.engine.accountData.setEmojiStatus(file: item?.itemFile?._parse(), expirationDate: nil) |> deliverOnMainQueue).start() } case let .backgroundSelection(completion): completion(item?.itemFile?._parse()) case let .customStatusSelection(completion): completion(item?.itemFile?._parse(), nil) case let .quickReactionSelection(completion): if let item = item, let itemFile = item.itemFile { var selectedReaction: MessageReaction.Reaction? if let availableReactions = self.availableReactions { for reaction in availableReactions.reactions { if reaction.selectAnimation.fileId == itemFile.fileId { selectedReaction = reaction.value break } } } if selectedReaction == nil { selectedReaction = .custom(itemFile.fileId.id) } if let selectedReaction = selectedReaction { let _ = context.engine.stickers.updateQuickReaction(reaction: selectedReaction).start() } } completion() } if animateOutToView, item != nil, let destinationView = controller.destinationItemView() { if let snapshotView = destinationView.snapshotView(afterScreenUpdates: false) { snapshotView.frame = destinationView.frame destinationView.superview?.insertSubview(snapshotView, belowSubview: destinationView) snapshotView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) } destinationView.isHidden = true } if animateOutToView, let item = item, let destinationView = controller.destinationItemView() { var emojiString: String? if let itemFile = item.itemFile { if let alt = itemFile.customEmojiAlt { emojiString = alt } } let context = self.context let _ = (context.engine.stickers.availableReactions() |> take(1) |> mapToSignal { availableReactions -> Signal in guard let emojiString = emojiString, let availableReactions = availableReactions else { return .single(nil) } for reaction in availableReactions.reactions { if case let .builtin(value) = reaction.value, value == emojiString { if let aroundAnimation = reaction.aroundAnimation?._parse() { return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource) |> take(1) |> map { data -> String? in if data.complete { return data.path } else { return nil } } } else { return .single(nil) } } } return .single(nil) } |> deliverOnMainQueue).start(next: { [weak self] filePath in guard let strongSelf = self else { return } guard let emojiView = strongSelf.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View, let sourceLayer = emojiView.layerForItem( groupId: groupId, item: item) else { strongSelf.controller?.dismiss() return } strongSelf.animateOutToStatus(item: item, sourceLayer: sourceLayer, customEffectFile: filePath, destinationView: destinationView, fromBackground: false) }) } else { controller.dismiss() } } } } public enum Mode { case statusSelection case backgroundSelection(completion: (TelegramMediaFile?) -> Void) case customStatusSelection(completion: (TelegramMediaFile?, Int32?) -> Void) case quickReactionSelection(completion: () -> Void) } private let context: AccountContext private weak var sourceView: UIView? private let emojiContent: Signal private let currentSelection: Int64? private let color: UIColor? private let mode: Mode private let destinationItemView: () -> UIView? fileprivate let _ready = Promise() override public var ready: Promise { return self._ready } override public var overlayWantsToBeBelowKeyboard: Bool { return true } public var pushController: ((ViewController) -> Void)? public init(context: AccountContext, mode: Mode, sourceView: UIView, emojiContent: Signal, currentSelection: Int64?, color: UIColor? = nil, destinationItemView: @escaping () -> UIView?) { self.context = context self.mode = mode self.sourceView = sourceView self.emojiContent = emojiContent self.currentSelection = currentSelection self.color = color self.destinationItemView = destinationItemView super.init(navigationBarPresentationData: nil) self.lockOrientation = true self.statusBar.statusBarStyle = .Ignore } required public init(coder: NSCoder) { preconditionFailure() } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } private func dismissNow() { self.presentingViewController?.dismiss(animated: false, completion: nil) } override public func dismiss(completion: (() -> Void)? = nil) { (self.displayNode as! Node).animateOut(completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() }, fromBackground: false) } override public func loadDisplayNode() { self.displayNode = Node(controller: self, context: self.context, sourceView: self.sourceView, emojiContent: self.emojiContent, currentSelection: self.currentSelection, color: self.color) super.displayNodeDidLoad() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) } } private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, midPointY: CGFloat) -> [CGPoint] { let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: midPointY) let x1 = sourcePoint.x let y1 = sourcePoint.y let x2 = midPoint.x let y2 = midPoint.y let x3 = targetPosition.x let y3 = targetPosition.y var keyframes: [CGPoint] = [] if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 { for i in 0 ..< 10 { let k = CGFloat(i) / CGFloat(10 - 1) let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k keyframes.append(CGPoint(x: x, y: y)) } } else { let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) for i in 0 ..< 10 { let k = CGFloat(i) / CGFloat(10 - 1) let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k let y = a * x * x + b * x + c keyframes.append(CGPoint(x: x, y: y)) } } return keyframes } extension EmojiPagerContentComponent.Item { var displayFile: TelegramMediaFile.Accessor? { if let file = self.itemFile { return file } else if let gift = self.itemGift { if let itemFile = gift.itemFile { return TelegramMediaFile.Accessor(itemFile) } else { return nil } } else { return nil } } }