feat: add searchable emoji picker

This commit is contained in:
Valdemar Lorenzen
2026-10-15 13:59:10 +02:00
parent 6574c97276
commit 1de7983345
12 changed files with 2322 additions and 43 deletions
+61
View File
@@ -0,0 +1,61 @@
import Foundation
public struct Emoji: Sendable {
public let value: String
public let name: String
public let category: String
}
public enum EmojiCatalog {
private struct Entry: Sendable {
let emoji: Emoji
let searchText: String
}
private static let entries: [Entry] = {
// SwiftPM's generated accessor does not search Contents/Resources in a packaged app.
let packagedBundle = Bundle.main.resourceURL.flatMap {
Bundle(url: $0.appendingPathComponent("Conjure_ConjureKit.bundle", isDirectory: true))
}
let bundle = packagedBundle ?? Bundle.module
guard let url = bundle.url(forResource: "emoji", withExtension: "tsv"),
let contents = try? String(contentsOf: url, encoding: .utf8) else {
preconditionFailure("The bundled emoji catalog is missing or unreadable")
}
// Pinned github/gemoji v4.1.0; attribution and license accompany the TSV.
return contents.split(whereSeparator: \.isNewline).map { line in
let fields = line.split(separator: "\t", omittingEmptySubsequences: false)
precondition(fields.count == 5, "Invalid bundled emoji catalog row")
return Entry(
emoji: Emoji(value: String(fields[0]), name: String(fields[1]), category: String(fields[2])),
searchText: normalized([fields[0], fields[1], fields[3], fields[4]].joined(separator: " "))
)
}
}()
public static let all: [Emoji] = entries.map(\.emoji)
public static let categories: [String] = {
var seen = Set<String>()
return all.compactMap { seen.insert($0.category).inserted ? $0.category : nil }
}()
/// Matches every query word against names, GitHub aliases, tags, or literal emoji.
/// Category names use the exact values supplied by `categories`; results retain catalog order.
public static func search(_ query: String, category: String? = nil) -> [Emoji] {
let words = normalized(query).split(whereSeparator: \.isWhitespace)
if words.isEmpty && category == nil { return all }
return entries.compactMap { entry in
guard category == nil || entry.emoji.category == category,
words.allSatisfy({ entry.searchText.contains($0) }) else { return nil }
return entry.emoji
}
}
private static func normalized(_ text: String) -> String {
text.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: Locale(identifier: "en_US_POSIX"))
.replacingOccurrences(of: "_", with: " ")
.replacingOccurrences(of: ":", with: " ")
}
}