diff --git a/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift index d93e4d9e29..c9c815ac9b 100644 --- a/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift +++ b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift @@ -1204,7 +1204,7 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { let query = text.trimmingCharacters(in: .whitespacesAndNewlines) if query.isSingleEmoji { - signals = .single([context.engine.stickers.searchStickers(query: text.basicEmoji.0) + signals = .single([context.engine.stickers.searchStickers(query: [text.basicEmoji.0]) |> map { (nil, $0.items) }]) } else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) @@ -1226,7 +1226,7 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { var signals: [Signal<(String?, [FoundStickerItem]), NoError>] = [] let emoticons = keywords.flatMap { $0.emoticons } for emoji in emoticons { - signals.append(context.engine.stickers.searchStickers(query: emoji.basicEmoji.0) + signals.append(context.engine.stickers.searchStickers(query: [emoji.basicEmoji.0]) |> take(1) |> map { (emoji, $0.items) }) } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 8f09bdb242..5f3fdd6a94 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -164,6 +164,23 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } + 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 context: AccountContext private let presentationData: PresentationData private let animationCache: AnimationCache @@ -235,7 +252,9 @@ 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 emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) + private var emojiSearchStateValue: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false) + private var emptyResultEmojis: [TelegramMediaFile] = [] private var stableEmptyResultEmoji: TelegramMediaFile? private let stableEmptyResultEmojiDisposable = MetaDisposable() @@ -440,14 +459,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.emojiContentDisposable = combineLatest(queue: .mainQueue(), getEmojiContent(self.animationCache, self.animationRenderer), - self.emojiSearchResult.get() - ).start(next: { [weak self] emojiContent, emojiSearchResult in + self.emojiSearchState.get() + ).start(next: { [weak self] emojiContent, emojiSearchState in guard let strongSelf = self else { return } var emojiContent = emojiContent - if let emojiSearchResult = emojiSearchResult { + if let emojiSearchResult = emojiSearchState.result { var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) { if strongSelf.stableEmptyResultEmoji == nil { @@ -460,7 +479,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } else { strongSelf.stableEmptyResultEmoji = nil } - emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: 0), emptySearchResults: emptySearchResults, searchState: .active) + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active) } else { strongSelf.stableEmptyResultEmoji = nil } @@ -1340,22 +1359,22 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.requestUpdateOverlayWantsToBeBelowKeyboard(transition.containedViewLayoutTransition) }, updateSearchQuery: { [weak self] query in - guard let strongSelf = self else { + guard let self else { return } switch query { case .none: - strongSelf.emojiSearchDisposable.set(nil) - strongSelf.emojiSearchResult.set(.single(nil)) + 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 { - strongSelf.emojiSearchDisposable.set(nil) - strongSelf.emojiSearchResult.set(.single(nil)) + self.emojiSearchDisposable.set(nil) + self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) } else { - let context = strongSelf.context + let context = self.context var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { @@ -1457,18 +1476,22 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - strongSelf.emojiSearchDisposable.set((resultSignal + var version = 0 + self.emojiSearchStateValue.isSearching = true + self.emojiSearchDisposable.set((resultSignal |> delay(0.15, queue: .mainQueue()) - |> deliverOnMainQueue).start(next: { result in - guard let strongSelf = self else { + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { return } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + version += 1 })) } case let .category(value): - let resultSignal = strongSelf.context.engine.stickers.searchEmoji(emojiString: value) - |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value) + |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] var existingIds = Set() @@ -1488,7 +1511,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { items.append(item) } - return .single([EmojiPagerContentComponent.ItemGroup( + return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, @@ -1502,16 +1525,26 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { displayPremiumBadges: false, headerItem: nil, items: items - )]) + )], isFinalResult)) } - - strongSelf.emojiSearchDisposable.set((resultSignal - |> delay(0.15, queue: .mainQueue()) - |> deliverOnMainQueue).start(next: { result in - guard let strongSelf = self else { + + var version = 0 + self.emojiSearchDisposable.set((resultSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { return } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value)))) + + guard let group = result.items.first else { + return + } + if group.items.isEmpty && !result.isFinalResult { + self.emojiSearchStateValue.isSearching = true + return + } + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + version += 1 })) } }, diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 41d88def06..3f4eb9c84e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -103,6 +103,7 @@ public struct Namespaces { public static let attachMenuBots: Int8 = 23 public static let featuredStickersConfiguration: Int8 = 24 public static let emojiSearchCategories: Int8 = 25 + public static let cachedEmojiQueryResults: Int8 = 26 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift index ba83cc602e..8b3db877da 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift @@ -81,14 +81,15 @@ func _internal_randomGreetingSticker(account: Account) -> Signal Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { +func _internal_searchStickers(account: Account, query: [String], scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { if scope.isEmpty { return .single(([], true)) } var query = query - if query == "\u{2764}" { - query = "\u{2764}\u{FE0F}" + if query == ["\u{2764}"] { + query = ["\u{2764}\u{FE0F}"] } + return account.postbox.transaction { transaction -> ([FoundStickerItem], CachedStickerQueryResult?, Bool, SearchStickersConfiguration) in let isPremium = transaction.getPeer(account.peerId)?.isPremium ?? false @@ -97,15 +98,17 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers) { if let item = entry.contents.get(SavedStickerItem.self) { for representation in item.stringRepresentations { - if representation.hasPrefix(query) { - result.append(FoundStickerItem(file: item.file, stringRepresentations: item.stringRepresentations)) - break + for queryItem in query { + if representation.hasPrefix(queryItem) { + result.append(FoundStickerItem(file: item.file, stringRepresentations: item.stringRepresentations)) + break + } } } } } - let currentItems = Set(result.map { $0.file.fileId }) + var currentItems = Set(result.map { $0.file.fileId }) var recentItems: [TelegramMediaFile] = [] var recentAnimatedItems: [TelegramMediaFile] = [] var recentItemsIds = Set() @@ -119,9 +122,14 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic } if !currentItems.contains(file.fileId) { + currentItems.insert(file.fileId) + for case let .Sticker(displayText, _, _) in file.attributes { - if displayText.hasPrefix(query) { - matchingRecentItemsIds.insert(file.fileId) + for queryItem in query { + if displayText.hasPrefix(queryItem) { + matchingRecentItemsIds.insert(file.fileId) + break + } } recentItemsIds.insert(file.fileId) if file.isAnimatedSticker || file.isVideoSticker { @@ -135,36 +143,42 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic } } - var searchQuery: ItemCollectionSearchQuery = .exact(ValueBoxKey(query)) - if query == "\u{2764}" { - searchQuery = .any([ValueBoxKey("\u{2764}"), ValueBoxKey("\u{2764}\u{FE0F}")]) + var searchQueries: [ItemCollectionSearchQuery] = query.map { queryItem -> ItemCollectionSearchQuery in + return .exact(ValueBoxKey(queryItem)) + } + if query == ["\u{2764}"] { + searchQueries = [.any([ValueBoxKey("\u{2764}"), ValueBoxKey("\u{2764}\u{FE0F}")])] } var installedItems: [FoundStickerItem] = [] var installedAnimatedItems: [FoundStickerItem] = [] var installedPremiumItems: [FoundStickerItem] = [] - for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudStickerPacks, query: searchQuery) { - if let item = item as? StickerPackItem { - if !currentItems.contains(item.file.fileId) { - var stringRepresentations: [String] = [] - for key in item.indexKeys { - key.withDataNoCopy { data in - if let string = String(data: data, encoding: .utf8) { - stringRepresentations.append(string) + for searchQuery in searchQueries { + for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudStickerPacks, query: searchQuery) { + if let item = item as? StickerPackItem { + if !currentItems.contains(item.file.fileId) { + currentItems.insert(item.file.fileId) + + var stringRepresentations: [String] = [] + for key in item.indexKeys { + key.withDataNoCopy { data in + if let string = String(data: data, encoding: .utf8) { + stringRepresentations.append(string) + } } } - } - if !recentItemsIds.contains(item.file.fileId) { - if item.file.isPremiumSticker { - installedPremiumItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations)) - } else if item.file.isAnimatedSticker || item.file.isVideoSticker { - installedAnimatedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations)) + if !recentItemsIds.contains(item.file.fileId) { + if item.file.isPremiumSticker { + installedPremiumItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations)) + } else if item.file.isAnimatedSticker || item.file.isVideoSticker { + installedAnimatedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations)) + } else { + installedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations)) + } } else { - installedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations)) + matchingRecentItemsIds.insert(item.file.fileId) } - } else { - matchingRecentItemsIds.insert(item.file.fileId) } } } @@ -175,7 +189,7 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic continue } if matchingRecentItemsIds.contains(file.fileId) { - result.append(FoundStickerItem(file: file, stringRepresentations: [query])) + result.append(FoundStickerItem(file: file, stringRepresentations: query)) } } @@ -184,7 +198,7 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic continue } if matchingRecentItemsIds.contains(file.fileId) { - result.append(FoundStickerItem(file: file, stringRepresentations: [query])) + result.append(FoundStickerItem(file: file, stringRepresentations: query)) } } @@ -192,7 +206,8 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic result.append(contentsOf: installedItems) } - var cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query)))?.get(CachedStickerQueryResult.self) + let combinedQuery = query.joined(separator: "") + var cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(combinedQuery)))?.get(CachedStickerQueryResult.self) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue @@ -278,7 +293,7 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic } } - let remote = account.network.request(Api.functions.messages.getStickers(emoticon: query, hash: cached?.hash ?? 0)) + let remote = account.network.request(Api.functions.messages.getStickers(emoticon: query.joined(separator: ""), hash: cached?.hash ?? 0)) |> `catch` { _ -> Signal in return .single(.stickersNotModified) } @@ -356,7 +371,7 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) if let entry = CodableEntry(CachedStickerQueryResult(items: files, hash: hash, timestamp: currentTime)) { - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query)), entry: entry) + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query.joined(separator: ""))), entry: entry) } return (result, true) @@ -371,6 +386,191 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic } } +func _internal_searchEmoji(account: Account, query: [String], scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { + if scope.isEmpty { + return .single(([], true)) + } + var query = query + if query == ["\u{2764}"] { + query = ["\u{2764}\u{FE0F}"] + } + + return account.postbox.transaction { transaction -> ([FoundStickerItem], CachedStickerQueryResult?, Bool, SearchStickersConfiguration) in + let isPremium = transaction.getPeer(account.peerId)?.isPremium ?? false + + var result: [FoundStickerItem] = [] + if scope.contains(.installed) { + var currentItems = Set(result.map { $0.file.fileId }) + var recentItems: [TelegramMediaFile] = [] + var recentAnimatedItems: [TelegramMediaFile] = [] + var recentItemsIds = Set() + var matchingRecentItemsIds = Set() + + for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.LocalRecentEmoji) { + if let item = entry.contents.get(RecentEmojiItem.self), case let .file(file) = item.content { + if !currentItems.contains(file.fileId) { + currentItems.insert(file.fileId) + + for case let .Sticker(displayText, _, _) in file.attributes { + for queryItem in query { + if displayText.hasPrefix(queryItem) { + matchingRecentItemsIds.insert(file.fileId) + break + } + } + recentItemsIds.insert(file.fileId) + if file.isAnimatedSticker || file.isVideoSticker { + recentAnimatedItems.append(file) + } else { + recentItems.append(file) + } + break + } + } + } + } + + var searchQueries: [ItemCollectionSearchQuery] = query.map { queryItem -> ItemCollectionSearchQuery in + return .exact(ValueBoxKey(queryItem)) + } + if query == ["\u{2764}"] { + searchQueries = [.any([ValueBoxKey("\u{2764}"), ValueBoxKey("\u{2764}\u{FE0F}")])] + } + + var installedItems: [FoundStickerItem] = [] + + for searchQuery in searchQueries { + for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudEmojiPacks, query: searchQuery) { + if let item = item as? StickerPackItem { + if !currentItems.contains(item.file.fileId) { + currentItems.insert(item.file.fileId) + + var stringRepresentations: [String] = [] + for key in item.indexKeys { + key.withDataNoCopy { data in + if let string = String(data: data, encoding: .utf8) { + stringRepresentations.append(string) + } + } + } + if !recentItemsIds.contains(item.file.fileId) { + installedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations)) + } else { + matchingRecentItemsIds.insert(item.file.fileId) + } + } + } + } + } + + result.append(contentsOf: installedItems) + } + + let combinedQuery = query.joined(separator: "") + var cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedEmojiQueryResults, key: CachedStickerQueryResult.cacheKey(combinedQuery)))?.get(CachedStickerQueryResult.self) + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let searchStickersConfiguration = SearchStickersConfiguration.with(appConfiguration: appConfiguration) + + if let currentCached = cached, currentTime > currentCached.timestamp + searchStickersConfiguration.cacheTimeout { + cached = nil + } + + return (result, cached, isPremium, searchStickersConfiguration) + } + |> mapToSignal { localItems, cached, isPremium, searchStickersConfiguration -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> in + if !scope.contains(.remote) { + return .single((localItems, true)) + } + + var tempResult: [FoundStickerItem] = [] + let currentItemIds = Set(localItems.map { $0.file.fileId }) + + var otherItems: [FoundStickerItem] = [] + + for item in localItems { + otherItems.append(item) + } + + if let cached = cached { + var cachedItems: [FoundStickerItem] = [] + + for file in cached.items { + if !currentItemIds.contains(file.fileId) { + cachedItems.append(FoundStickerItem(file: file, stringRepresentations: [])) + } + } + + otherItems.append(contentsOf: cachedItems) + + let allOtherItems = otherItems + + tempResult.append(contentsOf: allOtherItems) + } + + let remote = account.network.request(Api.functions.messages.searchCustomEmoji(emoticon: query.joined(separator: ""), hash: cached?.hash ?? 0)) + |> `catch` { _ -> Signal in + return .single(.emojiListNotModified) + } + |> mapToSignal { result -> Signal<(files: [TelegramMediaFile], hash: Int64)?, NoError> in + switch result { + case .emojiListNotModified: + return .single(nil) + case let .emojiList(hash, documentIds): + return TelegramEngine(account: account).stickers.resolveInlineStickers(fileIds: documentIds) + |> map { fileMap -> (files: [TelegramMediaFile], hash: Int64)? in + var files: [TelegramMediaFile] = [] + for documentId in documentIds { + if let file = fileMap[documentId] { + files.append(file) + } + } + return (files, hash) + } + } + } + |> mapToSignal { result -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> in + return account.postbox.transaction { transaction -> (items: [FoundStickerItem], isFinalResult: Bool) in + if let (fileItems, hash) = result { + var result: [FoundStickerItem] = [] + let currentItemIds = Set(localItems.map { $0.file.fileId }) + + var otherItems: [FoundStickerItem] = [] + + for item in localItems { + otherItems.append(item) + } + + var foundItems: [FoundStickerItem] = [] + + var files: [TelegramMediaFile] = [] + for file in fileItems { + files.append(file) + if !currentItemIds.contains(file.fileId) { + foundItems.append(FoundStickerItem(file: file, stringRepresentations: [])) + } + } + + let allOtherItems = otherItems + + result.append(contentsOf: allOtherItems) + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if let entry = CodableEntry(CachedStickerQueryResult(items: files, hash: hash, timestamp: currentTime)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedEmojiQueryResults, key: CachedStickerQueryResult.cacheKey(query.joined(separator: ""))), entry: entry) + } + + return (result, true) + } + return (tempResult, true) + } + } + return .single((tempResult, false)) + |> then(remote) + } +} + public struct FoundStickerSets { public var infos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)] public let entries: [ItemCollectionViewEntry] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index 2b4a9a136a..2f9e59726d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -30,7 +30,7 @@ public extension TelegramEngine { return _internal_randomGreetingSticker(account: self.account) } - public func searchStickers(query: String, scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { + public func searchStickers(query: [String], scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { return _internal_searchStickers(account: self.account, query: query, scope: scope) } @@ -184,8 +184,13 @@ public extension TelegramEngine { return _internal_resolveInlineStickers(postbox: self.account.postbox, network: self.account.network, fileIds: fileIds) } - public func searchEmoji(emojiString: String) -> Signal<[TelegramMediaFile], NoError> { - return self.account.network.request(Api.functions.messages.searchCustomEmoji(emoticon: emojiString, hash: 0)) + public func searchEmoji(emojiString: [String]) -> Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> { + return _internal_searchEmoji(account: self.account, query: emojiString) + |> map { items, isFinalResult -> (items: [TelegramMediaFile], isFinalResult: Bool) in + return (items.map(\.file), isFinalResult) + } + + /*return self.account.network.request(Api.functions.messages.searchCustomEmoji(emoticon: emojiString.joined(separator: ""), hash: 0)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -209,7 +214,7 @@ public extension TelegramEngine { default: return .single([]) } - } + }*/ } } } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 9403c7b7bc..f0e4658df6 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -180,6 +180,23 @@ final class AvatarEditorScreenComponent: Component { ) } + 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 + } + } + class View: UIView { private let navigationCancelButton = ComponentView() private let navigationDoneButton = ComponentView() @@ -212,7 +229,8 @@ final class AvatarEditorScreenComponent: Component { private var data: AvatarKeyboardInputData? private let emojiSearchDisposable = MetaDisposable() - private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) + private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) + private var emojiSearchStateValue: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false) private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? @@ -263,20 +281,20 @@ final class AvatarEditorScreenComponent: Component { } let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void = { [weak self] query in - guard let strongSelf = self, let context = strongSelf.state?.context else { + guard let self, let context = self.state?.context else { return } switch query { case .none: - strongSelf.emojiSearchDisposable.set(nil) - strongSelf.emojiSearchResult.set(.single(nil)) + 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 { - strongSelf.emojiSearchDisposable.set(nil) - strongSelf.emojiSearchResult.set(.single(nil)) + 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") { @@ -292,30 +310,30 @@ final class AvatarEditorScreenComponent: Component { } } + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + let resultSignal = signal |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in return combineLatest( - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), - combineLatest(keywords.map { context.engine.stickers.searchStickers(query: $0.emoticons.first!) - |> map { items -> [FoundStickerItem] in - return items.items - } - }) + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + context.engine.stickers.availableReactions(), + hasPremium ) - |> map { view, stickers -> [EmojiPagerContentComponent.ItemGroup] in - let hasPremium = true + |> take(1) + |> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in + var result: [(String, TelegramMediaFile?, String)] = [] - var emojis: [(String, TelegramMediaFile?, String)] = [] - - var existingEmoticons = Set() var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { allEmoticons[emoticon] = keyword.keyword - - if !existingEmoticons.contains(emoticon) { - existingEmoticons.insert(emoticon) - } } } @@ -328,9 +346,9 @@ final class AvatarEditorScreenComponent: Component { case let .CustomEmoji(_, _, alt, _): if !item.file.isPremiumEmoji || hasPremium { if !alt.isEmpty, let keyword = allEmoticons[alt] { - emojis.append((alt, item.file, keyword)) + result.append((alt, item.file, keyword)) } else if alt == query { - emojis.append((alt, item.file, alt)) + result.append((alt, item.file, alt)) } } default: @@ -339,9 +357,10 @@ final class AvatarEditorScreenComponent: Component { } } - var emojiItems: [EmojiPagerContentComponent.Item] = [] + var items: [EmojiPagerContentComponent.Item] = [] + var existingIds = Set() - for item in emojis { + for item in result { if let itemFile = item.1 { if existingIds.contains(itemFile.fileId) { continue @@ -351,190 +370,104 @@ final class AvatarEditorScreenComponent: Component { let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), - itemFile: itemFile, - subgroupId: nil, + itemFile: itemFile, subgroupId: nil, icon: .none, tintMode: animationData.isTemplate ? .primary : .none ) - emojiItems.append(item) + items.append(item) } } - var stickerItems: [EmojiPagerContentComponent.Item] = [] - for stickerResult in stickers { - for sticker in stickerResult { - if existingIds.contains(sticker.file.fileId) { - continue - } - - existingIds.insert(sticker.file.fileId) - let animationData = EntityKeyboardAnimationData(file: sticker.file) - let item = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: sticker.file, - subgroupId: nil, - icon: .none, - tintMode: .none - ) - stickerItems.append(item) - } - } - - var result: [EmojiPagerContentComponent.ItemGroup] = [] - if !emojiItems.isEmpty { - result.append( - EmojiPagerContentComponent.ItemGroup( - supergroupId: "search", - groupId: "emoji", - title: "Emoji", - subtitle: nil, - actionButtonTitle: nil, - isFeatured: false, - isPremiumLocked: false, - isEmbedded: false, - hasClear: false, - collapsedLineCount: nil, - displayPremiumBadges: false, - headerItem: nil, - items: emojiItems - ) - ) - } - if !stickerItems.isEmpty { - result.append( - EmojiPagerContentComponent.ItemGroup( - supergroupId: "search", - groupId: "stickers", - title: "Stickers", - subtitle: nil, - actionButtonTitle: nil, - isFeatured: false, - isPremiumLocked: false, - isEmbedded: false, - hasClear: false, - collapsedLineCount: nil, - displayPremiumBadges: false, - headerItem: nil, - items: stickerItems - ) - ) - } - return result + 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 + )] } } - strongSelf.emojiSearchDisposable.set((resultSignal + 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 strongSelf = self else { + guard let self else { return } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + version += 1 })) } case let .category(value): - if strongSelf.state?.keyboardContentId == AnyHashable("emoji") { - let resultSignal = context.engine.stickers.searchEmoji(emojiString: value) - |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in - var items: [EmojiPagerContentComponent.Item] = [] - - var existingIds = Set() - for itemFile in files { - if existingIds.contains(itemFile.fileId) { - continue - } - existingIds.insert(itemFile.fileId) - let animationData = EntityKeyboardAnimationData(file: itemFile) - let item = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: itemFile, subgroupId: nil, - icon: .none, - tintMode: animationData.isTemplate ? .primary : .none - ) - items.append(item) + let resultSignal = context.engine.stickers.searchEmoji(emojiString: value) + |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for itemFile in files { + if existingIds.contains(itemFile.fileId) { + continue } - - return .single([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 - )]) + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) } - strongSelf.emojiSearchDisposable.set((resultSignal - |> delay(0.15, queue: .mainQueue()) - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { - return - } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value)))) - })) - } else { - let resultSignal = context.engine.stickers.searchStickers(query: value) - |> filter { result -> Bool in - return !result.items.isEmpty - } - |> map { result -> [TelegramMediaFile] in - return result.items.map { $0.file } - } - |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in - var items: [EmojiPagerContentComponent.Item] = [] - - var existingIds = Set() - for itemFile in files { - if existingIds.contains(itemFile.fileId) { - continue - } - existingIds.insert(itemFile.fileId) - let animationData = EntityKeyboardAnimationData(file: itemFile) - let item = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: itemFile, subgroupId: nil, - icon: .none, - tintMode: animationData.isTemplate ? .primary : .none - ) - items.append(item) - } - - return .single([EmojiPagerContentComponent.ItemGroup( - supergroupId: "search", - groupId: "search", - title: nil, - subtitle: nil, - actionButtonTitle: nil, - isFeatured: false, - isPremiumLocked: false, - isEmbedded: false, - hasClear: false, - collapsedLineCount: nil, - displayPremiumBadges: false, - headerItem: nil, - items: items - )]) - } - - strongSelf.emojiSearchDisposable.set((resultSignal - |> delay(0.15, queue: .mainQueue()) - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { - return - } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value)))) - })) + return .single(([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 + )], isFinalResult)) } + + let _ = resultSignal + + var version = 0 + self.emojiSearchDisposable.set((resultSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + + guard let group = result.items.first else { + return + } + if group.items.isEmpty && !result.isFinalResult { + self.emojiSearchStateValue.isSearching = true + return + } + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + version += 1 + })) } } @@ -897,15 +830,15 @@ final class AvatarEditorScreenComponent: Component { let context = component.context let signal = combineLatest(queue: .mainQueue(), controller.inputData |> delay(0.01, queue: .mainQueue()), - self.emojiSearchResult.get() + self.emojiSearchState.get() ) self.dataDisposable = (signal |> deliverOnMainQueue - ).start(next: { [weak self, weak state] data, searchResult in + ).start(next: { [weak self, weak state] data, emojiSearchState in if let self { var data = data - if let searchResult = searchResult { + if let searchResult = emojiSearchState.result { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? if !searchResult.groups.contains(where: { !$0.items.isEmpty }) { @@ -916,9 +849,9 @@ final class AvatarEditorScreenComponent: Component { } if state?.keyboardContentId == AnyHashable("emoji") { - data.emoji = data.emoji.withUpdatedItemGroups(panelItemGroups: data.emoji.panelItemGroups, contentItemGroups: searchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: searchResult.id, version: 0), emptySearchResults: emptySearchResults, searchState: .active) + data.emoji = data.emoji.withUpdatedItemGroups(panelItemGroups: data.emoji.panelItemGroups, contentItemGroups: searchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: searchResult.id, version: searchResult.version), emptySearchResults: emptySearchResults, searchState: .active) } else { - data.stickers = data.stickers?.withUpdatedItemGroups(panelItemGroups: data.stickers?.panelItemGroups ?? searchResult.groups, contentItemGroups: searchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: searchResult.id, version: 0), emptySearchResults: emptySearchResults, searchState: .active) + data.stickers = data.stickers?.withUpdatedItemGroups(panelItemGroups: data.stickers?.panelItemGroups ?? searchResult.groups, contentItemGroups: searchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: searchResult.id, version: searchResult.version), emptySearchResults: emptySearchResults, searchState: .active) } } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 19e7429472..428c4b5d97 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -426,7 +426,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { case let .emojiSearch(query): gifItems = combineLatest( hasRecentGifs, - paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil), + paneGifSearchForQuery(context: context, query: query.joined(separator: ""), offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil), searchCategories ) |> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in @@ -503,7 +503,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let gifItems: Signal - gifItems = combineLatest(hasRecentGifs, paneGifSearchForQuery(context: context, query: query, offset: token, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil), searchCategories) + gifItems = combineLatest(hasRecentGifs, paneGifSearchForQuery(context: context, query: query.joined(separator: ""), offset: token, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil), searchCategories) |> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in var items: [GifPagerContentComponent.Item] = [] var existingIds = Set() @@ -977,22 +977,22 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } }, updateSearchQuery: { [weak self] query in - guard let strongSelf = self else { + guard let self = self else { return } switch query { case .none: - strongSelf.emojiSearchDisposable.set(nil) - strongSelf.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) + 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 { - strongSelf.emojiSearchDisposable.set(nil) - strongSelf.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) + self.emojiSearchDisposable.set(nil) + self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) } else { - let context = strongSelf.context + let context = self.context var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { @@ -1028,17 +1028,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { |> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in var result: [(String, TelegramMediaFile?, String)] = [] - var existingEmoticons = Set() - var allEmoticonsList: [String] = [] var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { allEmoticons[emoticon] = keyword.keyword - - if !existingEmoticons.contains(emoticon) { - allEmoticonsList.append(emoticon) - existingEmoticons.insert(emoticon) - } } } @@ -1075,8 +1068,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), - itemFile: itemFile, - subgroupId: nil, + itemFile: itemFile, subgroupId: nil, icon: .none, tintMode: animationData.isTemplate ? .primary : .none ) @@ -1084,17 +1076,6 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } - for emoji in allEmoticonsList { - items.append(EmojiPagerContentComponent.Item( - animationData: nil, - content: .staticEmoji(emoji), - itemFile: nil, - subgroupId: nil, - icon: .none, - tintMode: .none - )) - } - return [EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", @@ -1114,21 +1095,21 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } var version = 0 - strongSelf.emojiSearchStateValue.isSearching = true - strongSelf.emojiSearchDisposable.set((resultSignal + self.emojiSearchStateValue.isSearching = true + self.emojiSearchDisposable.set((resultSignal |> delay(0.15, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { + guard let self else { return } - strongSelf.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) version += 1 })) } case let .category(value): - let resultSignal = strongSelf.context.engine.stickers.searchEmoji(emojiString: value) - |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value) + |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] var existingIds = Set() @@ -1148,7 +1129,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { items.append(item) } - return .single([EmojiPagerContentComponent.ItemGroup( + return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, @@ -1162,25 +1143,25 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { displayPremiumBadges: false, headerItem: nil, items: items - )]) + )], isFinalResult)) } - - let delayValue: Double - /*#if DEBUG - delayValue = 2.3 - #else*/ - delayValue = 0.0 - //#endif var version = 0 - strongSelf.emojiSearchStateValue.isSearching = true - strongSelf.emojiSearchDisposable.set((resultSignal - |> delay(delayValue, queue: .mainQueue()) + self.emojiSearchDisposable.set((resultSignal |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { + guard let self else { return } - strongSelf.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + + guard let group = result.items.first else { + return + } + if group.items.isEmpty && !result.isFinalResult { + self.emojiSearchStateValue.isSearching = true + return + } + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) version += 1 })) } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift index 3deadacafe..4e8b5c162e 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift @@ -338,7 +338,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { let query = text.trimmingCharacters(in: .whitespacesAndNewlines) if query.isSingleEmoji { - signals = .single([context.engine.stickers.searchStickers(query: text.basicEmoji.0) + signals = .single([context.engine.stickers.searchStickers(query: [text.basicEmoji.0]) |> map { (nil, $0.items) }]) } else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) @@ -360,7 +360,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { var signals: [Signal<(String?, [FoundStickerItem]), NoError>] = [] let emoticons = keywords.flatMap { $0.emoticons } for emoji in emoticons { - signals.append(context.engine.stickers.searchStickers(query: emoji.basicEmoji.0) + signals.append(context.engine.stickers.searchStickers(query: [emoji.basicEmoji.0]) // |> take(1) |> map { (emoji, $0.items) }) } diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index bbbfb7608f..05e46109d8 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -234,6 +234,23 @@ public final class EmojiStatusSelectionComponent: Component { public final class EmojiStatusSelectionController: ViewController { private final class Node: ViewControllerTracingNode { + private struct EmojiSearchResult { + var groups: [EmojiPagerContentComponent.ItemGroup] + var id: AnyHashable + var version: Int + var isPreset: Bool + } + + private struct EmojiSearchState { + var result: EmojiSearchResult? + var isSearching: Bool + + init(result: EmojiSearchResult?, isSearching: Bool) { + self.result = result + self.isSearching = isSearching + } + } + private weak var controller: EmojiStatusSelectionController? private let context: AccountContext private weak var sourceView: UIView? @@ -258,7 +275,9 @@ public final class EmojiStatusSelectionController: ViewController { private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private let emojiSearchDisposable = MetaDisposable() - private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) + private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) + private var emojiSearchStateValue: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false) + private var emptyResultEmojis: [TelegramMediaFile] = [] private var stableEmptyResultEmoji: TelegramMediaFile? private let stableEmptyResultEmojiDisposable = MetaDisposable() @@ -349,16 +368,16 @@ public final class EmojiStatusSelectionController: ViewController { self.emojiContentDisposable = (combineLatest(queue: .mainQueue(), emojiContent, - self.emojiSearchResult.get() + self.emojiSearchState.get() ) - |> deliverOnMainQueue).start(next: { [weak self] emojiContent, emojiSearchResult in + |> deliverOnMainQueue).start(next: { [weak self] emojiContent, emojiSearchState in guard let strongSelf = self else { return } strongSelf.controller?._ready.set(.single(true)) var emojiContent = emojiContent - if let emojiSearchResult = emojiSearchResult { + if let emojiSearchResult = emojiSearchState.result { var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) { if strongSelf.stableEmptyResultEmoji == nil { @@ -371,7 +390,7 @@ public final class EmojiStatusSelectionController: ViewController { } else { strongSelf.stableEmptyResultEmoji = nil } - emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: 0), emptySearchResults: emptySearchResults, searchState: .active) + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active) } else { strongSelf.stableEmptyResultEmoji = nil } @@ -433,22 +452,22 @@ public final class EmojiStatusSelectionController: ViewController { requestUpdate: { _ in }, updateSearchQuery: { query in - guard let strongSelf = self else { + guard let self = self else { return } switch query { case .none: - strongSelf.emojiSearchDisposable.set(nil) - strongSelf.emojiSearchResult.set(.single(nil)) + 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 { - strongSelf.emojiSearchDisposable.set(nil) - strongSelf.emojiSearchResult.set(.single(nil)) + self.emojiSearchDisposable.set(nil) + self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) } else { - let context = strongSelf.context + let context = self.context var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { @@ -550,18 +569,22 @@ public final class EmojiStatusSelectionController: ViewController { } } - strongSelf.emojiSearchDisposable.set((resultSignal + var version = 0 + self.emojiSearchStateValue.isSearching = true + self.emojiSearchDisposable.set((resultSignal |> delay(0.15, queue: .mainQueue()) - |> deliverOnMainQueue).start(next: { result in - guard let strongSelf = self else { + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { return } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + version += 1 })) } case let .category(value): - let resultSignal = strongSelf.context.engine.stickers.searchEmoji(emojiString: value) - |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value) + |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] var existingIds = Set() @@ -581,7 +604,7 @@ public final class EmojiStatusSelectionController: ViewController { items.append(item) } - return .single([EmojiPagerContentComponent.ItemGroup( + return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, @@ -595,16 +618,26 @@ public final class EmojiStatusSelectionController: ViewController { displayPremiumBadges: false, headerItem: nil, items: items - )]) + )], isFinalResult)) } - strongSelf.emojiSearchDisposable.set((resultSignal - |> delay(0.15, queue: .mainQueue()) - |> deliverOnMainQueue).start(next: { result in - guard let strongSelf = self else { + var version = 0 + self.emojiSearchDisposable.set((resultSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { return } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value)))) + + guard let group = result.items.first else { + return + } + if group.items.isEmpty && !result.isFinalResult { + self.emojiSearchStateValue.isSearching = true + return + } + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + version += 1 })) } }, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index c0e449ad21..29d3e1ef8c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1607,7 +1607,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { private var textField: EmojiSearchTextField? private var tapRecognizer: UITapGestureRecognizer? - private(set) var currentPresetSearchTerm: String? + private(set) var currentPresetSearchTerm: [String]? private var params: Params? @@ -2365,7 +2365,7 @@ public final class EmojiPagerContentComponent: Component { public enum SearchQuery: Equatable { case text(value: String, language: String) - case category(value: String) + case category(value: [String]) } public enum ItemContent: Equatable { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift index c32e01f34d..02be699e83 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift @@ -23,6 +23,23 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode var deviceMetrics: DeviceMetrics } + 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 context: AccountContext private var initialFocusId: ItemCollectionId? private let hasPremiumForUse: Bool @@ -41,8 +58,9 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode public var onCancel: (() -> Void)? private let emojiSearchDisposable = MetaDisposable() - private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) - private var emojiSearchResultValue: (groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)? + private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) + private var emojiSearchStateValue: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false) + private var immediateEmojiSearchState: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false) private var dataDisposable: Disposable? @@ -160,13 +178,13 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode switch query { case .none: self.emojiSearchDisposable.set(nil) - self.emojiSearchResult.set(.single(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.emojiSearchResult.set(.single(nil)) + self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) } else { let context = self.context @@ -270,18 +288,22 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode } } + 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.emojiSearchResult.set(.single((result, AnyHashable(query)))) + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + version += 1 })) } case let .category(value): let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value) - |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] var existingIds = Set() @@ -301,7 +323,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode items.append(item) } - return .single([EmojiPagerContentComponent.ItemGroup( + return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, @@ -315,16 +337,28 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode displayPremiumBadges: false, headerItem: nil, items: items - )]) + )], isFinalResult)) } + + let _ = resultSignal + var version = 0 self.emojiSearchDisposable.set((resultSignal - |> delay(0.15, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } - self.emojiSearchResult.set(.single((result, AnyHashable(value)))) + + guard let group = result.items.first else { + return + } + if group.items.isEmpty && !result.isFinalResult { + self.emojiSearchStateValue.isSearching = true + return + } + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: false), isSearching: false) + version += 1 })) } }, @@ -347,13 +381,13 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode ) self.dataDisposable = ( - self.emojiSearchResult.get() + self.emojiSearchState.get() |> deliverOnMainQueue - ).start(next: { [weak self] emojiSearchResult in + ).start(next: { [weak self] emojiSearchState in guard let self else { return } - self.emojiSearchResultValue = emojiSearchResult + self.immediateEmojiSearchState = emojiSearchState self.update(transition: .immediate) }) } @@ -403,7 +437,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode selectedItems: Set() ) - if let emojiSearchResult = self.emojiSearchResultValue { + if let emojiSearchResult = self.immediateEmojiSearchState.result { var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) { emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( @@ -411,7 +445,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode iconFile: nil ) } - emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: 0), emptySearchResults: emptySearchResults, searchState: .empty(hasResults: true)) + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: self.immediateEmojiSearchState.isSearching ? .searching : .empty(hasResults: true)) } let _ = self.keyboardView.update( diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift index 888b2c4851..6ce874a3a1 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -103,7 +103,7 @@ final class EmojiSearchSearchBarComponent: Component { let useOpaqueTheme: Bool let textInputState: TextInputState let categories: EmojiSearchCategories? - let searchTermUpdated: (String?) -> Void + let searchTermUpdated: ([String]?) -> Void let activateTextInput: () -> Void init( @@ -113,7 +113,7 @@ final class EmojiSearchSearchBarComponent: Component { useOpaqueTheme: Bool, textInputState: TextInputState, categories: EmojiSearchCategories?, - searchTermUpdated: @escaping (String?) -> Void, + searchTermUpdated: @escaping ([String]?) -> Void, activateTextInput: @escaping () -> Void ) { self.context = context @@ -360,7 +360,7 @@ final class EmojiSearchSearchBarComponent: Component { self.componentState?.updated(transition: .easeInOut(duration: 0.2)) if let _ = self.selectedItem, let categories = component.categories, let group = categories.groups.first(where: { $0.id == itemId }) { - component.searchTermUpdated(group.identifiers.joined(separator: "")) + component.searchTermUpdated(group.identifiers) if let itemComponentView = itemView.view.view { var offset = self.scrollView.contentOffset.x diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 0ae4ea731e..d2ef6d3095 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -139,7 +139,7 @@ public final class GifPagerContentComponent: Component { public enum Subject: Equatable { case recent case trending - case emojiSearch(String) + case emojiSearch([String]) } public final class InputInteraction { @@ -147,14 +147,14 @@ public final class GifPagerContentComponent: Component { public let openGifContextMenu: (Item, UIView, CGRect, ContextGesture, Bool) -> Void public let loadMore: (String) -> Void public let openSearch: () -> Void - public let updateSearchQuery: (String?) -> Void + public let updateSearchQuery: ([String]?) -> Void public init( performItemAction: @escaping (Item, UIView, CGRect) -> Void, openGifContextMenu: @escaping (Item, UIView, CGRect, ContextGesture, Bool) -> Void, loadMore: @escaping (String) -> Void, openSearch: @escaping () -> Void, - updateSearchQuery: @escaping (String?) -> Void + updateSearchQuery: @escaping ([String]?) -> Void ) { self.performItemAction = performItemAction self.openGifContextMenu = openGifContextMenu diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 519400ce3f..50d791ef28 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -127,7 +127,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee case .installed: scope = [.installed] } - return context.engine.stickers.searchStickers(query: query.basicEmoji.0, scope: scope) + return context.engine.stickers.searchStickers(query: [query.basicEmoji.0], scope: scope) |> map { items -> [FoundStickerItem] in return items.items }