Skip to main content

iris_chat_core/
emoji.rs

1//! Shared emoji catalog and search aliases for native pickers and typed
2//! shortcode completion. Linux keeps its native GTK EmojiChooser and uses
3//! these aliases for completion in the message composer.
4
5#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
6pub struct EmojiCategorySnapshot {
7    pub name: String,
8    pub emojis: Vec<String>,
9}
10
11#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
12pub struct EmojiAliasSnapshot {
13    pub emoji: String,
14    pub keywords: String,
15}
16
17fn cat(name: &str, emojis: &[&str]) -> EmojiCategorySnapshot {
18    EmojiCategorySnapshot {
19        name: name.to_string(),
20        emojis: emojis.iter().map(|s| (*s).to_string()).collect(),
21    }
22}
23
24fn alias(emoji: &str, keywords: &str) -> EmojiAliasSnapshot {
25    EmojiAliasSnapshot {
26        emoji: emoji.to_string(),
27        keywords: keywords.to_string(),
28    }
29}
30
31#[uniffi::export]
32pub fn iris_emoji_catalog() -> Vec<EmojiCategorySnapshot> {
33    vec![
34        cat(
35            "Smileys",
36            &[
37                "๐Ÿ˜€",
38                "๐Ÿ˜ƒ",
39                "๐Ÿ˜„",
40                "๐Ÿ˜",
41                "๐Ÿ˜†",
42                "๐Ÿ˜…",
43                "๐Ÿคฃ",
44                "๐Ÿ˜‚",
45                "๐Ÿ™‚",
46                "๐Ÿ™ƒ",
47                "๐Ÿซ ",
48                "๐Ÿ˜‰",
49                "๐Ÿ˜Š",
50                "๐Ÿ˜‡",
51                "๐Ÿฅฐ",
52                "๐Ÿ˜",
53                "๐Ÿคฉ",
54                "๐Ÿ˜˜",
55                "๐Ÿ˜—",
56                "โ˜บ๏ธ",
57                "๐Ÿ˜š",
58                "๐Ÿ˜™",
59                "๐Ÿฅฒ",
60                "๐Ÿ˜‹",
61                "๐Ÿ˜›",
62                "๐Ÿ˜œ",
63                "๐Ÿคช",
64                "๐Ÿ˜",
65                "๐Ÿค‘",
66                "๐Ÿค—",
67                "๐Ÿคญ",
68                "๐Ÿซข",
69                "๐Ÿซฃ",
70                "๐Ÿคซ",
71                "๐Ÿค”",
72                "๐Ÿซก",
73                "๐Ÿค",
74                "๐Ÿคจ",
75                "๐Ÿ˜",
76                "๐Ÿ˜‘",
77                "๐Ÿ˜ถ",
78                "๐Ÿซฅ",
79                "๐Ÿ˜ถโ€๐ŸŒซ๏ธ",
80                "๐Ÿ˜",
81                "๐Ÿ˜’",
82                "๐Ÿ™„",
83                "๐Ÿ˜ฌ",
84                "๐Ÿ˜ฎโ€๐Ÿ’จ",
85                "๐Ÿคฅ",
86                "๐Ÿ˜Œ",
87                "๐Ÿ˜”",
88                "๐Ÿ˜ช",
89                "๐Ÿคค",
90                "๐Ÿ˜ด",
91                "๐Ÿ˜ท",
92                "๐Ÿค’",
93                "๐Ÿค•",
94                "๐Ÿคข",
95                "๐Ÿคฎ",
96                "๐Ÿคง",
97                "๐Ÿฅต",
98                "๐Ÿฅถ",
99                "๐Ÿฅด",
100                "๐Ÿ˜ต",
101                "๐Ÿ˜ตโ€๐Ÿ’ซ",
102                "๐Ÿคฏ",
103                "๐Ÿค ",
104                "๐Ÿฅณ",
105                "๐Ÿฅธ",
106                "๐Ÿ˜Ž",
107                "๐Ÿค“",
108                "๐Ÿง",
109                "๐Ÿ˜•",
110                "๐Ÿซค",
111                "๐Ÿ˜Ÿ",
112                "๐Ÿ™",
113                "โ˜น๏ธ",
114                "๐Ÿ˜ฎ",
115                "๐Ÿ˜ฏ",
116                "๐Ÿ˜ฒ",
117                "๐Ÿ˜ณ",
118                "๐Ÿฅบ",
119                "๐Ÿฅน",
120                "๐Ÿ˜ฆ",
121                "๐Ÿ˜ง",
122                "๐Ÿ˜จ",
123                "๐Ÿ˜ฐ",
124                "๐Ÿ˜ฅ",
125                "๐Ÿ˜ข",
126                "๐Ÿ˜ญ",
127                "๐Ÿ˜ฑ",
128                "๐Ÿ˜–",
129                "๐Ÿ˜ฃ",
130                "๐Ÿ˜ž",
131                "๐Ÿ˜“",
132                "๐Ÿ˜ฉ",
133                "๐Ÿ˜ซ",
134                "๐Ÿฅฑ",
135                "๐Ÿ˜ค",
136                "๐Ÿ˜ก",
137                "๐Ÿ˜ ",
138                "๐Ÿคฌ",
139                "๐Ÿ˜ˆ",
140                "๐Ÿ‘ฟ",
141                "๐Ÿ’€",
142                "โ˜ ๏ธ",
143                "๐Ÿ’ฉ",
144                "๐Ÿคก",
145                "๐Ÿ‘น",
146                "๐Ÿ‘บ",
147                "๐Ÿ‘ป",
148                "๐Ÿ‘ฝ",
149                "๐Ÿ‘พ",
150                "๐Ÿค–",
151                "๐Ÿ˜บ",
152                "๐Ÿ˜ธ",
153                "๐Ÿ˜น",
154                "๐Ÿ˜ป",
155                "๐Ÿ˜ผ",
156                "๐Ÿ˜ฝ",
157                "๐Ÿ™€",
158                "๐Ÿ˜ฟ",
159                "๐Ÿ˜พ",
160            ],
161        ),
162        cat(
163            "Hearts",
164            &[
165                "โค๏ธ",
166                "๐Ÿงก",
167                "๐Ÿ’›",
168                "๐Ÿ’š",
169                "๐Ÿ’™",
170                "๐Ÿ’œ",
171                "๐Ÿ–ค",
172                "๐Ÿค",
173                "๐ŸคŽ",
174                "๐Ÿ’–",
175                "๐Ÿ’—",
176                "๐Ÿ’“",
177                "๐Ÿ’ž",
178                "๐Ÿ’•",
179                "๐Ÿ’˜",
180                "๐Ÿ’",
181                "๐Ÿ’Ÿ",
182                "โ™ฅ๏ธ",
183                "๐Ÿ’”",
184                "โฃ๏ธ",
185                "โค๏ธโ€๐Ÿ”ฅ",
186                "โค๏ธโ€๐Ÿฉน",
187            ],
188        ),
189        cat(
190            "Hands",
191            &[
192                "๐Ÿ‘", "๐Ÿ‘Ž", "๐Ÿ‘Œ", "๐ŸคŒ", "๐Ÿค", "โœŒ๏ธ", "๐Ÿคž", "๐Ÿซฐ", "๐ŸคŸ", "๐Ÿค˜", "๐Ÿค™", "๐Ÿ‘ˆ", "๐Ÿ‘‰", "๐Ÿ‘†",
193                "๐Ÿ‘‡", "โ˜๏ธ", "โœ‹", "๐Ÿคš", "๐Ÿ–", "๐Ÿ––", "๐Ÿ‘‹", "๐Ÿค", "๐Ÿ™", "๐Ÿ‘", "๐Ÿ™Œ", "๐Ÿซถ", "๐Ÿ’ช", "๐Ÿซต",
194                "๐Ÿซฑ", "๐Ÿซฒ",
195            ],
196        ),
197        cat(
198            "Animals",
199            &[
200                "๐Ÿถ", "๐Ÿฑ", "๐Ÿญ", "๐Ÿน", "๐Ÿฐ", "๐ŸฆŠ", "๐Ÿป", "๐Ÿผ", "๐Ÿจ", "๐Ÿฏ", "๐Ÿฆ", "๐Ÿฎ", "๐Ÿท", "๐Ÿธ",
201                "๐Ÿต", "๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ”", "๐Ÿง", "๐Ÿฆ", "๐Ÿฆ…", "๐Ÿฆ‰", "๐Ÿฆ„", "๐Ÿ", "๐Ÿฆ‹", "๐Ÿž", "๐Ÿข",
202                "๐Ÿ", "๐Ÿฆ–", "๐Ÿ™", "๐Ÿฆ€", "๐Ÿฌ", "๐Ÿณ", "๐Ÿฆˆ",
203            ],
204        ),
205        cat(
206            "Food",
207            &[
208                "๐Ÿ", "๐ŸŽ", "๐Ÿ", "๐ŸŠ", "๐Ÿ‹", "๐ŸŒ", "๐Ÿ‰", "๐Ÿ‡", "๐Ÿ“", "๐Ÿซ", "๐Ÿ’", "๐Ÿ‘", "๐Ÿฅญ", "๐Ÿ",
209                "๐Ÿฅฅ", "๐Ÿฅ", "๐Ÿ…", "๐Ÿฅ‘", "๐Ÿฅ•", "๐ŸŒฝ", "๐Ÿ†", "๐Ÿฅ”", "๐Ÿ•", "๐Ÿ”", "๐ŸŸ", "๐ŸŒญ", "๐Ÿฟ", "๐Ÿฅช",
210                "๐ŸŒฎ", "๐ŸŒฏ", "๐Ÿฃ", "๐Ÿœ", "๐Ÿ", "๐Ÿฆ", "๐Ÿฉ", "๐Ÿช", "๐ŸŽ‚", "๐Ÿฐ", "โ˜•", "๐Ÿต", "๐Ÿบ", "๐Ÿฅ‚",
211                "๐Ÿท", "๐Ÿฅƒ",
212            ],
213        ),
214        cat(
215            "Activities",
216            &[
217                "โšฝ", "๐Ÿ€", "๐Ÿˆ", "โšพ", "๐ŸฅŽ", "๐ŸŽพ", "๐Ÿ", "๐Ÿ‰", "๐ŸŽฑ", "๐Ÿช€", "๐Ÿ“", "๐Ÿธ", "๐Ÿฅ…", "๐Ÿ’",
218                "๐Ÿ‘", "๐Ÿฅ", "๐Ÿ", "๐Ÿชƒ", "๐ŸฅŠ", "๐Ÿฅ‹", "๐ŸŽฝ", "โ›ธ", "๐ŸฅŒ", "๐Ÿ›ท", "๐Ÿช‚", "๐Ÿ‹๏ธ", "๐Ÿคธ", "๐Ÿคบ",
219                "๐Ÿ‡", "โ›ท", "๐Ÿ‚", "๐ŸŒ๏ธ", "๐Ÿ„", "๐Ÿšฃ", "๐ŸŠ", "๐Ÿคฝ", "๐Ÿšด", "๐Ÿšต", "๐ŸŽฏ", "๐ŸŽฎ", "๐ŸŽฒ", "๐ŸŽผ",
220                "๐ŸŽค", "๐ŸŽง", "๐ŸŽท", "๐ŸŽธ", "๐Ÿฅ",
221            ],
222        ),
223        cat(
224            "Travel",
225            &[
226                "๐Ÿš—", "๐Ÿš•", "๐Ÿš™", "๐ŸšŒ", "๐ŸšŽ", "๐ŸŽ", "๐Ÿš“", "๐Ÿš‘", "๐Ÿš’", "๐Ÿš", "๐Ÿ›ป", "๐Ÿšš", "๐Ÿš›", "๐Ÿšœ",
227                "๐Ÿ›ต", "๐Ÿ", "๐Ÿ›บ", "๐Ÿšฒ", "๐Ÿ›ด", "๐Ÿ›น", "๐Ÿš‚", "โœˆ๏ธ", "๐Ÿš€", "๐Ÿ›ธ", "๐Ÿ›ถ", "โ›ต", "๐Ÿšข", "๐Ÿš",
228                "๐Ÿ—บ", "๐Ÿ—ฝ", "๐Ÿ—ผ", "๐Ÿฐ", "๐ŸŽก", "๐ŸŽข", "๐ŸŽ ", "๐Ÿ–", "๐Ÿ", "๐Ÿ”", "๐ŸŒ‹", "๐Ÿ•", "๐ŸŒ„", "๐ŸŒ…",
229                "๐ŸŒŒ",
230            ],
231        ),
232        cat(
233            "Objects",
234            &[
235                "๐Ÿ“ฑ", "๐Ÿ’ป", "โŒจ๏ธ", "๐Ÿ–ฅ", "๐Ÿ–จ", "๐Ÿ–ฑ", "๐Ÿ’พ", "๐Ÿ’ฟ", "๐Ÿ“ท", "๐Ÿ“ธ", "๐Ÿ“น", "๐ŸŽฅ", "๐Ÿ“บ", "๐Ÿ“ป",
236                "๐Ÿ“ž", "โ˜Ž๏ธ", "๐Ÿ”Œ", "๐Ÿ”‹", "๐Ÿ’ก", "๐Ÿ”ฆ", "๐Ÿ•ฏ", "๐Ÿงฏ", "๐Ÿ›ข", "๐Ÿ’ต", "๐Ÿ’ฐ", "๐Ÿ’ณ", "๐Ÿ’Ž", "โš–๏ธ",
237                "๐Ÿ”ง", "๐Ÿ”จ", "๐Ÿ› ", "โ›", "๐Ÿช›", "๐Ÿชš", "๐Ÿ”ฉ", "โš™๏ธ", "๐Ÿงฑ", "โ›“", "๐Ÿงฒ", "๐Ÿ”ซ", "๐Ÿ’ฃ", "๐Ÿงจ",
238            ],
239        ),
240        cat(
241            "Symbols",
242            &[
243                "โœ…", "โŽ", "โœ”๏ธ", "โŒ", "โญ•", "๐Ÿšซ", "โš ๏ธ", "๐Ÿ”ฑ", "โ˜‘๏ธ", "๐Ÿ’ฏ", "๐Ÿ”ฅ", "โœจ", "๐ŸŒŸ", "โญ",
244                "๐ŸŒˆ", "โ˜€๏ธ", "๐ŸŒ™", "โšก", "โ˜„๏ธ", "๐Ÿ’ฅ", "๐ŸŒŠ", "๐Ÿ’ง", "๐Ÿ’ฆ", "๐ŸŽ‰", "๐ŸŽŠ", "๐ŸŽ", "๐ŸŽ€", "๐ŸŽˆ",
245                "๐Ÿช…", "๐Ÿพ", "๐Ÿฅ‡", "๐Ÿฅˆ", "๐Ÿฅ‰", "๐Ÿ†", "๐ŸŽ–", "๐Ÿ…", "๐Ÿ’ค", "๐Ÿ’ญ", "๐Ÿ—ฏ", "๐Ÿ’ฌ", "๐Ÿ†—", "๐Ÿ†•",
246                "๐Ÿ†’", "๐Ÿ†“", "๐Ÿ†™", "๐Ÿ”", "โ™ป๏ธ", "โ˜ฎ๏ธ", "โœ๏ธ", "โ˜ช๏ธ", "๐Ÿ•‰", "โ˜ธ๏ธ", "โœก๏ธ", "โ˜ฏ๏ธ", "โ˜ฆ๏ธ",
247            ],
248        ),
249    ]
250}
251
252#[uniffi::export]
253pub fn iris_emoji_search_aliases() -> Vec<EmojiAliasSnapshot> {
254    let search_aliases = vec![
255        alias("๐Ÿ˜‚", "laugh laughing lol haha"),
256        alias("๐Ÿคฃ", "laugh laughing lol haha rolling"),
257        alias("๐Ÿ˜Š", "smile smiling happy"),
258        alias("๐Ÿ™‚", "smile smiling happy"),
259        alias("๐Ÿ˜", "love heart eyes"),
260        alias("๐Ÿฅฐ", "love hearts"),
261        alias("๐Ÿ˜˜", "kiss love"),
262        alias("๐Ÿ˜ข", "sad tear crying"),
263        alias("๐Ÿ˜ญ", "sad cry crying"),
264        alias("๐Ÿ˜ ", "angry mad"),
265        alias("๐Ÿคฌ", "angry mad swearing"),
266        alias("๐Ÿ™", "pray praying thanks thank you please"),
267        alias("๐Ÿ‘", "clap applause"),
268        alias("๐Ÿ™Œ", "hooray yay hands"),
269        alias("โค๏ธ", "love heart red"),
270        alias("โ™ฅ๏ธ", "love heart red"),
271        alias("๐Ÿ”ฅ", "fire lit hot"),
272        alias("๐ŸŽ‰", "party celebrate celebration"),
273        alias("๐ŸŽŠ", "party celebrate celebration"),
274        alias("โœจ", "sparkle sparkles"),
275        alias("โœ…", "yes check done"),
276        alias("โŒ", "no cross x"),
277        alias("๐Ÿ‘€", "eyes look watching"),
278        alias("๐Ÿ’ฏ", "hundred perfect"),
279        alias("๐Ÿ˜ฎ", "wow surprised shock shocked open mouth"),
280        alias("๐Ÿ˜ฏ", "wow surprised hushed"),
281        alias("๐Ÿ˜ฒ", "wow surprised astonished shock"),
282        alias("๐Ÿคฏ", "mind blown shock wow"),
283        alias("๐Ÿ˜ฑ", "scream shock shocked wow"),
284        alias("๐Ÿค”", "thinking hmm think"),
285        alias("๐Ÿคข", "sick gross nauseated"),
286        alias("๐Ÿคฎ", "sick gross vomit puke"),
287        alias("๐Ÿฅฑ", "yawn yawning tired bored"),
288        alias("๐Ÿ˜ด", "sleep sleeping tired"),
289        alias("๐Ÿค“", "nerd nerdy glasses"),
290        alias("๐Ÿง", "monocle inspect curious"),
291        alias("๐Ÿ’€", "skull dead"),
292        alias("๐Ÿ‘ป", "ghost spooky"),
293        alias("๐Ÿคก", "clown"),
294        alias("๐Ÿ‘ฝ", "alien"),
295        alias("๐Ÿค–", "robot bot"),
296        alias("๐Ÿ’ฉ", "poo poop"),
297        alias("๐Ÿ˜ˆ", "devil smiling devil"),
298        alias("๐Ÿ‘", "thumbs up like yes"),
299        alias("๐Ÿ‘Ž", "thumbs down dislike no"),
300        alias("๐Ÿฅบ", "pleading puppy eyes"),
301    ];
302    // Shortcode names come first so native composers can show one stable label.
303    // Keep natural-language search words after them for the existing pickers.
304    include_str!("../assets/emoji-shortcodes.tsv")
305        .lines()
306        .filter_map(|line| line.split_once('\t'))
307        .map(|(emoji, shortcodes)| {
308            let extra = search_aliases.iter().find(|entry| entry.emoji == emoji);
309            let keywords = match extra {
310                Some(entry) => format!("{shortcodes} {}", entry.keywords),
311                None => shortcodes.to_string(),
312            };
313            EmojiAliasSnapshot {
314                emoji: emoji.to_string(),
315                keywords,
316            }
317        })
318        .collect()
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn catalog_contains_wow_emoji() {
327        let catalog = iris_emoji_catalog();
328        let smileys = catalog
329            .iter()
330            .find(|c| c.name == "Smileys")
331            .expect("Smileys category");
332        assert!(
333            smileys.emojis.iter().any(|e| e == "๐Ÿ˜ฎ"),
334            "Smileys must include the open-mouth (wow) emoji"
335        );
336    }
337
338    #[test]
339    fn aliases_route_wow_to_open_mouth() {
340        let aliases = iris_emoji_search_aliases();
341        let hit = aliases
342            .iter()
343            .find(|a| a.emoji == "๐Ÿ˜ฎ")
344            .expect("alias entry for open-mouth emoji");
345        assert!(hit.keywords.contains("wow"));
346    }
347
348    #[test]
349    fn every_picker_emoji_has_a_unique_completion_label() {
350        let aliases = iris_emoji_search_aliases();
351        let mut labels = std::collections::HashSet::new();
352        for emoji in iris_emoji_catalog().into_iter().flat_map(|c| c.emojis) {
353            let entry = aliases.iter().find(|entry| entry.emoji == emoji).unwrap();
354            let label = entry.keywords.split_whitespace().next().unwrap();
355            assert!(labels.insert(label), "duplicate shortcode: {label}");
356            assert!(label
357                .chars()
358                .all(|c| c.is_ascii_alphanumeric() || "_+-".contains(c)));
359        }
360        assert_eq!(labels.len(), aliases.len());
361    }
362
363    #[test]
364    fn familiar_shortcodes_keep_the_same_emoji_on_every_platform() {
365        let aliases = iris_emoji_search_aliases();
366        for (emoji, expected) in [
367            ("๐Ÿ˜„", "smile"),
368            ("๐Ÿ˜‚", "joy"),
369            ("โค๏ธ", "heart"),
370            ("๐Ÿ‘", "thumbsup"),
371            ("๐Ÿ”ฅ", "fire"),
372        ] {
373            let entry = aliases.iter().find(|entry| entry.emoji == emoji).unwrap();
374            assert_eq!(entry.keywords.split_whitespace().next(), Some(expected));
375        }
376        assert!(aliases
377            .iter()
378            .find(|entry| entry.emoji == "๐Ÿ‘")
379            .unwrap()
380            .keywords
381            .split_whitespace()
382            .any(|word| word == "+1"));
383    }
384}