Add emoji search result cache

This commit is contained in:
Ali 2023-01-27 14:06:44 +01:00
parent 50ac94a2fe
commit b3140ffed9
14 changed files with 577 additions and 357 deletions

View File

@ -1204,7 +1204,7 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode {
let query = text.trimmingCharacters(in: .whitespacesAndNewlines) let query = text.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isSingleEmoji { 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) }]) |> map { (nil, $0.items) }])
} else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { } 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) 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>] = [] var signals: [Signal<(String?, [FoundStickerItem]), NoError>] = []
let emoticons = keywords.flatMap { $0.emoticons } let emoticons = keywords.flatMap { $0.emoticons }
for emoji in 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) |> take(1)
|> map { (emoji, $0.items) }) |> map { (emoji, $0.items) })
} }

View File

@ -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 context: AccountContext
private let presentationData: PresentationData private let presentationData: PresentationData
private let animationCache: AnimationCache private let animationCache: AnimationCache
@ -235,7 +252,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private var emojiContentDisposable: Disposable? private var emojiContentDisposable: Disposable?
private let emojiSearchDisposable = MetaDisposable() private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) private let emojiSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var emojiSearchStateValue: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false)
private var emptyResultEmojis: [TelegramMediaFile] = [] private var emptyResultEmojis: [TelegramMediaFile] = []
private var stableEmptyResultEmoji: TelegramMediaFile? private var stableEmptyResultEmoji: TelegramMediaFile?
private let stableEmptyResultEmojiDisposable = MetaDisposable() private let stableEmptyResultEmojiDisposable = MetaDisposable()
@ -440,14 +459,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
self.emojiContentDisposable = combineLatest(queue: .mainQueue(), self.emojiContentDisposable = combineLatest(queue: .mainQueue(),
getEmojiContent(self.animationCache, self.animationRenderer), getEmojiContent(self.animationCache, self.animationRenderer),
self.emojiSearchResult.get() self.emojiSearchState.get()
).start(next: { [weak self] emojiContent, emojiSearchResult in ).start(next: { [weak self] emojiContent, emojiSearchState in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
var emojiContent = emojiContent var emojiContent = emojiContent
if let emojiSearchResult = emojiSearchResult { if let emojiSearchResult = emojiSearchState.result {
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) { if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) {
if strongSelf.stableEmptyResultEmoji == nil { if strongSelf.stableEmptyResultEmoji == nil {
@ -460,7 +479,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} else { } else {
strongSelf.stableEmptyResultEmoji = nil 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 { } else {
strongSelf.stableEmptyResultEmoji = nil strongSelf.stableEmptyResultEmoji = nil
} }
@ -1340,22 +1359,22 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
strongSelf.requestUpdateOverlayWantsToBeBelowKeyboard(transition.containedViewLayoutTransition) strongSelf.requestUpdateOverlayWantsToBeBelowKeyboard(transition.containedViewLayoutTransition)
}, },
updateSearchQuery: { [weak self] query in updateSearchQuery: { [weak self] query in
guard let strongSelf = self else { guard let self else {
return return
} }
switch query { switch query {
case .none: case .none:
strongSelf.emojiSearchDisposable.set(nil) self.emojiSearchDisposable.set(nil)
strongSelf.emojiSearchResult.set(.single(nil)) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
case let .text(rawQuery, languageCode): case let .text(rawQuery, languageCode):
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty { if query.isEmpty {
strongSelf.emojiSearchDisposable.set(nil) self.emojiSearchDisposable.set(nil)
strongSelf.emojiSearchResult.set(.single(nil)) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
} else { } else {
let context = strongSelf.context let context = self.context
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
if !languageCode.lowercased().hasPrefix("en") { 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()) |> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else { guard let self else {
return 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): case let .category(value):
let resultSignal = strongSelf.context.engine.stickers.searchEmoji(emojiString: 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 items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>() var existingIds = Set<MediaId>()
@ -1488,7 +1511,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
items.append(item) items.append(item)
} }
return .single([EmojiPagerContentComponent.ItemGroup( return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search", supergroupId: "search",
groupId: "search", groupId: "search",
title: nil, title: nil,
@ -1502,16 +1525,26 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
displayPremiumBadges: false, displayPremiumBadges: false,
headerItem: nil, headerItem: nil,
items: items items: items
)]) )], isFinalResult))
} }
strongSelf.emojiSearchDisposable.set((resultSignal var version = 0
|> delay(0.15, queue: .mainQueue()) self.emojiSearchDisposable.set((resultSignal
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else { guard let self else {
return 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
})) }))
} }
}, },

View File

@ -103,6 +103,7 @@ public struct Namespaces {
public static let attachMenuBots: Int8 = 23 public static let attachMenuBots: Int8 = 23
public static let featuredStickersConfiguration: Int8 = 24 public static let featuredStickersConfiguration: Int8 = 24
public static let emojiSearchCategories: Int8 = 25 public static let emojiSearchCategories: Int8 = 25
public static let cachedEmojiQueryResults: Int8 = 26
} }
public struct UnorderedItemList { public struct UnorderedItemList {

View File

@ -81,14 +81,15 @@ func _internal_randomGreetingSticker(account: Account) -> Signal<FoundStickerIte
} }
} }
func _internal_searchStickers(account: Account, query: String, scope: SearchStickersScope = [.installed, .remote]) -> 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 { if scope.isEmpty {
return .single(([], true)) return .single(([], true))
} }
var query = query var query = query
if query == "\u{2764}" { if query == ["\u{2764}"] {
query = "\u{2764}\u{FE0F}" query = ["\u{2764}\u{FE0F}"]
} }
return account.postbox.transaction { transaction -> ([FoundStickerItem], CachedStickerQueryResult?, Bool, SearchStickersConfiguration) in return account.postbox.transaction { transaction -> ([FoundStickerItem], CachedStickerQueryResult?, Bool, SearchStickersConfiguration) in
let isPremium = transaction.getPeer(account.peerId)?.isPremium ?? false 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) { for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers) {
if let item = entry.contents.get(SavedStickerItem.self) { if let item = entry.contents.get(SavedStickerItem.self) {
for representation in item.stringRepresentations { for representation in item.stringRepresentations {
if representation.hasPrefix(query) { for queryItem in query {
if representation.hasPrefix(queryItem) {
result.append(FoundStickerItem(file: item.file, stringRepresentations: item.stringRepresentations)) result.append(FoundStickerItem(file: item.file, stringRepresentations: item.stringRepresentations))
break break
} }
} }
} }
} }
}
let currentItems = Set<MediaId>(result.map { $0.file.fileId }) var currentItems = Set<MediaId>(result.map { $0.file.fileId })
var recentItems: [TelegramMediaFile] = [] var recentItems: [TelegramMediaFile] = []
var recentAnimatedItems: [TelegramMediaFile] = [] var recentAnimatedItems: [TelegramMediaFile] = []
var recentItemsIds = Set<MediaId>() var recentItemsIds = Set<MediaId>()
@ -119,9 +122,14 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic
} }
if !currentItems.contains(file.fileId) { if !currentItems.contains(file.fileId) {
currentItems.insert(file.fileId)
for case let .Sticker(displayText, _, _) in file.attributes { for case let .Sticker(displayText, _, _) in file.attributes {
if displayText.hasPrefix(query) { for queryItem in query {
if displayText.hasPrefix(queryItem) {
matchingRecentItemsIds.insert(file.fileId) matchingRecentItemsIds.insert(file.fileId)
break
}
} }
recentItemsIds.insert(file.fileId) recentItemsIds.insert(file.fileId)
if file.isAnimatedSticker || file.isVideoSticker { if file.isAnimatedSticker || file.isVideoSticker {
@ -135,18 +143,23 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic
} }
} }
var searchQuery: ItemCollectionSearchQuery = .exact(ValueBoxKey(query)) var searchQueries: [ItemCollectionSearchQuery] = query.map { queryItem -> ItemCollectionSearchQuery in
if query == "\u{2764}" { return .exact(ValueBoxKey(queryItem))
searchQuery = .any([ValueBoxKey("\u{2764}"), ValueBoxKey("\u{2764}\u{FE0F}")]) }
if query == ["\u{2764}"] {
searchQueries = [.any([ValueBoxKey("\u{2764}"), ValueBoxKey("\u{2764}\u{FE0F}")])]
} }
var installedItems: [FoundStickerItem] = [] var installedItems: [FoundStickerItem] = []
var installedAnimatedItems: [FoundStickerItem] = [] var installedAnimatedItems: [FoundStickerItem] = []
var installedPremiumItems: [FoundStickerItem] = [] var installedPremiumItems: [FoundStickerItem] = []
for searchQuery in searchQueries {
for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudStickerPacks, query: searchQuery) { for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudStickerPacks, query: searchQuery) {
if let item = item as? StickerPackItem { if let item = item as? StickerPackItem {
if !currentItems.contains(item.file.fileId) { if !currentItems.contains(item.file.fileId) {
currentItems.insert(item.file.fileId)
var stringRepresentations: [String] = [] var stringRepresentations: [String] = []
for key in item.indexKeys { for key in item.indexKeys {
key.withDataNoCopy { data in key.withDataNoCopy { data in
@ -169,13 +182,14 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic
} }
} }
} }
}
for file in recentAnimatedItems { for file in recentAnimatedItems {
if file.isPremiumSticker && !isPremium { if file.isPremiumSticker && !isPremium {
continue continue
} }
if matchingRecentItemsIds.contains(file.fileId) { 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 continue
} }
if matchingRecentItemsIds.contains(file.fileId) { 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) 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 currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue 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<Api.messages.Stickers, NoError> in |> `catch` { _ -> Signal<Api.messages.Stickers, NoError> in
return .single(.stickersNotModified) return .single(.stickersNotModified)
} }
@ -356,7 +371,7 @@ func _internal_searchStickers(account: Account, query: String, scope: SearchStic
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let entry = CodableEntry(CachedStickerQueryResult(items: files, hash: hash, timestamp: currentTime)) { 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) 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<MediaId>(result.map { $0.file.fileId })
var recentItems: [TelegramMediaFile] = []
var recentAnimatedItems: [TelegramMediaFile] = []
var recentItemsIds = Set<MediaId>()
var matchingRecentItemsIds = Set<MediaId>()
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<MediaId>(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<Api.EmojiList, NoError> 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<MediaId>(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 struct FoundStickerSets {
public var infos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)] public var infos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)]
public let entries: [ItemCollectionViewEntry] public let entries: [ItemCollectionViewEntry]

View File

@ -30,7 +30,7 @@ public extension TelegramEngine {
return _internal_randomGreetingSticker(account: self.account) 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) 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) return _internal_resolveInlineStickers(postbox: self.account.postbox, network: self.account.network, fileIds: fileIds)
} }
public func searchEmoji(emojiString: String) -> Signal<[TelegramMediaFile], NoError> { public func searchEmoji(emojiString: [String]) -> Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> {
return self.account.network.request(Api.functions.messages.searchCustomEmoji(emoticon: emojiString, hash: 0)) 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) |> map(Optional.init)
|> `catch` { _ -> Signal<Api.EmojiList?, NoError> in |> `catch` { _ -> Signal<Api.EmojiList?, NoError> in
return .single(nil) return .single(nil)
@ -209,7 +214,7 @@ public extension TelegramEngine {
default: default:
return .single([]) return .single([])
} }
} }*/
} }
} }
} }

