import Foundation import UIKit import Display import AccountContext import TelegramCore import Postbox import SwiftSignalKit import TelegramPresentationData import ComponentFlow import ViewControllerComponent import AttachmentUI import EntityKeyboard import ChatEntityKeyboardInputNode import ChatPresentationInterfaceState import PagerComponent import FeaturedStickersScreen import TelegramNotices import CounterControllerTitleView import GlassBackgroundComponent import GlassBarButtonComponent import BundleIconComponent final class StickerAttachmentScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let mode: StickerAttachmentScreen.Mode let completion: (AnyMediaReference) -> Void init( context: AccountContext, mode: StickerAttachmentScreen.Mode, completion: @escaping (AnyMediaReference) -> Void ) { self.context = context self.mode = mode self.completion = completion } static func ==(lhs: StickerAttachmentScreenComponent, rhs: StickerAttachmentScreenComponent) -> Bool { return true } final class KeyboardClippingView: UIView { var hitEdgeInsets: UIEdgeInsets = .zero override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { let bounds = self.bounds.inset(by: self.hitEdgeInsets) return bounds.contains(point) } } final class View: UIView, UIScrollViewDelegate { fileprivate let keyboardView: ComponentView private let keyboardClippingView: KeyboardClippingView private let panelBackgroundView: GlassBackgroundView private let panelClippingView: UIView private let panelHostView: PagerExternalTopPanelContainer private let cancelButton: ComponentView private var component: StickerAttachmentScreenComponent? private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? private var interaction: ChatEntityKeyboardInputNode.Interaction? private var inputNodeInteraction: ChatMediaInputNodeInteraction? private var searchVisible = false private var forceUpdate = false private var ignoreNextZeroScrollingOffset = false private var topPanelScrollingOffset: CGFloat = 0.0 private var keyboardContentId: AnyHashable? private let contentDisposable = MetaDisposable() private var emojiContent: EmojiPagerContentComponent? private var stickerContent: EmojiPagerContentComponent? private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? 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)) } } override init(frame: CGRect) { self.keyboardView = ComponentView() self.keyboardClippingView = KeyboardClippingView() self.panelBackgroundView = GlassBackgroundView() self.panelClippingView = UIView() self.panelHostView = PagerExternalTopPanelContainer() self.cancelButton = ComponentView() super.init(frame: frame) self.addSubview(self.keyboardClippingView) self.addSubview(self.panelBackgroundView) self.panelBackgroundView.contentView.addSubview(self.panelClippingView) self.panelClippingView.addSubview(self.panelHostView) self.interaction = ChatEntityKeyboardInputNode.Interaction( sendSticker: { [weak self] file, _, _, _, _, _, _, _, _ in if let self { self.complete(file.abstract) } return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, editGif: { _, _ in }, updateChoosingSticker: { _ in }, switchToTextInput: {}, dismissTextInput: {}, insertText: { _ in }, backwardsDeleteText: {}, openStickerEditor: {}, presentController: { [weak self] c, a in if let self, let controller = self.environment?.controller() { controller.present(c, in: .window(.root), with: a) } }, presentGlobalOverlayController: { [weak self] c, a in if let self, let controller = self.environment?.controller() { controller.presentInGlobalOverlay(c, with: a) } }, getNavigationController: { return nil }, requestLayout: { transition in let _ = transition } ) self.inputNodeInteraction = ChatMediaInputNodeInteraction( navigateToCollectionId: { _ in }, navigateBackToStickers: { }, setGifMode: { _ in }, openSettings: { }, openTrending: { _ in }, dismissTrendingPacks: { _ in }, toggleSearch: { _, _, _ in }, openPeerSpecificSettings: { }, dismissPeerSpecificSettings: { }, clearRecentlyUsedStickers: { } ) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.contentDisposable.dispose() } func complete(_ fileReference: AnyMediaReference) { guard let component = self.component else { return } component.completion(fileReference) (self.environment?.controller() as? StickerAttachmentScreen)?.dismiss(animated: true) } func updateContent(component: StickerAttachmentScreenComponent) { self.emojiContent?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] groupId, item, _, _, _, _ in guard let self, let component = self.component else { return } let context = component.context if groupId == AnyHashable("featuredTop"), let file = item.itemFile { let _ = ( combineLatest( ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: context.account.peerId, premiumIfSavedMessages: true), ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: context.account.peerId, premiumIfSavedMessages: false) ) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] hasPremium, hasGlobalPremium in guard let self else { return } 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 self] emojiPacksView, views in guard let view = views.views[viewKey] as? OrderedItemListView else { return } guard let self else { return } 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.keyboardView.view as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiContent?.inputInteractionHolder.inputInteraction { pagerView.openCustomSearch(content: EmojiSearchContent( context: context, forceTheme: nil, items: stickerPacks, initialFocusId: featuredStickerPack.info.id, hasPremiumForUse: hasPremium, hasPremiumForInstallation: hasGlobalPremium, parentInputInteraction: emojiInputInteraction )) } break } } }) }) } else if let file = item.itemFile?._parse() { self.complete(.standalone(media: file)) } }, deleteBackwards: nil, openStickerSettings: nil, openFeatured: nil, openSearch: { }, addGroupAction: { [weak self] groupId, isPremiumLocked, _ in guard let self, let component = self.component, let collectionId = groupId.base as? ItemCollectionId else { return } let context = component.context 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 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._parse(), items: items) } case .fetching: break case .none: break } return .complete() } |> deliverOnMainQueue).start(completed: { }) break } } }) }, clearGroup: { [weak self] groupId in guard let self, let component = self.component else { return } let context = component.context let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } if groupId == AnyHashable("recent") { 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() }) ])]) context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) } else if groupId == AnyHashable("popular") { let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true)) items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() guard let self else { return } self.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular")) let _ = context.engine.stickers.clearRecentlyUsedReactions().start() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) } }, editAction: { _ in }, pushController: { c in }, presentController: { c in }, presentGlobalOverlayController: { c in }, navigationController: { [weak self] in return self?.environment?.controller()?.navigationController as? NavigationController }, requestUpdate: { [weak self] transition in guard let self else { return } if !transition.animation.isImmediate { self.state?.updated(transition: transition) } }, updateSearchQuery: { [weak self] query in guard let self, let component = self.component else { return } let context = component.context 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 { 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: Signal = .single(true) 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> if hasPremium { remoteSignal = context.engine.stickers.searchEmoji(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) } else { remoteSignal = .single(([], true)) } return remoteSignal |> mapToSignal { foundEmoji -> 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: 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) } if hasPremium { appendUnicodeEmoji() } 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 )]) } } 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 = 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 = 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: { // [weak self] in // if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View { // componentView.scrolledToItemGroup() // } // self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) }, onScroll: {}, chatPeerId: nil, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, customContentView: nil, useOpaqueTheme: true, hideBackground: true, stateContext: nil, addImage: nil ) var stickerPeekBehavior: EmojiContentPeekBehaviorImpl? stickerPeekBehavior = EmojiContentPeekBehaviorImpl( context: component.context, forceTheme: nil, interaction: nil, chatPeerId: nil, present: { [weak self] c, a in self?.environment?.controller()?.presentInGlobalOverlay(c, with: a) } ) self.stickerContent?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] groupId, item, _, _, _, _ in guard let self, let component = self.component, let file = item.itemFile?._parse() else { return } let context = component.context let presentationData = context.sharedContext.currentPresentationData.with { $0 } 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 self] views in guard let self, let controller = self.environment?.controller(), 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 }) { controller.push(FeaturedStickersScreen( context: context, highlightedPackId: featuredStickerPack.info.id, forceTheme: nil, stickerActionTitle: presentationData.strings.StickerPack_AddSticker, sendSticker: { [weak self] fileReference, _, _ in if let self { self.complete(fileReference.abstract) } return true } )) break } } }) } else { let reference: FileMediaReference if groupId == AnyHashable("saved") { reference = .savedSticker(media: file) } else if groupId == AnyHashable("recent") { reference = .recentSticker(media: file) } else { reference = .standalone(media: file) } self.complete(reference.abstract) } }, deleteBackwards: nil, openStickerSettings: nil, openFeatured: nil, openSearch: { [weak self] in guard let self else { return } if let pagerView = self.keyboardView.view as? EntityKeyboardComponent.View { pagerView.openSearch() } }, addGroupAction: { [weak self] groupId, isPremiumLocked, _ in guard let strongSelf = self, let component = strongSelf.component, let collectionId = groupId.base as? ItemCollectionId else { return } let context = component.context 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._parse(), items: items) } case .fetching: break case .none: break } return .complete() } |> deliverOnMainQueue).start(completed: { }) break } } }) }, clearGroup: { [weak self] groupId in guard let strongSelf = self, let component = strongSelf.component else { return } let context = component.context if groupId == AnyHashable("recent") { 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.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() }) ])]) context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) } 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: { _ in }, pushController: { c in }, presentController: { c in }, presentGlobalOverlayController: { c in }, navigationController: { [weak self] in return self?.environment?.controller()?.navigationController as? NavigationController }, requestUpdate: { [weak self] transition in guard let self else { return } if !transition.animation.isImmediate { self.state?.updated(transition: transition) } }, updateSearchQuery: { [weak self] query in guard let self = self, let component = self.component else { return } let context = component.context switch query { case .none: self.stickerSearchDisposable.set(nil) self.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case .text: self.stickerSearchDisposable.set(nil) self.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .category(value): let resultSignal = context.engine.stickers.searchStickers(category: 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: 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 )], files.isFinalResult)) } var version = 0 self.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 = 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 } strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) version += 1 })) } }, updateScrollingToItemGroup: { // if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View { // componentView.scrolledToItemGroup() // } // self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) }, onScroll: {}, chatPeerId: nil, peekBehavior: stickerPeekBehavior, customLayout: nil, externalBackground: nil, externalExpansionView: nil, customContentView: nil, useOpaqueTheme: true, hideBackground: true, stateContext: nil, addImage: nil ) } func update(component: StickerAttachmentScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[EnvironmentType.self].value self.environment = environment self.backgroundColor = environment.theme.list.plainBackgroundColor if self.component == nil { let data = combineLatest( queue: Queue.mainQueue(), self.stickerSearchState.get(), self.emojiSearchState.get() ) self.contentDisposable.set(data.start(next: { [weak self] stickerSearchState, emojiSearchState in guard let self else { return } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } switch component.mode { case var .emoji(emojiContent): 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 ) } let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty(hasResults: true) emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState) } else if emojiSearchState.isSearching { emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiContent.contentItemGroups, itemContentUniqueId: emojiContent.itemContentUniqueId, emptySearchResults: emojiContent.emptySearchResults, searchState: .searching) } self.emojiContent = emojiContent case var .stickers(stickerContent): 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 ) } let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty(hasResults: true) stickerContent = stickerContent.withUpdatedItemGroups(panelItemGroups: stickerContent.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: stickerSearchState.isSearching ? .searching : defaultSearchState) } else if stickerSearchState.isSearching { stickerContent = stickerContent.withUpdatedItemGroups(panelItemGroups: stickerContent.panelItemGroups, contentItemGroups: stickerContent.contentItemGroups, itemContentUniqueId: stickerContent.itemContentUniqueId, emptySearchResults: stickerContent.emptySearchResults, searchState: .searching) } self.stickerContent = stickerContent } self.updateContent(component: component) })) } self.component = component self.state = state let topPanelHeight: CGFloat = 42.0 let topInset: CGFloat = 64.0 //component.topInset let context = component.context let stickerPeekBehavior = EmojiContentPeekBehaviorImpl( context: context, forceTheme: nil, interaction: nil, chatPeerId: nil, present: { c, a in } ) let keyboardSize = self.keyboardView.update( transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), component: AnyComponent(EntityKeyboardComponent( theme: environment.theme, strings: environment.strings, isContentInFocus: true, containerInsets: UIEdgeInsets(top: topPanelHeight + topInset - 11.0, left: 0.0, bottom: 0.0, right: 0.0), topPanelInsets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0), emojiContent: self.emojiContent, stickerContent: self.stickerContent, maskContent: nil, gifContent: nil, hasRecentGifs: false, availableGifSearchEmojies: [], defaultToEmojiTab: self.emojiContent != nil, externalTopPanelContainer: self.panelHostView, externalBottomPanelContainer: nil, externalTintMaskContainer: nil, displayTopPanelBackground: .blur, topPanelExtensionUpdated: { _, _ in }, topPanelScrollingOffset: { [weak self] offset, transition in if let self { if self.ignoreNextZeroScrollingOffset && offset == 0.0 { } else { self.ignoreNextZeroScrollingOffset = false self.topPanelScrollingOffset = offset } } }, hideInputUpdated: { [weak self] _, searchVisible, transition in guard let self else { return } self.forceUpdate = true self.searchVisible = searchVisible self.state?.updated(transition: transition) let transition: ComponentTransition = .easeInOut(duration: 0.2) if let controller = self.environment?.controller() as? StickerAttachmentScreen { if let titleView = controller.navigationItem.titleView { transition.setAlpha(view: titleView, alpha: searchVisible ? 0.0 : 1.0) } if searchVisible { controller.requestAttachmentMenuExpansion() } } }, hideTopPanelUpdated: { _, _ in }, switchToTextInput: {}, switchToGifSubject: { _ in }, reorderItems: { _, _ in }, makeSearchContainerNode: { [weak self] content in guard let self, let interaction = self.interaction, let inputNodeInteraction = self.inputNodeInteraction else { return nil } let mappedMode: ChatMediaInputSearchMode switch content { case .stickers: mappedMode = .sticker case .gifs: mappedMode = .gif } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let searchContainerNode = PaneSearchContainerNode( context: context, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction, mode: mappedMode, batchVideoRenderingContext: nil, stickerActionTitle: presentationData.strings.StickerPack_AddSticker, trendingGifsPromise: Promise(nil), cancel: { }, peekBehavior: stickerPeekBehavior ) return searchContainerNode }, contentIdUpdated: { [weak self] id in guard let self else { return } self.keyboardContentId = id }, deviceMetrics: environment.deviceMetrics, hiddenInputHeight: 0.0, inputHeight: 0.0, displayBottomPanel: false, isExpanded: true, clipContentToTopPanel: false, useExternalSearchContainer: false )), environment: {}, forceUpdate: self.forceUpdate, containerSize: availableSize ) self.forceUpdate = false if let keyboardComponentView = self.keyboardView.view { if keyboardComponentView.superview == nil { self.keyboardClippingView.addSubview(keyboardComponentView) } self.keyboardClippingView.clipsToBounds = false transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + topInset), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight - topInset))) self.keyboardClippingView.hitEdgeInsets = UIEdgeInsets(top: -topPanelHeight - topInset, left: 0.0, bottom: 0.0, right: 0.0) let panelBackgroundFrame = CGRect(origin: CGPoint(x: 12.0, y: topPanelHeight + topInset - 29.0), size: CGSize(width: availableSize.width - 24.0, height: 44.0)) self.panelClippingView.clipsToBounds = true self.panelClippingView.layer.cornerRadius = panelBackgroundFrame.height * 0.5 transition.setFrame(view: self.panelClippingView, frame: CGRect(origin: .zero, size: panelBackgroundFrame.size)) self.panelBackgroundView.update(size: panelBackgroundFrame.size, cornerRadius: panelBackgroundFrame.size.height * 0.5, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, isVisible: !self.searchVisible, transition: transition) transition.setFrame(view: self.panelBackgroundView, frame: panelBackgroundFrame) transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight - topInset), size: keyboardSize)) transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: -12.0, y: 8.0 - UIScreenPixel), size: CGSize(width: keyboardSize.width, height: 0.0))) } let barButtonSize = CGSize(width: 44.0, height: 44.0) let cancelButtonSize = self.cancelButton.update( transition: transition, component: AnyComponent(GlassBarButtonComponent( size: barButtonSize, backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", tintColor: environment.theme.chat.inputPanel.panelControlColor ) )), action: { [weak self] _ in guard let self else { return } (self.environment?.controller() as? StickerAttachmentScreen)?.dismiss(animated: true) } )), environment: {}, containerSize: barButtonSize ) let cancelButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelButtonSize) if let cancelButtonView = self.cancelButton.view { if cancelButtonView.superview == nil { self.addSubview(cancelButtonView) } transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size)) transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center) transition.setAlpha(view: cancelButtonView, alpha: self.searchVisible ? 0.0 : 1.0) transition.setScale(view: cancelButtonView, scale: self.searchVisible ? 0.001 : 1.0) } return availableSize } } func makeView() -> View { return View() } 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) } } final class StickerAttachmentScreen: ViewControllerComponentContainer, AttachmentContainable { enum Mode { case stickers(EmojiPagerContentComponent) case emoji(EmojiPagerContentComponent) } enum Source: Equatable { enum PollMode: Equatable { case description case quizAnswer case option } case poll(PollMode) } private let context: AccountContext private let mode: Mode private let completion: (AnyMediaReference) -> Void init(context: AccountContext, mode: Mode, source: Source, completion: @escaping (AnyMediaReference) -> Void) { self.context = context self.mode = mode self.completion = completion super.init(context: context, component: StickerAttachmentScreenComponent( context: context, mode: mode, completion: completion ), navigationBarAppearance: .transparent, theme: .default) self._hasGlassStyle = true let presentationData = context.sharedContext.currentPresentationData.with { $0 } switch source { case let .poll(pollMode): //TODO:localize let title: String let subtitle: String switch mode { case .stickers: title = "Sticker" switch pollMode { case .description: subtitle = "Add sticker to the poll description" case .quizAnswer: subtitle = "Add sticker to the quiz explanation" case .option: subtitle = "Add sticker to this option" } case .emoji: title = "Emoji" switch pollMode { case .description: subtitle = "Add emoji to the poll description" case .quizAnswer: subtitle = "Add emoji to the quiz explanation" case .option: subtitle = "Add emoji to this option" } } let titleView = CounterControllerTitleView(theme: presentationData.theme, verticalOffset: -2.0) titleView.title = CounterControllerTitle(title: title, counter: subtitle) self.navigationItem.titleView = titleView } self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) } required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public var isMinimized: Bool = false public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } public var parentController: () -> ViewController? = { return nil } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } public var mediaPickerContext: AttachmentMediaPickerContext? public var isPanGestureEnabled: (() -> Bool)? { return { return true // guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { // return true // } // return componentView.isPanGestureEnabled() } } public func isContainerPanningUpdated(_ panning: Bool) { } public func resetForReuse() { } public func prepareForReuse() { } public func requestDismiss(completion: @escaping () -> Void) { completion() } public func shouldDismissImmediately() -> Bool { return true } }