Fix #1934 - Hook reaction pickers into the system's recently used keyboard emojis (#3453)

This commit is contained in:
Stefan Ceriu
2024-10-25 19:58:56 +03:00
committed by GitHub
parent 35cbc84a99
commit 7a47e37d38
35 changed files with 274 additions and 133 deletions
@@ -1,13 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
struct EmojiCategory: Equatable, Identifiable {
let id: String
let emojis: [EmojiItem]
}
@@ -1,19 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
struct EmojiItem: Equatable, Identifiable {
var id: String {
label
}
let label: String
let unicode: String
let keywords: [String]
let shortcodes: [String]
}
@@ -7,31 +7,38 @@
import Emojibase
import Foundation
@MainActor
protocol EmojiProviderProtocol {
func categories(searchString: String?) async -> [EmojiCategory]
}
private enum EmojiProviderState {
case notLoaded
case inProgress(Task<[EmojiCategory], Never>)
case loaded([EmojiCategory])
}
import OrderedCollections
class EmojiProvider: EmojiProviderProtocol {
private let loader: EmojiLoaderProtocol
private var state: EmojiProviderState = .notLoaded
private let appSettings: AppSettings
init(loader: EmojiLoaderProtocol = EmojibaseDatasource()) {
private(set) var state: EmojiProviderState = .notLoaded
init(loader: EmojiLoaderProtocol = EmojibaseDatasource(), appSettings: AppSettings) {
self.loader = loader
self.appSettings = appSettings
Task {
await loadIfNeeded()
}
}
func categories(searchString: String? = nil) async -> [EmojiCategory] {
let emojiCategories = await loadIfNeeded()
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
let frequentlyUsedEmojis = frequentlyUsedSystemEmojis().prefix(20)
let emojis = allEmojis.filter { frequentlyUsedEmojis.contains($0.unicode) }
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 {
@@ -39,6 +46,40 @@ class EmojiProvider: EmojiProviderProtocol {
}
}
func frequentlyUsedSystemEmojis() -> [String] {
guard appSettings.frequentEmojisEnabled, !ProcessInfo.processInfo.isiOSAppOnMac else {
return []
}
guard let preferences = UserDefaults(suiteName: "com.apple.EmojiPreferences"),
let defaults = preferences.dictionary(forKey: "EMFDefaultsKey"),
let recents = defaults["EMFRecentsKey"] as? [String]
else {
return []
}
return recents
}
func markEmojiAsFrequentlyUsed(_ emoji: String) {
guard appSettings.frequentEmojisEnabled else {
return
}
guard let preferences = UserDefaults(suiteName: "com.apple.EmojiPreferences"),
let defaults = preferences.dictionary(forKey: "EMFDefaultsKey"),
let recents = defaults["EMFRecentsKey"] as? [String] else {
return
}
var uniqueOrderedRecents = OrderedSet(recents)
uniqueOrderedRecents.insert(emoji, at: 0)
preferences.setValue(["EMFRecentsKey": Array(uniqueOrderedRecents)], forKey: "EMFDefaultsKey")
}
// MARK: - Private
private func search(searchString: String, emojiCategories: [EmojiCategory]) -> [EmojiCategory] {
emojiCategories.compactMap { category in
let emojis = category.emojis.filter { emoji in
@@ -0,0 +1,42 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
struct EmojiItem: Equatable, Identifiable {
var id: String {
label
}
let label: String
let unicode: String
let keywords: [String]
let shortcodes: [String]
}
struct EmojiCategory: Equatable, Identifiable {
static let frequentlyUsedCategoryIdentifier = "io.element.elementx.frequently_used"
let id: String
let emojis: [EmojiItem]
}
enum EmojiProviderState {
case notLoaded
case inProgress(Task<[EmojiCategory], Never>)
case loaded([EmojiCategory])
}
@MainActor
protocol EmojiProviderProtocol {
var state: EmojiProviderState { get }
func categories(searchString: String?) async -> [EmojiCategory]
func frequentlyUsedSystemEmojis() -> [String]
func markEmojiAsFrequentlyUsed(_ emoji: String)
}