diff --git a/lib/controllers/emoji_controller.dart b/lib/controllers/emoji_controller.dart new file mode 100644 index 0000000..358f98b --- /dev/null +++ b/lib/controllers/emoji_controller.dart @@ -0,0 +1,88 @@ +import "dart:convert"; +import "package:emoji_text_field/models/emoji_category.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:http/http.dart"; +import "package:nexus/models/emoji.dart"; + +typedef EmojiTuple = (IMap, IMap>); + +class EmojiController extends AsyncNotifier { + @override + Future build() async { + final response = await get( + Uri.https( + "github.com", + "github/gemoji/raw/refs/heads/master/db/emoji.json", + ), + ); + + if (response.statusCode != 200) { + throw Exception("Failed to load emoji data"); + } + + final data = json.decode(response.body); + + final entries = (data as List) + .cast>() + .map(Emoji.fromJson) + .toIList(); + + final categoryMap = entries.fold>>( + const IMap.empty(), + (acc, entry) => acc.update( + entry.category, + (list) => list.add(entry.emoji), + ifAbsent: () => IList([entry.emoji]), + ), + ); + + final keywordMap = entries.fold>>( + const IMap.empty(), + (acc, entry) => acc.add( + entry.emoji, + IList([...entry.tags, ...entry.aliases, entry.description]), + ), + ); + + final customCategories = IMap.fromEntries( + categoryMap.entries.map( + (entry) => MapEntry( + entry.key, + EmojiCategory( + name: entry.key, + icon: switch (entry.key) { + "Smileys & Emotion" => Icons.emoji_emotions, + "People & Body" => Icons.emoji_people, + "Animals & Nature" => Icons.emoji_nature, + "Food & Drink" => Icons.emoji_food_beverage, + "Travel & Places" => Icons.travel_explore, + "Activities" => Icons.sports_soccer, + "Objects" => Icons.emoji_objects, + "Symbols" => Icons.emoji_symbols, + "Flags" => Icons.emoji_flags, + _ => Icons.category, + }, + emojis: entry.value.toList(growable: false), + ), + ), + ), + ); + + final customKeywords = IMap( + Map.fromEntries( + keywordMap.entries.map( + (e) => MapEntry(e.key, e.value.toList(growable: false)), + ), + ), + ); + + return (customCategories, customKeywords); + } + + static final provider = + AsyncNotifierProvider.autoDispose( + EmojiController.new, + ); +} diff --git a/lib/models/emoji.dart b/lib/models/emoji.dart new file mode 100644 index 0000000..8e4eac6 --- /dev/null +++ b/lib/models/emoji.dart @@ -0,0 +1,17 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +part "emoji.freezed.dart"; +part "emoji.g.dart"; + +@freezed +abstract class Emoji with _$Emoji { + const factory Emoji({ + required String emoji, + required String category, + required IList aliases, + required String description, + required IList tags, + }) = _Emoji; + + factory Emoji.fromJson(Map json) => _$EmojiFromJson(json); +} diff --git a/lib/widgets/chat_page/emoji_picker_button.dart b/lib/widgets/chat_page/emoji_picker_button.dart index 0c43c48..e8805ca 100644 --- a/lib/widgets/chat_page/emoji_picker_button.dart +++ b/lib/widgets/chat_page/emoji_picker_button.dart @@ -1,8 +1,9 @@ import "package:emoji_text_field/emoji_text_field.dart"; import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/emoji_controller.dart"; -class EmojiPickerButton extends HookWidget { +class EmojiPickerButton extends HookConsumerWidget { final TextEditingController? controller; final void Function(String emoji)? onSelection; final VoidCallback? onPressed; @@ -16,25 +17,34 @@ class EmojiPickerButton extends HookWidget { }); @override - Widget build(_) => IconButton( - onPressed: () { + Widget build(_, WidgetRef ref) => IconButton( + onPressed: () async { onPressed?.call(); final controller = this.controller ?? TextEditingController(); - showModalBottomSheet( - context: context, - builder: (context) => EmojiKeyboardView( - config: EmojiViewConfig( - showRecentTab: false, - backgroundColor: Theme.of(context).colorScheme.surfaceContainer, - height: 600, + + final emojis = await ref.watch(EmojiController.provider.future); + if (context.mounted) { + showModalBottomSheet( + context: context, + builder: (context) => EmojiKeyboardView( + config: EmojiViewConfig( + showRecentTab: false, + customCategories: emojis.$1.unlock, + customKeywords: emojis.$2.unlock, + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + height: 600, + ), + textController: controller + ..addListener(() { + // Without this, there will sometimes be a debugLocked is not true error sometimes + Future.delayed(Duration.zero, () { + if (context.mounted) Navigator.of(context).pop(); + }); + onSelection?.call(controller.text); + }), ), - textController: controller - ..addListener(() async { - Navigator.of(context).pop(); - onSelection?.call(controller.text); - }), - ), - ); + ); + } }, icon: Icon(Icons.emoji_emotions), ); diff --git a/linux/nix/pkg/default.nix b/linux/nix/pkg/default.nix index 26f2a17..adaeb15 100644 --- a/linux/nix/pkg/default.nix +++ b/linux/nix/pkg/default.nix @@ -26,7 +26,7 @@ flutter.buildFlutterApplication { dynamic_system_colors = "sha256-es6rjMK1drkqZBKYUP77yw/q5+0uLwWOEDOXRawy3Dc="; flutter_chat_ui = "sha256-4fuag7lRH5cMBFD3fUzj2K541JwXLoz8HF/4OMr3uhk="; flutter_link_previewer = "sha256-4fuag7lRH5cMBFD3fUzj2K541JwXLoz8HF/4OMr3uhk="; - emoji_text_field = "sha256-F0QbIHP3wpKoL6QbJ20Oun0SsOdwnXe84IqsK2ad85w="; + emoji_text_field = "sha256-3TOys09EP2GRo6pUBGPXaqBlE39O2Cmwt42Hs1cTDKo="; }; postInstall = '' diff --git a/pubspec.lock b/pubspec.lock index ef7fcd9..984341b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -359,7 +359,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "0e90703a6e876939be70bd1816c49cf14474de61" + resolved-ref: "5f7baaf8a6f059ec3ab8ff0f5d02339b00bf6997" url: "https://github.com/Henry-Hiles/emoji_text_field" source: git version: "1.0.0"