View File

@ -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 { class View: UIView {
private let navigationCancelButton = ComponentView<Empty>() private let navigationCancelButton = ComponentView<Empty>()
private let navigationDoneButton = ComponentView<Empty>() private let navigationDoneButton = ComponentView<Empty>()
@ -212,7 +229,8 @@ final class AvatarEditorScreenComponent: Component {
private var data: AvatarKeyboardInputData? private var data: AvatarKeyboardInputData?
private let emojiSearchDisposable = MetaDisposable() private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) private let emojiSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var emojiSearchStateValue: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false)
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
@ -263,20 +281,20 @@ final class AvatarEditorScreenComponent: Component {
} }
let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void = { [weak self] query in 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 return
} }
switch query { switch query {
case .none: case .none:
strongSelf.emojiSearchDisposable.set(nil) self.emojiSearchDisposable.set(nil)
strongSelf.emojiSearchResult.set(.single(nil)) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
case let .text(rawQuery, languageCode): case let .text(rawQuery, languageCode):
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty { if query.isEmpty {
strongSelf.emojiSearchDisposable.set(nil) self.emojiSearchDisposable.set(nil)
strongSelf.emojiSearchResult.set(.single(nil)) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
} else { } else {
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
if !languageCode.lowercased().hasPrefix("en") { 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 let resultSignal = signal
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
return combineLatest( return combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
combineLatest(keywords.map { context.engine.stickers.searchStickers(query: $0.emoticons.first!) context.engine.stickers.availableReactions(),
|> map { items -> [FoundStickerItem] in hasPremium
return items.items
}
})
) )
|> map { view, stickers -> [EmojiPagerContentComponent.ItemGroup] in |> take(1)
let hasPremium = true |> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in
var result: [(String, TelegramMediaFile?, String)] = []
var emojis: [(String, TelegramMediaFile?, String)] = []
var existingEmoticons = Set<String>()
var allEmoticons: [String: String] = [:] var allEmoticons: [String: String] = [:]
for keyword in keywords { for keyword in keywords {
for emoticon in keyword.emoticons { for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword allEmoticons[emoticon] = keyword.keyword
if !existingEmoticons.contains(emoticon) {
existingEmoticons.insert(emoticon)
}
} }
} }
@ -328,9 +346,9 @@ final class AvatarEditorScreenComponent: Component {
case let .CustomEmoji(_, _, alt, _): case let .CustomEmoji(_, _, alt, _):
if !item.file.isPremiumEmoji || hasPremium { if !item.file.isPremiumEmoji || hasPremium {
if !alt.isEmpty, let keyword = allEmoticons[alt] { if !alt.isEmpty, let keyword = allEmoticons[alt] {
emojis.append((alt, item.file, keyword)) result.append((alt, item.file, keyword))
} else if alt == query { } else if alt == query {
emojis.append((alt, item.file, alt)) result.append((alt, item.file, alt))
} }
} }
default: default:
@ -339,9 +357,10 @@ final class AvatarEditorScreenComponent: Component {
} }
} }
var emojiItems: [EmojiPagerContentComponent.Item] = [] var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>() var existingIds = Set<MediaId>()
for item in emojis { for item in result {
if let itemFile = item.1 { if let itemFile = item.1 {
if existingIds.contains(itemFile.fileId) { if existingIds.contains(itemFile.fileId) {
continue continue
@ -351,43 +370,18 @@ final class AvatarEditorScreenComponent: Component {
let item = EmojiPagerContentComponent.Item( let item = EmojiPagerContentComponent.Item(
animationData: animationData, animationData: animationData,
content: .animation(animationData), content: .animation(animationData),
itemFile: itemFile, itemFile: itemFile, subgroupId: nil,
subgroupId: nil,
icon: .none, icon: .none,
tintMode: animationData.isTemplate ? .primary : .none tintMode: animationData.isTemplate ? .primary : .none
) )
emojiItems.append(item) items.append(item)
} }
} }
var stickerItems: [EmojiPagerContentComponent.Item] = [] return [EmojiPagerContentComponent.ItemGroup(
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", supergroupId: "search",
groupId: "emoji", groupId: "search",
title: "Emoji", title: nil,
subtitle: nil, subtitle: nil,
actionButtonTitle: nil, actionButtonTitle: nil,
isFeatured: false, isFeatured: false,
@ -397,46 +391,27 @@ final class AvatarEditorScreenComponent: Component {
collapsedLineCount: nil, collapsedLineCount: nil,
displayPremiumBadges: false, displayPremiumBadges: false,
headerItem: nil, headerItem: nil,
items: emojiItems items: items
) )]
)
}
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
} }
} }
strongSelf.emojiSearchDisposable.set((resultSignal var version = 0
self.emojiSearchStateValue.isSearching = true
self.emojiSearchDisposable.set((resultSignal
|> delay(0.15, queue: .mainQueue()) |> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else { guard let self else {
return 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): case let .category(value):
if strongSelf.state?.keyboardContentId == AnyHashable("emoji") {
let resultSignal = context.engine.stickers.searchEmoji(emojiString: value) let resultSignal = 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 items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>() var existingIds = Set<MediaId>()
@ -456,7 +431,7 @@ final class AvatarEditorScreenComponent: Component {
items.append(item) items.append(item)
} }
return .single([EmojiPagerContentComponent.ItemGroup( return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search", supergroupId: "search",
groupId: "search", groupId: "search",
title: nil, title: nil,
@ -470,71 +445,29 @@ final class AvatarEditorScreenComponent: Component {
displayPremiumBadges: false, displayPremiumBadges: false,
headerItem: nil, headerItem: nil,
items: items items: items
)]) )], isFinalResult))
} }
strongSelf.emojiSearchDisposable.set((resultSignal let _ = resultSignal
|> delay(0.15, queue: .mainQueue())
var version = 0
self.emojiSearchDisposable.set((resultSignal
|> deliverOnMainQueue).start(next: { [weak self] result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else { guard let self else {
return 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<MediaId>() guard let group = result.items.first else {
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 return
} }
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value)))) 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 context = component.context
let signal = combineLatest(queue: .mainQueue(), let signal = combineLatest(queue: .mainQueue(),
controller.inputData |> delay(0.01, queue: .mainQueue()), controller.inputData |> delay(0.01, queue: .mainQueue()),
self.emojiSearchResult.get() self.emojiSearchState.get()
) )
self.dataDisposable = (signal self.dataDisposable = (signal
|> deliverOnMainQueue |> deliverOnMainQueue
).start(next: { [weak self, weak state] data, searchResult in ).start(next: { [weak self, weak state] data, emojiSearchState in
if let self { if let self {
var data = data var data = data
if let searchResult = searchResult { if let searchResult = emojiSearchState.result {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !searchResult.groups.contains(where: { !$0.items.isEmpty }) { if !searchResult.groups.contains(where: { !$0.items.isEmpty }) {
@ -916,9 +849,9 @@ final class AvatarEditorScreenComponent: Component {
} }
if state?.keyboardContentId == AnyHashable("emoji") { 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 { } 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)
} }
} }

View File

@ -426,7 +426,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
case let .emojiSearch(query): case let .emojiSearch(query):
gifItems = combineLatest( gifItems = combineLatest(
hasRecentGifs, 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 searchCategories
) )
|> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in |> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in
@ -503,7 +503,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let gifItems: Signal<EntityKeyboardGifContent, NoError> let gifItems: Signal<EntityKeyboardGifContent, NoError>
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 |> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in
var items: [GifPagerContentComponent.Item] = [] var items: [GifPagerContentComponent.Item] = []
var existingIds = Set<MediaId>() var existingIds = Set<MediaId>()
@ -977,22 +977,22 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
} }
}, },
updateSearchQuery: { [weak self] query in updateSearchQuery: { [weak self] query in
guard let strongSelf = self else { guard let self = self else {
return return
} }
switch query { switch query {
case .none: case .none:
strongSelf.emojiSearchDisposable.set(nil) self.emojiSearchDisposable.set(nil)
strongSelf.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
case let .text(rawQuery, languageCode): case let .text(rawQuery, languageCode):
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty { if query.isEmpty {
strongSelf.emojiSearchDisposable.set(nil) self.emojiSearchDisposable.set(nil)
strongSelf.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
} else { } else {
let context = strongSelf.context let context = self.context
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
if !languageCode.lowercased().hasPrefix("en") { if !languageCode.lowercased().hasPrefix("en") {
@ -1028,17 +1028,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in |> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in
var result: [(String, TelegramMediaFile?, String)] = [] var result: [(String, TelegramMediaFile?, String)] = []
var existingEmoticons = Set<String>()
var allEmoticonsList: [String] = []
var allEmoticons: [String: String] = [:] var allEmoticons: [String: String] = [:]
for keyword in keywords { for keyword in keywords {
for emoticon in keyword.emoticons { for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword 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( let item = EmojiPagerContentComponent.Item(
animationData: animationData, animationData: animationData,
content: .animation(animationData), content: .animation(animationData),
itemFile: itemFile, itemFile: itemFile, subgroupId: nil,
subgroupId: nil,
icon: .none, icon: .none,
tintMode: animationData.isTemplate ? .primary : .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( return [EmojiPagerContentComponent.ItemGroup(
supergroupId: "search", supergroupId: "search",
groupId: "search", groupId: "search",
@ -1114,21 +1095,21 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
} }
var version = 0 var version = 0
strongSelf.emojiSearchStateValue.isSearching = true self.emojiSearchStateValue.isSearching = true
strongSelf.emojiSearchDisposable.set((resultSignal self.emojiSearchDisposable.set((resultSignal
|> delay(0.15, queue: .mainQueue()) |> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else { guard let self else {
return 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 version += 1
})) }))
} }
case let .category(value): case let .category(value):
let resultSignal = strongSelf.context.engine.stickers.searchEmoji(emojiString: 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 items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>() var existingIds = Set<MediaId>()
@ -1148,7 +1129,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
items.append(item) items.append(item)
} }
return .single([EmojiPagerContentComponent.ItemGroup( return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search", supergroupId: "search",
groupId: "search", groupId: "search",
title: nil, title: nil,
@ -1162,25 +1143,25 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
displayPremiumBadges: false, displayPremiumBadges: false,
headerItem: nil, headerItem: nil,
items: items items: items
)]) )], isFinalResult))
} }
let delayValue: Double
/*#if DEBUG
delayValue = 2.3
#else*/
delayValue = 0.0
//#endif
var version = 0 var version = 0
strongSelf.emojiSearchStateValue.isSearching = true self.emojiSearchDisposable.set((resultSignal
strongSelf.emojiSearchDisposable.set((resultSignal
|> delay(delayValue, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else { guard let self else {
return 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 version += 1
})) }))
} }

View File

@ -338,7 +338,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
let query = text.trimmingCharacters(in: .whitespacesAndNewlines) let query = text.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isSingleEmoji { 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) }]) |> map { (nil, $0.items) }])
} else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { } 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) 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>] = [] var signals: [Signal<(String?, [FoundStickerItem]), NoError>] = []
let emoticons = keywords.flatMap { $0.emoticons } let emoticons = keywords.flatMap { $0.emoticons }
for emoji in 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) // |> take(1)
|> map { (emoji, $0.items) }) |> map { (emoji, $0.items) })
} }

