diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index a99310ac37..7f9ab08422 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -235,7 +235,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var emojiContentDisposable: Disposable? private let emojiSearchDisposable = MetaDisposable() - private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) + private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable, emptyResultEmoji: TelegramMediaFile?)?>(nil) + private var stableEmptyResultEmoji: TelegramMediaFile? private var horizontalExpandRecognizer: UIPanGestureRecognizer? private var horizontalExpandStartLocation: CGPoint? @@ -419,7 +420,22 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { var emojiContent = emojiContent if let emojiSearchResult = emojiSearchResult { - emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id) + var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? + if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) { + if strongSelf.stableEmptyResultEmoji == nil { + strongSelf.stableEmptyResultEmoji = emojiSearchResult.emptyResultEmoji + } + //TODO:localize + emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( + text: "No emoji found", + iconFile: strongSelf.stableEmptyResultEmoji + ) + } else { + strongSelf.stableEmptyResultEmoji = nil + } + emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) + } else { + strongSelf.stableEmptyResultEmoji = nil } strongSelf.emojiContent = emojiContent @@ -1315,12 +1331,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { |> distinctUntilChanged let resultSignal = signal - |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + |> mapToSignal { keywords -> Signal<(result: [EmojiPagerContentComponent.ItemGroup], emptyResultEmoji: TelegramMediaFile?), NoError> in return combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + context.engine.stickers.availableReactions(), hasPremium ) - |> map { view, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in + |> take(1) + |> map { view, availableReactions, hasPremium -> (result: [EmojiPagerContentComponent.ItemGroup], emptyResultEmoji: TelegramMediaFile?) in var result: [(String, TelegramMediaFile?, String)] = [] var allEmoticons: [String: String] = [:] @@ -1371,8 +1389,42 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - return [EmojiPagerContentComponent.ItemGroup( - supergroupId: "search", groupId: "search", title: nil, subtitle: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, items: items)] + var emptyResultEmoji: TelegramMediaFile? + if let availableReactions = availableReactions { + //let reactionFilter: [String] = ["😖", "😫", "🫠", "😨", "❓"] + let filteredReactions: [TelegramMediaFile] = availableReactions.reactions.compactMap { reaction -> TelegramMediaFile? in + switch reaction.value { + case let .builtin(value): + let _ = value + //if reactionFilter.contains(value) { + return reaction.selectAnimation + /*} else { + return nil + }*/ + case .custom: + return nil + } + } + emptyResultEmoji = filteredReactions.randomElement() + } + + return (result: [EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + items: items + )], + emptyResultEmoji: emptyResultEmoji + ) } } @@ -1382,7 +1434,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { guard let strongSelf = self else { return } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + strongSelf.emojiSearchResult.set(.single((result.result, AnyHashable(query), result.emptyResultEmoji))) })) } }, diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index d8618291d3..e3b44c471f 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -210,6 +210,7 @@ public final class EmojiStatusSelectionComponent: Component { 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 @@ -250,6 +251,10 @@ public final class EmojiStatusSelectionController: ViewController { private var freezeUpdates: Bool = false private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? + private let emojiSearchDisposable = MetaDisposable() + private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable, emptyResultEmoji: TelegramMediaFile?)?>(nil) + private var stableEmptyResultEmoji: TelegramMediaFile? + private var previewItem: (groupId: AnyHashable, item: EmojiPagerContentComponent.Item)? private var dismissedPreviewItem: (groupId: AnyHashable, item: EmojiPagerContentComponent.Item)? private var previewScreenView: ComponentView? @@ -265,6 +270,8 @@ public final class EmojiStatusSelectionController: ViewController { 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?) { self.controller = controller self.context = context @@ -306,12 +313,36 @@ public final class EmojiStatusSelectionController: ViewController { self.layer.addSublayer(self.cloudLayer0) self.layer.addSublayer(self.cloudLayer1) - self.emojiContentDisposable = (emojiContent - |> deliverOnMainQueue).start(next: { [weak self] emojiContent in + self.emojiContentDisposable = (combineLatest(queue: .mainQueue(), + emojiContent, + self.emojiSearchResult.get() + ) + |> deliverOnMainQueue).start(next: { [weak self] emojiContent, emojiSearchResult in guard let strongSelf = self else { return } strongSelf.controller?._ready.set(.single(true)) + + var emojiContent = emojiContent + if let emojiSearchResult = emojiSearchResult { + var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? + if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) { + if strongSelf.stableEmptyResultEmoji == nil { + strongSelf.stableEmptyResultEmoji = emojiSearchResult.emptyResultEmoji + } + //TODO:localize + emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( + text: "No emoji found", + iconFile: strongSelf.stableEmptyResultEmoji + ) + } else { + strongSelf.stableEmptyResultEmoji = nil + } + emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) + } else { + strongSelf.stableEmptyResultEmoji = nil + } + if strongSelf.emojiContent == nil || !strongSelf.freezeUpdates { strongSelf.emojiContent = emojiContent } @@ -366,7 +397,150 @@ public final class EmojiStatusSelectionController: ViewController { }, requestUpdate: { _ in }, - updateSearchQuery: { _ in + updateSearchQuery: { rawQuery in + guard let strongSelf = self else { + return + } + + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) + + if query.isEmpty { + strongSelf.emojiSearchDisposable.set(nil) + strongSelf.emojiSearchResult.set(.single(nil)) + } else { + let context = strongSelf.context + + let languageCode = "en" + 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<(result: [EmojiPagerContentComponent.ItemGroup], emptyResultEmoji: TelegramMediaFile?), 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 -> (result: [EmojiPagerContentComponent.ItemGroup], emptyResultEmoji: TelegramMediaFile?) in + var result: [(String, TelegramMediaFile?, 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 + } + for attribute in item.file.attributes { + switch attribute { + case let .CustomEmoji(_, alt, _): + 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)) + } + } + default: + break + } + } + } + + 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, + accentTint: false + ) + items.append(item) + } + } + + var emptyResultEmoji: TelegramMediaFile? + if let availableReactions = availableReactions { + //let reactionFilter: [String] = ["😖", "😫", "🫠", "😨", "❓"] + let filteredReactions: [TelegramMediaFile] = availableReactions.reactions.compactMap { reaction -> TelegramMediaFile? in + switch reaction.value { + case let .builtin(value): + let _ = value + //if reactionFilter.contains(value) { + return reaction.selectAnimation + /*} else { + return nil + }*/ + case .custom: + return nil + } + } + emptyResultEmoji = filteredReactions.randomElement() + } + + return (result: [EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + items: items + )], + emptyResultEmoji: emptyResultEmoji + ) + } + } + + strongSelf.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { result in + guard let strongSelf = self else { + return + } + strongSelf.emojiSearchResult.set(.single((result.result, AnyHashable(query), result.emptyResultEmoji))) + })) + } }, chatPeerId: nil, peekBehavior: nil, @@ -398,6 +572,7 @@ public final class EmojiStatusSelectionController: ViewController { self.emojiContentDisposable?.dispose() self.availableReactionsDisposable?.dispose() self.genericReactionEffectDisposable?.dispose() + self.emojiSearchDisposable.dispose() } private func refreshLayout(transition: Transition) { @@ -664,8 +839,14 @@ public final class EmojiStatusSelectionController: ViewController { emojiContent: emojiContent, backgroundColor: listBackgroundColor, separatorColor: separatorColor, - hideTopPanel: false, - hideTopPanelUpdated: { _, _ in } + hideTopPanel: self.isReactionSearchActive, + 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)) @@ -1045,6 +1226,10 @@ public final class EmojiStatusSelectionController: ViewController { return self._ready } + override public var overlayWantsToBeBelowKeyboard: Bool { + return true + } + public init(context: AccountContext, mode: Mode, sourceView: UIView, emojiContent: Signal, currentSelection: Int64?, destinationItemView: @escaping () -> UIView?) { self.context = context self.mode = mode diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 59b25623e3..5298ee819c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -29,14 +29,12 @@ swift_library( "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", "//submodules/SoftwareVideo:SoftwareVideo", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/PhotoResources:PhotoResources", "//submodules/StickerResources:StickerResources", "//submodules/AppBundle:AppBundle", - #"//submodules/ContextUI:ContextUI", - #"//submodules/PremiumUI:PremiumUI", - #"//submodules/StickerPeekUI:StickerPeekUI", "//submodules/UndoUI:UndoUI", "//submodules/Components/MultilineTextComponent:MultilineTextComponent", "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index b8b9937c72..309d8a5664 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -22,6 +22,7 @@ import UndoUI import AudioToolbox import SolidRoundedButtonComponent import EmojiTextAttachmentView +import EmojiStatusComponent private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white) @@ -1512,6 +1513,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { private struct Params: Equatable { var theme: PresentationTheme var strings: PresentationStrings + var text: String var useOpaqueTheme: Bool var isActive: Bool var size: CGSize @@ -1523,6 +1525,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { if lhs.strings !== rhs.strings { return false } + if lhs.text != rhs.text { + return false + } if lhs.useOpaqueTheme != rhs.useOpaqueTheme { return false } @@ -1733,13 +1738,14 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { return } self.params = nil - self.update(theme: params.theme, strings: params.strings, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, transition: transition) + self.update(theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, transition: transition) } - public func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, transition: Transition) { + public func update(theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, transition: Transition) { let params = Params( theme: theme, strings: strings, + text: text, useOpaqueTheme: useOpaqueTheme, isActive: isActive, size: size @@ -1776,11 +1782,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { self.backgroundLayer.cornerRadius = inputHeight * 0.5 self.tintBackgroundLayer.cornerRadius = inputHeight * 0.5 - //TODO:localize let textSize = self.textView.update( transition: .immediate, component: AnyComponent(Text( - text: "Search Reactions", + text: text, font: Font.regular(17.0), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor )), @@ -1790,7 +1795,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { let _ = self.tintTextView.update( transition: .immediate, component: AnyComponent(Text( - text: "Search Reactions", + text: text, font: Font.regular(17.0), color: .white )), @@ -1892,6 +1897,99 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { } } +private final class EmptySearchResultsView: UIView { + override public static var layerClass: AnyClass { + return PassthroughLayer.self + } + + let tintContainerView: UIView + let titleLabel: ComponentView + let titleTintLabel: ComponentView + let icon: ComponentView + + override init(frame: CGRect) { + self.tintContainerView = UIView() + + self.titleLabel = ComponentView() + self.titleTintLabel = ComponentView() + self.icon = ComponentView() + + super.init(frame: frame) + + (self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerView.layer + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(context: AccountContext, theme: PresentationTheme, useOpaqueTheme: Bool, text: String, file: TelegramMediaFile?, size: CGSize, transition: Transition) { + let titleColor: UIColor + if useOpaqueTheme { + titleColor = theme.chat.inputMediaPanel.panelContentControlOpaqueOverlayColor + } else { + titleColor = theme.chat.inputMediaPanel.panelContentControlVibrantOverlayColor + } + + let iconSize: CGSize + if let file = file { + iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + content: .animation(content: .file(file: file), size: CGSize(width: 32.0, height: 32.0), placeholderColor: titleColor, themeColor: nil, loopMode: .forever), + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: 32.0, height: 32.0) + ) + } else { + iconSize = CGSize() + } + + let titleSize = self.titleLabel.update( + transition: .immediate, + component: AnyComponent(Text(text: text, font: Font.regular(15.0), color: titleColor)), + environment: {}, + containerSize: CGSize(width: size.width, height: 100.0) + ) + let _ = self.titleTintLabel.update( + transition: .immediate, + component: AnyComponent(Text(text: text, font: Font.regular(15.0), color: .white)), + environment: {}, + containerSize: CGSize(width: size.width, height: 100.0) + ) + + let spacing: CGFloat = 4.0 + let contentHeight = iconSize.height + spacing + titleSize.height + let contentOriginY = floor((size.height - contentHeight) / 2.0) + let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: iconFrame.maxY + spacing), size: titleSize) + + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: iconFrame) + } + if let titleLabelView = self.titleLabel.view { + if titleLabelView.superview == nil { + self.addSubview(titleLabelView) + } + transition.setFrame(view: titleLabelView, frame: titleFrame) + } + if let titleTintLabelView = self.titleTintLabel.view { + if titleTintLabelView.superview == nil { + self.tintContainerView.addSubview(titleTintLabelView) + } + transition.setFrame(view: titleTintLabelView, frame: titleFrame) + } + } +} + public protocol EmojiContentPeekBehavior: AnyObject { func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, EmojiPagerContentComponent.View.ItemLayer, TelegramMediaFile)?) } @@ -2211,6 +2309,26 @@ public final class EmojiPagerContentComponent: Component { case detailed } + public final class EmptySearchResults: Equatable { + public let text: String + public let iconFile: TelegramMediaFile? + + public init(text: String, iconFile: TelegramMediaFile?) { + self.text = text + self.iconFile = iconFile + } + + public static func ==(lhs: EmptySearchResults, rhs: EmptySearchResults) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.iconFile?.fileId != rhs.iconFile?.fileId { + return false + } + return true + } + } + public let id: AnyHashable public let context: AccountContext public let avatarPeer: EnginePeer? @@ -2221,7 +2339,8 @@ public final class EmojiPagerContentComponent: Component { public let itemLayoutType: ItemLayoutType public let itemContentUniqueId: AnyHashable? public let warpContentsOnEdges: Bool - public let displaySearch: Bool + public let displaySearchWithPlaceholder: String? + public let emptySearchResults: EmptySearchResults? public let enableLongPress: Bool public let selectedItems: Set @@ -2236,7 +2355,8 @@ public final class EmojiPagerContentComponent: Component { itemLayoutType: ItemLayoutType, itemContentUniqueId: AnyHashable?, warpContentsOnEdges: Bool, - displaySearch: Bool, + displaySearchWithPlaceholder: String?, + emptySearchResults: EmptySearchResults?, enableLongPress: Bool, selectedItems: Set ) { @@ -2250,12 +2370,13 @@ public final class EmojiPagerContentComponent: Component { self.itemLayoutType = itemLayoutType self.itemContentUniqueId = itemContentUniqueId self.warpContentsOnEdges = warpContentsOnEdges - self.displaySearch = displaySearch + self.displaySearchWithPlaceholder = displaySearchWithPlaceholder + self.emptySearchResults = emptySearchResults self.enableLongPress = enableLongPress self.selectedItems = selectedItems } - public func withUpdatedItemGroups(itemGroups: [ItemGroup], itemContentUniqueId: AnyHashable?) -> EmojiPagerContentComponent { + public func withUpdatedItemGroups(itemGroups: [ItemGroup], itemContentUniqueId: AnyHashable?, emptySearchResults: EmptySearchResults?) -> EmojiPagerContentComponent { return EmojiPagerContentComponent( id: self.id, context: self.context, @@ -2267,7 +2388,8 @@ public final class EmojiPagerContentComponent: Component { itemLayoutType: self.itemLayoutType, itemContentUniqueId: itemContentUniqueId, warpContentsOnEdges: self.warpContentsOnEdges, - displaySearch: self.displaySearch, + displaySearchWithPlaceholder: self.displaySearchWithPlaceholder, + emptySearchResults: emptySearchResults, enableLongPress: self.enableLongPress, selectedItems: self.selectedItems ) @@ -2304,7 +2426,10 @@ public final class EmojiPagerContentComponent: Component { if lhs.warpContentsOnEdges != rhs.warpContentsOnEdges { return false } - if lhs.displaySearch != rhs.displaySearch { + if lhs.displaySearchWithPlaceholder != rhs.displaySearchWithPlaceholder { + return false + } + if lhs.emptySearchResults != rhs.emptySearchResults { return false } if lhs.enableLongPress != rhs.enableLongPress { @@ -3079,6 +3204,7 @@ public final class EmojiPagerContentComponent: Component { private let placeholdersContainerView: UIView private var visibleSearchHeader: EmojiSearchHeaderView? + private var visibleEmptySearchResultsView: EmptySearchResultsView? private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:] private var visibleItemSelectionLayers: [ItemLayer.Key: ItemSelectionLayer] = [:] private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:] @@ -5703,7 +5829,7 @@ public final class EmojiPagerContentComponent: Component { itemGroups: itemGroups, expandedGroupIds: self.expandedGroupIds, curveNearBounds: component.warpContentsOnEdges, - displaySearch: component.displaySearch, + displaySearch: component.displaySearchWithPlaceholder != nil, isSearchActivated: self.isSearchActivated, customLayout: component.inputInteractionHolder.inputInteraction?.customLayout ) @@ -5730,7 +5856,7 @@ public final class EmojiPagerContentComponent: Component { transition.setPosition(view: self.scrollView, position: CGPoint(x: 0.0, y: scrollOriginY)) let previousSize = self.scrollView.bounds.size var resetScrolling = false - if self.scrollView.bounds.isEmpty && component.displaySearch { + if self.scrollView.bounds.isEmpty && component.displaySearchWithPlaceholder != nil { resetScrolling = true } if previousComponent?.itemContentUniqueId != component.itemContentUniqueId { @@ -5829,7 +5955,7 @@ public final class EmojiPagerContentComponent: Component { } if resetScrolling { - if component.displaySearch && !self.isSearchActivated { + if component.displaySearchWithPlaceholder != nil && !self.isSearchActivated { self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 50.0), size: scrollSize) } else { self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize) @@ -5879,7 +6005,9 @@ public final class EmojiPagerContentComponent: Component { } } - if component.displaySearch { + let useOpaqueTheme = component.inputInteractionHolder.inputInteraction?.useOpaqueTheme ?? false + + if let displaySearchWithPlaceholder = component.displaySearchWithPlaceholder { let visibleSearchHeader: EmojiSearchHeaderView if let current = self.visibleSearchHeader { visibleSearchHeader = current @@ -5924,12 +6052,10 @@ public final class EmojiPagerContentComponent: Component { } } - let useOpaqueTheme = component.inputInteractionHolder.inputInteraction?.useOpaqueTheme ?? false - let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight)) - visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, transition: transition) - transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] _ in - guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else { + visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, transition: transition) + transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in + guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { return } @@ -5946,6 +6072,37 @@ public final class EmojiPagerContentComponent: Component { } } + if let emptySearchResults = component.emptySearchResults { + let visibleEmptySearchResultsView: EmptySearchResultsView + var emptySearchResultsTransition = transition + if let current = self.visibleEmptySearchResultsView { + visibleEmptySearchResultsView = current + } else { + emptySearchResultsTransition = .immediate + visibleEmptySearchResultsView = EmptySearchResultsView(frame: CGRect()) + self.visibleEmptySearchResultsView = visibleEmptySearchResultsView + self.addSubview(visibleEmptySearchResultsView) + self.mirrorContentClippingView?.addSubview(visibleEmptySearchResultsView.tintContainerView) + } + let emptySearchResultsSize = CGSize(width: availableSize.width, height: availableSize.height - itemLayout.searchInsets.top - itemLayout.searchHeight) + visibleEmptySearchResultsView.update( + context: component.context, + theme: keyboardChildEnvironment.theme, + useOpaqueTheme: useOpaqueTheme, + text: emptySearchResults.text, + file: emptySearchResults.iconFile, + size: emptySearchResultsSize, + transition: emptySearchResultsTransition + ) + emptySearchResultsTransition.setFrame(view: visibleEmptySearchResultsView, frame: CGRect(origin: CGPoint(x: 0.0, y: itemLayout.searchInsets.top + itemLayout.searchHeight), size: emptySearchResultsSize)) + } else { + if let visibleEmptySearchResultsView = self.visibleEmptySearchResultsView { + self.visibleEmptySearchResultsView = nil + visibleEmptySearchResultsView.removeFromSuperview() + visibleEmptySearchResultsView.tintContainerView.removeFromSuperview() + } + } + self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: attemptSynchronousLoads, previousItemPositions: previousItemPositions, previousAbsoluteItemPositions: previousAbsoluteItemPositions, updatedItemPositions: updatedItemPositions, hintDisappearingGroupFrame: hintDisappearingGroupFrame) return availableSize @@ -6623,6 +6780,13 @@ public final class EmojiPagerContentComponent: Component { } } + var displaySearchWithPlaceholder: String? + if isReactionSelection { + displaySearchWithPlaceholder = "Search Reactions" + } else if isStatusSelection { + displaySearchWithPlaceholder = "Search Statuses" + } + return EmojiPagerContentComponent( id: "emoji", context: context, @@ -6667,7 +6831,8 @@ public final class EmojiPagerContentComponent: Component { itemLayoutType: .compact, itemContentUniqueId: nil, warpContentsOnEdges: isReactionSelection || isStatusSelection, - displaySearch: isReactionSelection, + displaySearchWithPlaceholder: displaySearchWithPlaceholder, + emptySearchResults: nil, enableLongPress: (isReactionSelection && !isQuickReactionSelection) || isStatusSelection, selectedItems: selectedItems ) diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 7c01f8f8eb..b8d59aea7b 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -537,7 +537,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemLayoutType: .detailed, itemContentUniqueId: nil, warpContentsOnEdges: false, - displaySearch: false, + displaySearchWithPlaceholder: nil, + emptySearchResults: nil, enableLongPress: false, selectedItems: Set() ) @@ -1714,9 +1715,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { private func processInputData(inputData: InputData) -> InputData { return InputData( - emoji: inputData.emoji.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.itemGroups), itemContentUniqueId: nil), + emoji: inputData.emoji.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.itemGroups), itemContentUniqueId: nil, emptySearchResults: nil), stickers: inputData.stickers.flatMap { stickers in - return stickers.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.itemGroups), itemContentUniqueId: nil) + return stickers.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.itemGroups), itemContentUniqueId: nil, emptySearchResults: nil) }, gifs: inputData.gifs, availableGifSearchEmojies: inputData.availableGifSearchEmojies