import Foundation import UIKit import Display import ComponentFlow import PagerComponent import TelegramPresentationData import TelegramCore import Postbox import BundleIconComponent import AudioToolbox import SwiftSignalKit import LocalizedPeerData public final class EntityKeyboardChildEnvironment: Equatable { public let theme: PresentationTheme public let strings: PresentationStrings public let isContentInFocus: Bool public let getContentActiveItemUpdated: (AnyHashable) -> ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>? public init( theme: PresentationTheme, strings: PresentationStrings, isContentInFocus: Bool, getContentActiveItemUpdated: @escaping (AnyHashable) -> ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>? ) { self.theme = theme self.strings = strings self.isContentInFocus = isContentInFocus self.getContentActiveItemUpdated = getContentActiveItemUpdated } public static func ==(lhs: EntityKeyboardChildEnvironment, rhs: EntityKeyboardChildEnvironment) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } if lhs.isContentInFocus != rhs.isContentInFocus { return false } return true } } public enum EntitySearchContentType { case stickers case gifs } public final class EntityKeyboardComponent: Component { public final class MarkInputCollapsed { public init() { } } public enum ReorderCategory { case stickers case emoji case masks } public enum DisplayTopPanelBackground { case none case blur case opaque } public struct GifSearchEmoji: Equatable { public var emoji: String public var file: TelegramMediaFile public var title: String public init(emoji: String, file: TelegramMediaFile, title: String) { self.emoji = emoji self.file = file self.title = title } public static func ==(lhs: GifSearchEmoji, rhs: GifSearchEmoji) -> Bool { if lhs.emoji != rhs.emoji { return false } if lhs.file.fileId != rhs.file.fileId { return false } if lhs.title != rhs.title { return false } return true } } public let theme: PresentationTheme public let strings: PresentationStrings public let isContentInFocus: Bool public let containerInsets: UIEdgeInsets public let topPanelInsets: UIEdgeInsets public let emojiContent: EmojiPagerContentComponent? public let stickerContent: EmojiPagerContentComponent? public let maskContent: EmojiPagerContentComponent? public let gifContent: GifPagerContentComponent? public let hasRecentGifs: Bool public let availableGifSearchEmojies: [GifSearchEmoji] public let defaultToEmojiTab: Bool public let externalTopPanelContainer: PagerExternalTopPanelContainer? public let externalBottomPanelContainer: PagerExternalTopPanelContainer? public let displayTopPanelBackground: DisplayTopPanelBackground public let topPanelExtensionUpdated: (CGFloat, ComponentTransition) -> Void public let topPanelScrollingOffset: (CGFloat, ComponentTransition) -> Void public let hideInputUpdated: (Bool, Bool, ComponentTransition) -> Void public let hideTopPanelUpdated: (Bool, ComponentTransition) -> Void public let switchToTextInput: () -> Void public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void public let reorderItems: (ReorderCategory, [EntityKeyboardTopPanelComponent.Item]) -> Void public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode? public let contentIdUpdated: (AnyHashable) -> Void public let deviceMetrics: DeviceMetrics public let hiddenInputHeight: CGFloat public let inputHeight: CGFloat public let displayBottomPanel: Bool public let isExpanded: Bool public let clipContentToTopPanel: Bool public let useExternalSearchContainer: Bool public let hidePanels: Bool public let customTintColor: UIColor? public init( theme: PresentationTheme, strings: PresentationStrings, isContentInFocus: Bool, containerInsets: UIEdgeInsets, topPanelInsets: UIEdgeInsets, emojiContent: EmojiPagerContentComponent?, stickerContent: EmojiPagerContentComponent?, maskContent: EmojiPagerContentComponent?, gifContent: GifPagerContentComponent?, hasRecentGifs: Bool, availableGifSearchEmojies: [GifSearchEmoji], defaultToEmojiTab: Bool, externalTopPanelContainer: PagerExternalTopPanelContainer?, externalBottomPanelContainer: PagerExternalTopPanelContainer?, displayTopPanelBackground: DisplayTopPanelBackground, topPanelExtensionUpdated: @escaping (CGFloat, ComponentTransition) -> Void, topPanelScrollingOffset: @escaping (CGFloat, ComponentTransition) -> Void, hideInputUpdated: @escaping (Bool, Bool, ComponentTransition) -> Void, hideTopPanelUpdated: @escaping (Bool, ComponentTransition) -> Void, switchToTextInput: @escaping () -> Void, switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void, reorderItems: @escaping (ReorderCategory, [EntityKeyboardTopPanelComponent.Item]) -> Void, makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode?, contentIdUpdated: @escaping (AnyHashable) -> Void, deviceMetrics: DeviceMetrics, hiddenInputHeight: CGFloat, inputHeight: CGFloat, displayBottomPanel: Bool, isExpanded: Bool, clipContentToTopPanel: Bool, useExternalSearchContainer: Bool, hidePanels: Bool = false, customTintColor: UIColor? = nil ) { self.theme = theme self.strings = strings self.isContentInFocus = isContentInFocus self.containerInsets = containerInsets self.topPanelInsets = topPanelInsets self.emojiContent = emojiContent self.stickerContent = stickerContent self.maskContent = maskContent self.gifContent = gifContent self.hasRecentGifs = hasRecentGifs self.availableGifSearchEmojies = availableGifSearchEmojies self.defaultToEmojiTab = defaultToEmojiTab self.externalTopPanelContainer = externalTopPanelContainer self.externalBottomPanelContainer = externalBottomPanelContainer self.displayTopPanelBackground = displayTopPanelBackground self.topPanelExtensionUpdated = topPanelExtensionUpdated self.topPanelScrollingOffset = topPanelScrollingOffset self.hideInputUpdated = hideInputUpdated self.hideTopPanelUpdated = hideTopPanelUpdated self.switchToTextInput = switchToTextInput self.switchToGifSubject = switchToGifSubject self.reorderItems = reorderItems self.makeSearchContainerNode = makeSearchContainerNode self.contentIdUpdated = contentIdUpdated self.deviceMetrics = deviceMetrics self.hiddenInputHeight = hiddenInputHeight self.inputHeight = inputHeight self.displayBottomPanel = displayBottomPanel self.isExpanded = isExpanded self.clipContentToTopPanel = clipContentToTopPanel self.useExternalSearchContainer = useExternalSearchContainer self.hidePanels = hidePanels self.customTintColor = customTintColor } public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } if lhs.isContentInFocus != rhs.isContentInFocus { return false } if lhs.containerInsets != rhs.containerInsets { return false } if lhs.topPanelInsets != rhs.topPanelInsets { return false } if lhs.emojiContent != rhs.emojiContent { return false } if lhs.stickerContent != rhs.stickerContent { return false } if lhs.maskContent != rhs.maskContent { return false } if lhs.gifContent != rhs.gifContent { return false } if lhs.hasRecentGifs != rhs.hasRecentGifs { return false } if lhs.availableGifSearchEmojies != rhs.availableGifSearchEmojies { return false } if lhs.defaultToEmojiTab != rhs.defaultToEmojiTab { return false } if lhs.externalTopPanelContainer != rhs.externalTopPanelContainer { return false } if lhs.displayTopPanelBackground != rhs.displayTopPanelBackground { return false } if lhs.deviceMetrics != rhs.deviceMetrics { return false } if lhs.hiddenInputHeight != rhs.hiddenInputHeight { return false } if lhs.inputHeight != rhs.inputHeight { return false } if lhs.displayBottomPanel != rhs.displayBottomPanel { return false } if lhs.isExpanded != rhs.isExpanded { return false } if lhs.clipContentToTopPanel != rhs.clipContentToTopPanel { return false } if lhs.useExternalSearchContainer != rhs.useExternalSearchContainer { return false } if lhs.customTintColor != rhs.customTintColor { return false } return true } public final class View: UIView { private let tintContainerView: UIView private let pagerView: ComponentHostView private var component: EntityKeyboardComponent? public private(set) weak var state: EmptyComponentState? private var searchView: ComponentHostView? private var searchComponent: EntitySearchContentComponent? private var topPanelExtension: CGFloat? private var isTopPanelExpanded: Bool = false private var isTopPanelHidden: Bool = false private var topPanelScrollingOffset: CGFloat? public var centralId: AnyHashable? { if let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View { return pagerView.centralId } else { return nil } } override init(frame: CGRect) { self.tintContainerView = UIView() self.pagerView = ComponentHostView() super.init(frame: frame) //self.clipsToBounds = true self.disablesInteractiveTransitionGestureRecognizer = true self.addSubview(self.pagerView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: EntityKeyboardComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.state = state var contents: [AnyComponentWithIdentity<(EntityKeyboardChildEnvironment, PagerComponentChildEnvironment)>] = [] var contentTopPanels: [AnyComponentWithIdentity] = [] var contentIcons: [PagerComponentContentIcon] = [] var contentAccessoryLeftButtons: [AnyComponentWithIdentity] = [] var contentAccessoryRightButtons: [AnyComponentWithIdentity] = [] let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>() let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>() let masksContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>() if transition.userData(MarkInputCollapsed.self) != nil { self.searchComponent = nil } if let maskContent = component.maskContent { var topMaskItems: [EntityKeyboardTopPanelComponent.Item] = [] for itemGroup in maskContent.panelItemGroups { if let id = itemGroup.supergroupId.base as? String { let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ "saved": .saved, "recent": .recent, "premium": .premium, "liked": .liked ] let titleMapping: [String: String] = [ "saved": component.strings.Stickers_Favorites, "recent": component.strings.Stickers_Recent, "premium": component.strings.EmojiInput_PanelTitlePremium, ] if let icon = iconMapping[id], let title = titleMapping[id] { topMaskItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: false, content: AnyComponent(EntityKeyboardIconTopPanelComponent( icon: icon, theme: component.theme, useAccentColor: false, customTintColor: component.customTintColor, title: title, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil) } )) )) } } else { if !itemGroup.items.isEmpty { if let animationData = itemGroup.items[0].animationData { topMaskItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: !itemGroup.isFeatured, content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: maskContent.context, item: itemGroup.headerItem ?? animationData, isFeatured: itemGroup.isFeatured, isPremiumLocked: itemGroup.isPremiumLocked, animationCache: maskContent.animationCache, animationRenderer: maskContent.animationRenderer, theme: component.theme, title: itemGroup.title ?? "", customTintColor: component.customTintColor, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil) } )) )) } } } } contents.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(maskContent))) contentTopPanels.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(EntityKeyboardTopPanelComponent( id: "masks", theme: component.theme, customTintColor: component.customTintColor, items: topMaskItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, defaultActiveItemId: maskContent.panelItemGroups.first?.groupId, activeContentItemIdUpdated: masksContentItemIdUpdated, reorderItems: { [weak self] items in guard let strongSelf = self else { return } strongSelf.reorderPacks(category: .masks, items: items) } )))) contentIcons.append(PagerComponentContentIcon(id: "masks", imageName: "Chat/Input/Media/EntityInputMasksIcon", title: component.strings.EmojiInput_TabMasks)) if let _ = component.maskContent?.inputInteractionHolder.inputInteraction?.openStickerSettings { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputSettingsIcon", tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil )), action: { maskContent.inputInteractionHolder.inputInteraction?.openStickerSettings?() } ).minSize(CGSize(width: 38.0, height: 38.0))))) } } if let gifContent = component.gifContent { contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(gifContent))) contentIcons.append(PagerComponentContentIcon(id: "gifs", imageName: "Chat/Input/Media/EntityInputGifsIcon", title: component.strings.EmojiInput_TabGifs)) if let addImage = component.stickerContent?.inputInteractionHolder.inputInteraction?.addImage { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Media Editor/AddImage", tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil )), action: { addImage() } ).minSize(CGSize(width: 38.0, height: 38.0))))) } } if let stickerContent = component.stickerContent { var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = [] if let _ = stickerContent.inputInteractionHolder.inputInteraction?.openFeatured { topStickerItems.append(EntityKeyboardTopPanelComponent.Item( id: "featuredTop", isReorderable: false, content: AnyComponent(EntityKeyboardIconTopPanelComponent( icon: .featured, theme: component.theme, useAccentColor: false, customTintColor: component.customTintColor, title: component.strings.Stickers_Trending, pressed: { [weak self] in self?.component?.stickerContent?.inputInteractionHolder.inputInteraction?.openFeatured?() } )) )) } for itemGroup in stickerContent.panelItemGroups { if let id = itemGroup.supergroupId.base as? String { if id == "peerSpecific" { if let avatarPeer = stickerContent.avatarPeer { topStickerItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: false, content: AnyComponent(EntityKeyboardAvatarTopPanelComponent( context: stickerContent.context, peer: avatarPeer, theme: component.theme, title: avatarPeer.compactDisplayTitle, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) } )) )) } } else { let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ "saved": .saved, "recent": .recent, "liked": .liked, "premium": .premium ] let titleMapping: [String: String] = [ "saved": component.strings.Stickers_Favorites, "recent": component.strings.Stickers_Recent, "premium": component.strings.EmojiInput_PanelTitlePremium ] if let icon = iconMapping[id], let title = titleMapping[id] { topStickerItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: false, content: AnyComponent(EntityKeyboardIconTopPanelComponent( icon: icon, theme: component.theme, useAccentColor: false, customTintColor: component.customTintColor, title: title, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) } )) )) } } } else { if !itemGroup.items.isEmpty { if let animationData = itemGroup.items[0].animationData { topStickerItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: !itemGroup.isFeatured, content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: stickerContent.context, item: itemGroup.headerItem ?? animationData, isFeatured: itemGroup.isFeatured, isPremiumLocked: itemGroup.isPremiumLocked, animationCache: stickerContent.animationCache, animationRenderer: stickerContent.animationRenderer, theme: component.theme, title: itemGroup.title ?? "", customTintColor: component.customTintColor, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) } )) )) } } } } contents.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(stickerContent))) contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( id: "stickers", theme: component.theme, customTintColor: component.customTintColor, items: topStickerItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, defaultActiveItemId: stickerContent.panelItemGroups.first?.groupId, activeContentItemIdUpdated: stickersContentItemIdUpdated, reorderItems: { [weak self] items in guard let strongSelf = self else { return } strongSelf.reorderPacks(category: .stickers, items: items) } )))) contentIcons.append(PagerComponentContentIcon(id: "stickers", imageName: "Chat/Input/Media/EntityInputStickersIcon", title: component.strings.EmojiInput_TabStickers)) if let _ = component.stickerContent?.inputInteractionHolder.inputInteraction?.openStickerSettings { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputSettingsIcon", tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil )), action: { stickerContent.inputInteractionHolder.inputInteraction?.openStickerSettings?() } ).minSize(CGSize(width: 38.0, height: 38.0))))) } if let addImage = component.stickerContent?.inputInteractionHolder.inputInteraction?.addImage { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Media Editor/AddImage", tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil )), action: { addImage() } ).minSize(CGSize(width: 38.0, height: 38.0))))) } } let deleteBackwards = component.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>() if let emojiContent = component.emojiContent { contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent))) var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] for itemGroup in emojiContent.panelItemGroups { if !itemGroup.items.isEmpty { if let id = itemGroup.groupId.base as? String, id != "peerSpecific" { if id == "recent" || id == "liked" { let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ "recent": .recent, "liked": .liked, ] let titleMapping: [String: String] = [ "recent": component.strings.Stickers_Recent, "liked": "", ] if let icon = iconMapping[id], let title = titleMapping[id] { topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: false, content: AnyComponent(EntityKeyboardIconTopPanelComponent( icon: icon, theme: component.theme, useAccentColor: false, customTintColor: component.customTintColor, title: title, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) } )) )) } } else if id == "static" { topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: false, content: AnyComponent(EntityKeyboardStaticStickersPanelComponent( theme: component.theme, title: component.strings.EmojiInput_PanelTitleEmoji, pressed: { [weak self] subgroupId in guard let strongSelf = self else { return } strongSelf.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: subgroupId.rawValue) } )) )) } } else { if let animationData = itemGroup.items[0].animationData { topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: !itemGroup.isFeatured, content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: emojiContent.context, item: itemGroup.headerItem ?? animationData, isFeatured: itemGroup.isFeatured, isPremiumLocked: itemGroup.isPremiumLocked, animationCache: emojiContent.animationCache, animationRenderer: emojiContent.animationRenderer, theme: component.theme, title: itemGroup.title ?? "", customTintColor: component.customTintColor ?? itemGroup.customTintColor, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) } )) )) } } } } contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( id: "emoji", theme: component.theme, customTintColor: component.customTintColor, items: topEmojiItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, activeContentItemIdUpdated: emojiContentItemIdUpdated, activeContentItemMapping: ["popular": "recent"], reorderItems: { [weak self] items in guard let strongSelf = self else { return } strongSelf.reorderPacks(category: .emoji, items: items) } )))) contentIcons.append(PagerComponentContentIcon(id: "emoji", imageName: "Chat/Input/Media/EntityInputEmojiIcon", title: component.strings.EmojiInput_TabEmoji)) if let _ = deleteBackwards { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputGlobeIcon", tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil )), action: { [weak self] in guard let strongSelf = self, let component = strongSelf.component else { return } component.switchToTextInput() } ).minSize(CGSize(width: 38.0, height: 38.0))))) } else if let addImage = component.emojiContent?.inputInteractionHolder.inputInteraction?.addImage { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Media Editor/AddImage", tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil )), action: { addImage() } ).minSize(CGSize(width: 38.0, height: 38.0))))) } } if let _ = deleteBackwards { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputClearIcon", tintColor: component.theme.chat.inputMediaPanel.panelIconColor, maxSize: nil )), action: { deleteBackwards?() AudioServicesPlaySystemSound(1155) } ).withHoldAction({ _ in deleteBackwards?() AudioServicesPlaySystemSound(1155) }).minSize(CGSize(width: 38.0, height: 38.0))))) } let panelHideBehavior: PagerComponentPanelHideBehavior if self.searchComponent != nil { panelHideBehavior = .hide } else if component.hidePanels { panelHideBehavior = .disable } else if component.isExpanded { panelHideBehavior = .show } else { panelHideBehavior = .hideOnScroll } var forceUpdate = false if let _ = transition.userData(PagerComponentForceUpdate.self) { forceUpdate = true } let isContentInFocus = component.isContentInFocus && self.searchComponent == nil let pagerSize = self.pagerView.update( transition: transition, component: AnyComponent(PagerComponent( isContentInFocus: isContentInFocus, contentInsets: component.containerInsets, contents: contents, contentTopPanels: contentTopPanels, contentIcons: contentIcons, contentAccessoryLeftButtons: contentAccessoryLeftButtons, contentAccessoryRightButtons: contentAccessoryRightButtons, defaultId: component.defaultToEmojiTab ? "emoji" : "stickers", contentBackground: nil, topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent( theme: component.theme, overflowHeight: component.hiddenInputHeight, displayBackground: component.externalTopPanelContainer != nil ? .none : component.displayTopPanelBackground )), externalTopPanelContainer: component.externalTopPanelContainer, bottomPanel: component.displayBottomPanel ? AnyComponent(EntityKeyboardBottomPanelComponent( theme: component.theme, containerInsets: component.containerInsets, deleteBackwards: { [weak self] in self?.component?.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards?() AudioServicesPlaySystemSound(0x451) } )) : nil, externalBottomPanelContainer: component.externalBottomPanelContainer, panelStateUpdated: { [weak self] panelState, transition in guard let strongSelf = self else { return } strongSelf.topPanelExtensionUpdated(height: panelState.topPanelHeight, transition: transition) strongSelf.topPanelScrollingOffset(offset: panelState.scrollingPanelOffsetToTopEdge, transition: transition) }, isTopPanelExpandedUpdated: { [weak self] isExpanded, transition in guard let strongSelf = self else { return } strongSelf.isTopPanelExpandedUpdated(isExpanded: isExpanded, transition: transition) }, isTopPanelHiddenUpdated: { [weak self] isTopPanelHidden, transition in guard let strongSelf = self else { return } strongSelf.isTopPanelHiddenUpdated(isTopPanelHidden: isTopPanelHidden, transition: transition) }, contentIdUpdated: { [weak self] id in guard let strongSelf = self, let component = strongSelf.component else { return } component.contentIdUpdated(id) }, panelHideBehavior: panelHideBehavior, clipContentToTopPanel: component.clipContentToTopPanel, isExpanded: component.isExpanded )), environment: { EntityKeyboardChildEnvironment( theme: component.theme, strings: component.strings, isContentInFocus: isContentInFocus, getContentActiveItemUpdated: { id in if id == AnyHashable("gifs") { return gifsContentItemIdUpdated } else if id == AnyHashable("stickers") { return stickersContentItemIdUpdated } else if id == AnyHashable("emoji") { return emojiContentItemIdUpdated } else if id == AnyHashable("masks") { return masksContentItemIdUpdated } return nil } ) }, forceUpdate: forceUpdate, containerSize: availableSize ) transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize)) let accountContext = component.emojiContent?.context ?? component.stickerContent?.context if let searchComponent = self.searchComponent, let accountContext = accountContext { var animateIn = false let searchView: ComponentHostView var searchViewTransition = transition if let current = self.searchView { searchView = current } else { searchViewTransition = .immediate searchView = ComponentHostView() self.searchView = searchView self.addSubview(searchView) animateIn = true component.topPanelExtensionUpdated(0.0, transition) } let _ = searchView.update( transition: searchViewTransition, component: AnyComponent(searchComponent), environment: { EntitySearchContentEnvironment( context: accountContext, theme: component.theme, deviceMetrics: component.deviceMetrics, inputHeight: component.inputHeight ) }, containerSize: availableSize ) searchViewTransition.setFrame(view: searchView, frame: CGRect(origin: CGPoint(), size: availableSize)) if animateIn { transition.animateAlpha(view: searchView, from: 0.0, to: 1.0) transition.animatePosition(view: searchView, from: CGPoint(x: 0.0, y: self.topPanelExtension ?? 0.0), to: CGPoint(), additive: true, completion: nil) } } else { if let searchView = self.searchView { self.searchView = nil transition.setFrame(view: searchView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.topPanelExtension ?? 0.0), size: availableSize)) transition.setAlpha(view: searchView, alpha: 0.0, completion: { [weak searchView] _ in searchView?.removeFromSuperview() }) if let topPanelExtension = self.topPanelExtension { component.topPanelExtensionUpdated(topPanelExtension, transition) } } } self.component = component return availableSize } private func topPanelExtensionUpdated(height: CGFloat, transition: ComponentTransition) { guard let component = self.component else { return } if self.topPanelExtension != height { self.topPanelExtension = height } if self.searchComponent == nil { component.topPanelExtensionUpdated(height, transition) } } private func topPanelScrollingOffset(offset: CGFloat, transition: ComponentTransition) { guard let component = self.component else { return } if self.topPanelScrollingOffset != offset { self.topPanelScrollingOffset = offset } if self.searchComponent == nil { component.topPanelScrollingOffset(offset, transition) } } private func isTopPanelExpandedUpdated(isExpanded: Bool, transition: ComponentTransition) { if self.isTopPanelExpanded != isExpanded { self.isTopPanelExpanded = isExpanded } guard let component = self.component else { return } component.hideInputUpdated(self.isTopPanelExpanded, false, transition) } private func isTopPanelHiddenUpdated(isTopPanelHidden: Bool, transition: ComponentTransition) { if self.isTopPanelHidden != isTopPanelHidden { self.isTopPanelHidden = isTopPanelHidden } guard let component = self.component else { return } component.hideTopPanelUpdated(self.isTopPanelHidden, transition) } public func scrollToContentId(_ contentId: AnyHashable) { guard let _ = self.component else { return } if let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View { pagerView.navigateToContentId(contentId) } } public func openSearch() { guard let component = self.component else { return } if self.searchComponent != nil { return } if let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View, let centralId = pagerView.centralId { let contentType: EntitySearchContentType if centralId == AnyHashable("gifs") { contentType = .gifs } else { contentType = .stickers } if component.useExternalSearchContainer, let containerNode = component.makeSearchContainerNode(contentType) { let controller = EntitySearchContainerController(containerNode: containerNode) self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller) } else { self.searchComponent = EntitySearchContentComponent( makeContainerNode: { return component.makeSearchContainerNode(contentType) }, dismissSearch: { [weak self] in self?.closeSearch() } ) } component.hideInputUpdated(true, true, ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } public func openCustomSearch(content: EntitySearchContainerNode) { guard let component = self.component else { return } if self.searchComponent != nil { return } if component.useExternalSearchContainer { let controller = EntitySearchContainerController(containerNode: content) self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller) } else { self.searchComponent = EntitySearchContentComponent( makeContainerNode: { return content }, dismissSearch: { [weak self] in self?.closeSearch() } ) } component.hideInputUpdated(true, true, ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } private func closeSearch() { guard let component = self.component else { return } if self.searchComponent == nil { return } self.searchComponent = nil //self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) component.hideInputUpdated(false, false, ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } public func scrollToItemGroup(contentId: String, groupId: AnyHashable, subgroupId: Int32?, animated: Bool = true) { guard let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View else { return } guard let pagerContentView = self.pagerView.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: contentId)) as? EmojiPagerContentComponent.View else { return } if let topPanelView = pagerView.topPanelComponentView as? EntityKeyboardTopContainerPanelComponent.View { topPanelView.internalUpdatePanelsAreCollapsed() } self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.updateScrollingToItemGroup() pagerContentView.scrollToItemGroup(id: groupId, subgroupId: subgroupId, animated: animated) pagerView.collapseTopPanel() } private func reorderPacks(category: ReorderCategory, items: [EntityKeyboardTopPanelComponent.Item]) { self.component?.reorderItems(category, items) } } 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) } }