View File

@ -234,6 +234,23 @@ public final class EmojiStatusSelectionComponent: Component {
public final class EmojiStatusSelectionController: ViewController { public final class EmojiStatusSelectionController: ViewController {
private final class Node: ViewControllerTracingNode { 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 weak var controller: EmojiStatusSelectionController?
private let context: AccountContext private let context: AccountContext
private weak var sourceView: UIView? private weak var sourceView: UIView?
@ -258,7 +275,9 @@ public final class EmojiStatusSelectionController: ViewController {
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
private let emojiSearchDisposable = MetaDisposable() private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) private let emojiSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var emojiSearchStateValue: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false)
private var emptyResultEmojis: [TelegramMediaFile] = [] private var emptyResultEmojis: [TelegramMediaFile] = []
private var stableEmptyResultEmoji: TelegramMediaFile? private var stableEmptyResultEmoji: TelegramMediaFile?
private let stableEmptyResultEmojiDisposable = MetaDisposable() private let stableEmptyResultEmojiDisposable = MetaDisposable()
@ -349,16 +368,16 @@ public final class EmojiStatusSelectionController: ViewController {
self.emojiContentDisposable = (combineLatest(queue: .mainQueue(), self.emojiContentDisposable = (combineLatest(queue: .mainQueue(),
emojiContent, 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 { guard let strongSelf = self else {
return return
} }
strongSelf.controller?._ready.set(.single(true)) strongSelf.controller?._ready.set(.single(true))
var emojiContent = emojiContent var emojiContent = emojiContent
if let emojiSearchResult = emojiSearchResult { if let emojiSearchResult = emojiSearchState.result {
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) { if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) {
if strongSelf.stableEmptyResultEmoji == nil { if strongSelf.stableEmptyResultEmoji == nil {
@ -371,7 +390,7 @@ public final class EmojiStatusSelectionController: ViewController {
} else { } else {
strongSelf.stableEmptyResultEmoji = nil 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 { } else {
strongSelf.stableEmptyResultEmoji = nil strongSelf.stableEmptyResultEmoji = nil
} }
@ -433,22 +452,22 @@ public final class EmojiStatusSelectionController: ViewController {
requestUpdate: { _ in requestUpdate: { _ in
}, },
updateSearchQuery: { query in updateSearchQuery: { query in
guard let strongSelf = self else { guard let self = self else {
return return
} }
switch query { switch query {
case .none: case .none:
strongSelf.emojiSearchDisposable.set(nil) self.emojiSearchDisposable.set(nil)
strongSelf.emojiSearchResult.set(.single(nil)) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
case let .text(rawQuery, languageCode): case let .text(rawQuery, languageCode):
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty { if query.isEmpty {
strongSelf.emojiSearchDisposable.set(nil) self.emojiSearchDisposable.set(nil)
strongSelf.emojiSearchResult.set(.single(nil)) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
} else { } else {
let context = strongSelf.context let context = self.context
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
if !languageCode.lowercased().hasPrefix("en") { 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()) |> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else { guard let self else {
return 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): case let .category(value):
let resultSignal = strongSelf.context.engine.stickers.searchEmoji(emojiString: 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 items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>() var existingIds = Set<MediaId>()
@ -581,7 +604,7 @@ public final class EmojiStatusSelectionController: ViewController {
items.append(item) items.append(item)
} }
return .single([EmojiPagerContentComponent.ItemGroup( return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search", supergroupId: "search",
groupId: "search", groupId: "search",
title: nil, title: nil,
@ -595,16 +618,26 @@ public final class EmojiStatusSelectionController: ViewController {
displayPremiumBadges: false, displayPremiumBadges: false,
headerItem: nil, headerItem: nil,
items: items items: items
)]) )], isFinalResult))
} }
strongSelf.emojiSearchDisposable.set((resultSignal var version = 0
|> delay(0.15, queue: .mainQueue()) self.emojiSearchDisposable.set((resultSignal
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else { guard let self else {
return 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
})) }))
} }
}, },

View File

@ -1607,7 +1607,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
private var textField: EmojiSearchTextField? private var textField: EmojiSearchTextField?
private var tapRecognizer: UITapGestureRecognizer? private var tapRecognizer: UITapGestureRecognizer?
private(set) var currentPresetSearchTerm: String? private(set) var currentPresetSearchTerm: [String]?
private var params: Params? private var params: Params?
@ -2365,7 +2365,7 @@ public final class EmojiPagerContentComponent: Component {
public enum SearchQuery: Equatable { public enum SearchQuery: Equatable {
case text(value: String, language: String) case text(value: String, language: String)
case category(value: String) case category(value: [String])
} }
public enum ItemContent: Equatable { public enum ItemContent: Equatable {

View File

@ -23,6 +23,23 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
var deviceMetrics: DeviceMetrics 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 let context: AccountContext
private var initialFocusId: ItemCollectionId? private var initialFocusId: ItemCollectionId?
private let hasPremiumForUse: Bool private let hasPremiumForUse: Bool
@ -41,8 +58,9 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
public var onCancel: (() -> Void)? public var onCancel: (() -> Void)?
private let emojiSearchDisposable = MetaDisposable() private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) private let emojiSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var emojiSearchResultValue: (groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)? private var emojiSearchStateValue: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false)
private var immediateEmojiSearchState: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false)
private var dataDisposable: Disposable? private var dataDisposable: Disposable?
@ -160,13 +178,13 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
switch query { switch query {
case .none: case .none:
self.emojiSearchDisposable.set(nil) self.emojiSearchDisposable.set(nil)
self.emojiSearchResult.set(.single(nil)) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
case let .text(rawQuery, languageCode): case let .text(rawQuery, languageCode):
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty { if query.isEmpty {
self.emojiSearchDisposable.set(nil) self.emojiSearchDisposable.set(nil)
self.emojiSearchResult.set(.single(nil)) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
} else { } else {
let context = self.context 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 self.emojiSearchDisposable.set((resultSignal
|> delay(0.15, queue: .mainQueue()) |> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else { guard let self else {
return 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): case let .category(value):
let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: 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 items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>() var existingIds = Set<MediaId>()
@ -301,7 +323,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
items.append(item) items.append(item)
} }
return .single([EmojiPagerContentComponent.ItemGroup( return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search", supergroupId: "search",
groupId: "search", groupId: "search",
title: nil, title: nil,
@ -315,16 +337,28 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
displayPremiumBadges: false, displayPremiumBadges: false,
headerItem: nil, headerItem: nil,
items: items items: items
)]) )], isFinalResult))
} }
let _ = resultSignal
var version = 0
self.emojiSearchDisposable.set((resultSignal self.emojiSearchDisposable.set((resultSignal
|> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else { guard let self else {
return 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.dataDisposable = (
self.emojiSearchResult.get() self.emojiSearchState.get()
|> deliverOnMainQueue |> deliverOnMainQueue
).start(next: { [weak self] emojiSearchResult in ).start(next: { [weak self] emojiSearchState in
guard let self else { guard let self else {
return return
} }
self.emojiSearchResultValue = emojiSearchResult self.immediateEmojiSearchState = emojiSearchState
self.update(transition: .immediate) self.update(transition: .immediate)
}) })
} }
@ -403,7 +437,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
selectedItems: Set() selectedItems: Set()
) )
if let emojiSearchResult = self.emojiSearchResultValue { if let emojiSearchResult = self.immediateEmojiSearchState.result {
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) { if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) {
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
@ -411,7 +445,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
iconFile: nil 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( let _ = self.keyboardView.update(

View File

@ -103,7 +103,7 @@ final class EmojiSearchSearchBarComponent: Component {
let useOpaqueTheme: Bool let useOpaqueTheme: Bool
let textInputState: TextInputState let textInputState: TextInputState
let categories: EmojiSearchCategories? let categories: EmojiSearchCategories?
let searchTermUpdated: (String?) -> Void let searchTermUpdated: ([String]?) -> Void
let activateTextInput: () -> Void let activateTextInput: () -> Void
init( init(
@ -113,7 +113,7 @@ final class EmojiSearchSearchBarComponent: Component {
useOpaqueTheme: Bool, useOpaqueTheme: Bool,
textInputState: TextInputState, textInputState: TextInputState,
categories: EmojiSearchCategories?, categories: EmojiSearchCategories?,
searchTermUpdated: @escaping (String?) -> Void, searchTermUpdated: @escaping ([String]?) -> Void,
activateTextInput: @escaping () -> Void activateTextInput: @escaping () -> Void
) { ) {
self.context = context self.context = context
@ -360,7 +360,7 @@ final class EmojiSearchSearchBarComponent: Component {
self.componentState?.updated(transition: .easeInOut(duration: 0.2)) 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 }) { 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 { if let itemComponentView = itemView.view.view {
var offset = self.scrollView.contentOffset.x var offset = self.scrollView.contentOffset.x

View File

@ -139,7 +139,7 @@ public final class GifPagerContentComponent: Component {
public enum Subject: Equatable { public enum Subject: Equatable {
case recent case recent
case trending case trending
case emojiSearch(String) case emojiSearch([String])
} }
public final class InputInteraction { public final class InputInteraction {
@ -147,14 +147,14 @@ public final class GifPagerContentComponent: Component {
public let openGifContextMenu: (Item, UIView, CGRect, ContextGesture, Bool) -> Void public let openGifContextMenu: (Item, UIView, CGRect, ContextGesture, Bool) -> Void
public let loadMore: (String) -> Void public let loadMore: (String) -> Void
public let openSearch: () -> Void public let openSearch: () -> Void
public let updateSearchQuery: (String?) -> Void public let updateSearchQuery: ([String]?) -> Void
public init( public init(
performItemAction: @escaping (Item, UIView, CGRect) -> Void, performItemAction: @escaping (Item, UIView, CGRect) -> Void,
openGifContextMenu: @escaping (Item, UIView, CGRect, ContextGesture, Bool) -> Void, openGifContextMenu: @escaping (Item, UIView, CGRect, ContextGesture, Bool) -> Void,
loadMore: @escaping (String) -> Void, loadMore: @escaping (String) -> Void,
openSearch: @escaping () -> Void, openSearch: @escaping () -> Void,
updateSearchQuery: @escaping (String?) -> Void updateSearchQuery: @escaping ([String]?) -> Void
) { ) {
self.performItemAction = performItemAction self.performItemAction = performItemAction
self.openGifContextMenu = openGifContextMenu self.openGifContextMenu = openGifContextMenu

View File

@ -127,7 +127,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
case .installed: case .installed:
scope = [.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 |> map { items -> [FoundStickerItem] in
return items.items return items.items
} }