mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-06 04:18:31 +00:00
fc75f90ef5
* Remove the dynamic member lookup from UserSettings in favour of nested access. * Remove forwarding methods from UserSettings. * Update the Enterprise submodule.
140 lines
4.9 KiB
Swift
140 lines
4.9 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
@preconcurrency import Emojibase
|
|
import Foundation
|
|
|
|
class EmojiProvider: EmojiProviderProtocol {
|
|
private let maxFrequentEmojis = 20
|
|
private let loader: EmojiLoaderProtocol
|
|
private let userSettings: UserSettings
|
|
|
|
private(set) var state: EmojiProviderState = .notLoaded
|
|
|
|
init(loader: EmojiLoaderProtocol = EmojibaseDatasource(), userSettings: UserSettings) {
|
|
self.loader = loader
|
|
self.userSettings = userSettings
|
|
|
|
Task {
|
|
await loadIfNeeded()
|
|
}
|
|
}
|
|
|
|
func categories(searchString: String? = nil) async -> [EmojiCategory] {
|
|
var emojiCategories = await loadIfNeeded()
|
|
|
|
let allEmojis = emojiCategories.reduce([]) { partialResult, category in
|
|
partialResult + category.emojis
|
|
}
|
|
|
|
// Map frequently used system unicode emojis to our emoji provider ones and preserve the order
|
|
let frequentlyUsedEmojis = frequentlyUsedSystemEmojis().prefix(maxFrequentEmojis)
|
|
let emojis = allEmojis
|
|
.filter { frequentlyUsedEmojis.contains($0.unicode) }
|
|
.sorted { first, second in
|
|
guard let firstIndex = frequentlyUsedEmojis.firstIndex(of: first.unicode),
|
|
let secondIndex = frequentlyUsedEmojis.firstIndex(of: second.unicode) else {
|
|
return false
|
|
}
|
|
|
|
return firstIndex < secondIndex
|
|
}
|
|
|
|
if !emojis.isEmpty {
|
|
emojiCategories.insert(.init(id: EmojiCategory.frequentlyUsedCategoryIdentifier, emojis: emojis), at: 0)
|
|
}
|
|
|
|
if let searchString, searchString.isEmpty == false {
|
|
return search(searchString: searchString, emojiCategories: emojiCategories)
|
|
} else {
|
|
return emojiCategories
|
|
}
|
|
}
|
|
|
|
func frequentlyUsedSystemEmojis() -> [String] {
|
|
guard !ProcessInfo.processInfo.isiOSAppOnMac else {
|
|
return []
|
|
}
|
|
|
|
return userSettings.app.frequentlyUsedSystemEmojis.map(\.key)
|
|
}
|
|
|
|
func markEmojiAsFrequentlyUsed(_ emoji: String) {
|
|
guard !ProcessInfo.processInfo.isiOSAppOnMac else {
|
|
return
|
|
}
|
|
|
|
let frequentlyUsed = if !frequentlyUsedSystemEmojis().contains(emoji) {
|
|
userSettings.app.frequentlyUsedSystemEmojis + [.init(count: 0, key: emoji)]
|
|
} else {
|
|
userSettings.app.frequentlyUsedSystemEmojis.map { frequentlyUsedEmoji in
|
|
if frequentlyUsedEmoji.key == emoji {
|
|
return FrequentlyUsedEmoji(count: frequentlyUsedEmoji.count + 1, key: emoji)
|
|
}
|
|
|
|
return frequentlyUsedEmoji
|
|
}
|
|
}
|
|
|
|
userSettings.app.frequentlyUsedSystemEmojis = frequentlyUsed.sorted { $0.count > $1.count }
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func search(searchString: String, emojiCategories: [EmojiCategory]) -> [EmojiCategory] {
|
|
emojiCategories.compactMap { category in
|
|
let emojis = category.emojis.filter { emoji in
|
|
let searchArray = [emoji.label + emoji.unicode] + emoji.shortcodes + emoji.keywords
|
|
return searchArray.description.range(of: searchString, options: .caseInsensitive) != nil
|
|
}
|
|
return emojis.isEmpty ? nil : EmojiCategory(id: category.id, emojis: emojis)
|
|
}
|
|
}
|
|
|
|
private func loadIfNeeded() async -> [EmojiCategory] {
|
|
switch state {
|
|
case .notLoaded:
|
|
let task = Task {
|
|
await loader.load()
|
|
}
|
|
state = .inProgress(task)
|
|
let categories = await task.value
|
|
state = .loaded(categories)
|
|
return categories
|
|
case .loaded(let categories):
|
|
return categories
|
|
case .inProgress(let task):
|
|
return await task.value
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated extension EmojibaseDatasource: EmojiLoaderProtocol {
|
|
func load() async -> [EmojiCategory] {
|
|
do {
|
|
let store: EmojibaseStore = try await load()
|
|
return EmojibaseCategory.allCases.map { category in
|
|
let emojis = store.categories[category.rawValue] ?? []
|
|
return EmojiCategory(id: category.rawValue, emojis: emojis.compactMap(EmojiItem.init(from:)))
|
|
}
|
|
} catch {
|
|
MXLog.error("Failed retrieving emojis from the emojibase datasource: \(error)")
|
|
}
|
|
return []
|
|
}
|
|
}
|
|
|
|
extension EmojiItem {
|
|
init?(from emojibase: Emoji) {
|
|
unicode = emojibase.unicode
|
|
label = emojibase.label
|
|
shortcodes = emojibase.shortcodes
|
|
keywords = emojibase.tags ?? []
|
|
}
|
|
}
|