import 'package:flutter/foundation.dart'; /// The standard (Unicode) emoji set, projected from the same emoji-mart dataset /// desktop uses. /// /// The asset at `assets/emoji/emoji-data.json` is generated by /// `just mobile-emoji-data`; ids here are emoji-mart ids, which are exactly the /// `:shortcode:` values desktop emits from its picker and resolves in /// `emojiDisplayName`. Keeping both clients on one dataset is what stops a /// shortcode from meaning different things on mobile and desktop. @immutable class EmojiEntry { /// emoji-mart id, i.e. the shortcode without the surrounding colons. final String id; /// Human-readable name, e.g. `Index Pointing Up`. final String name; /// Search keywords from the dataset. final List keywords; /// The default-skin glyph. final String native; /// Position within emoji-mart's skin list. The default skin is zero. final int skinIndex; /// Owning category id, e.g. `people`. final String categoryId; const EmojiEntry({ required this.id, required this.name, required this.keywords, required this.native, required this.categoryId, this.skinIndex = 0, }); /// Unique tile id while preserving the desktop-identical shortcode for the /// default skin. String get tileId => skinIndex == 0 ? id : '$id-$skinIndex'; } /// One dataset category, in emoji-mart's own order. @immutable class EmojiCategory { final String id; final List emoji; const EmojiCategory({required this.id, required this.emoji}); /// Title-cased label for the category rail. String get label => switch (id) { 'people' => 'Smileys & People', 'nature' => 'Animals & Nature', 'foods' => 'Food & Drink', 'activity' => 'Activity', 'places' => 'Travel & Places', 'objects' => 'Objects', 'symbols' => 'Symbols', 'flags' => 'Flags', _ => id, }; } /// Parsed emoji dataset: ordered categories, a flat list for search, and a /// reverse glyph index for resolving a reaction back to its shortcode. @immutable class EmojiDataset { final List categories; /// Every entry, in category order. Search scans this. final List all; /// Glyph to `:shortcode:`. Mirrors desktop's `emojiDisplayName`. final Map nativeToShortcode; const EmojiDataset({ required this.categories, required this.all, required this.nativeToShortcode, }); static const empty = EmojiDataset( categories: [], all: [], nativeToShortcode: {}, ); bool get isEmpty => all.isEmpty; /// Resolve a reaction value to something displayable as a name: a custom /// emoji's `:shortcode:` passes through, a Unicode glyph maps to its /// shortcode, and anything unknown falls back to itself. String displayName(String emoji) { final trimmed = emoji.trim(); if (trimmed.startsWith(':') && trimmed.endsWith(':')) return trimmed; return nativeToShortcode[trimmed] ?? trimmed; } /// Parse the generated asset. Runs on a background isolate via `compute`, so /// it must stay a top-level-callable pure function over plain JSON. static EmojiDataset fromJson(Map json) { final rawEmoji = (json['emoji'] as Map).cast(); final rawCategories = (json['categories'] as List).cast(); final categories = []; final all = []; final nativeToShortcode = {}; for (final rawCategory in rawCategories) { final category = (rawCategory as Map).cast(); final categoryId = category['id'] as String; final entries = []; for (final rawId in (category['emoji'] as List).cast()) { final id = rawId as String; final record = (rawEmoji[id] as Map?)?.cast(); if (record == null) continue; // Older committed assets used one string; accept that shape so the // parser remains safe while generated assets move to all skin variants. final natives = switch (record['u']) { final List values => values.cast(), final String value => [value], _ => const [], }; for (final (skinIndex, native) in natives.indexed) { final entry = EmojiEntry( id: id, name: record['n'] as String, keywords: (record['k'] as List).cast(), native: native, categoryId: categoryId, skinIndex: skinIndex, ); entries.add(entry); all.add(entry); // First writer wins so the earliest category owns a shared glyph, // matching desktop's map build order. nativeToShortcode.putIfAbsent(entry.native, () => ':$id:'); } } categories.add(EmojiCategory(id: categoryId, emoji: entries)); } return EmojiDataset( categories: categories, all: all, nativeToShortcode: nativeToShortcode, ); } }