feat: better emoji picker
This commit is contained in:
parent
e21c9e8c22
commit
e5f560d03b
11 changed files with 23557 additions and 141 deletions
23479
assets/bundled/emoji.json
Normal file
23479
assets/bundled/emoji.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,84 +1,40 @@
|
||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
import "package:emoji_text_field/models/emoji_category.dart";
|
|
||||||
|
import "package:collection/collection.dart";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:flutter/services.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:http/http.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:nexus/models/emoji.dart";
|
import "package:nexus/models/emoji.dart";
|
||||||
|
import "package:nexus/models/emoji_category.dart";
|
||||||
|
|
||||||
typedef EmojiTuple = (IMap<String, EmojiCategory>, IMap<String, List<String>>);
|
class EmojiController extends AsyncNotifier<IList<EmojiCategory>> {
|
||||||
|
|
||||||
class EmojiController extends AsyncNotifier<EmojiTuple> {
|
|
||||||
@override
|
@override
|
||||||
Future<EmojiTuple> build() async {
|
Future<IList<EmojiCategory>> build() async {
|
||||||
final response = await get(
|
final emojiJson = await rootBundle.loadString("assets/bundled/emoji.json");
|
||||||
.https("github.com", "github/gemoji/raw/refs/heads/master/db/emoji.json"),
|
final decoded = (jsonDecode(emojiJson) as List<dynamic>)
|
||||||
|
.cast<Map<String, Object?>>();
|
||||||
|
|
||||||
|
final entries = decoded.map(
|
||||||
|
(json) =>
|
||||||
|
(category: json["category"]! as String, emoji: Emoji.fromJson(json)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
final grouped = groupBy(entries, (e) => e.category);
|
||||||
throw Exception("Failed to load emoji data");
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = json.decode(response.body);
|
return .new(
|
||||||
|
grouped.entries.map(
|
||||||
final entries = (data as List)
|
(entry) => .new(
|
||||||
.cast<Map<String, dynamic>>()
|
icon: Text(entry.value.first.emoji.emoji),
|
||||||
.map(Emoji.fromJson)
|
|
||||||
.toIList();
|
|
||||||
|
|
||||||
final categoryMap = entries.fold<IMap<String, IList<String>>>(
|
|
||||||
.new(),
|
|
||||||
(acc, entry) => acc.update(
|
|
||||||
entry.category,
|
|
||||||
(list) => list.add(entry.emoji),
|
|
||||||
ifAbsent: () => .new([entry.emoji]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final keywordMap = entries.fold<IMap<String, IList<String>>>(
|
|
||||||
.new(),
|
|
||||||
(acc, entry) => acc.add(
|
|
||||||
entry.emoji,
|
|
||||||
.new([...entry.tags, ...entry.aliases, entry.description]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final customCategories = IMap.fromEntries(
|
|
||||||
categoryMap.entries.map(
|
|
||||||
(entry) => MapEntry(
|
|
||||||
entry.key,
|
|
||||||
EmojiCategory(
|
|
||||||
name: entry.key,
|
name: entry.key,
|
||||||
icon: switch (entry.key) {
|
emojis: .new(entry.value.map((e) => e.emoji)),
|
||||||
"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(
|
|
||||||
.fromEntries(
|
|
||||||
keywordMap.entries.map(
|
|
||||||
(e) => .new(e.key, e.value.toList(growable: false)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (customCategories, customKeywords);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider<EmojiController, EmojiTuple>(
|
static final provider =
|
||||||
|
AsyncNotifierProvider<EmojiController, IList<EmojiCategory>>(
|
||||||
EmojiController.new,
|
EmojiController.new,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ part "emoji.g.dart";
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Emoji({
|
class Emoji({
|
||||||
required final String emoji,
|
required final String emoji,
|
||||||
required final String category,
|
|
||||||
required final IList<String> aliases,
|
required final IList<String> aliases,
|
||||||
required final String description,
|
required final String description,
|
||||||
required final IList<String> tags,
|
required final IList<String> tags,
|
||||||
|
|
|
||||||
12
lib/models/emoji_category.dart
Normal file
12
lib/models/emoji_category.dart
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
import "package:nexus/models/emoji.dart";
|
||||||
|
import "package:flutter/widgets.dart";
|
||||||
|
part "emoji_category.freezed.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
class EmojiCategory({
|
||||||
|
required final Widget icon,
|
||||||
|
required final String name,
|
||||||
|
required final IList<Emoji> emojis,
|
||||||
|
}) with _$EmojiCategory;
|
||||||
|
|
@ -15,7 +15,6 @@ import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/widgets/composer/mention_overlay.dart";
|
import "package:nexus/widgets/composer/mention_overlay.dart";
|
||||||
import "package:nexus/widgets/composer/relation_preview.dart";
|
import "package:nexus/widgets/composer/relation_preview.dart";
|
||||||
import "package:nexus/widgets/emoji_picker_button.dart";
|
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
||||||
class const Composer(
|
class const Composer(
|
||||||
|
|
@ -119,10 +118,11 @@ class const Composer(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
? [
|
? [
|
||||||
EmojiPickerButton(
|
IconButton(
|
||||||
context: context,
|
onPressed: () {
|
||||||
onSelection: (_) => node?.requestFocus(),
|
//TODO onSelection: (_) => node?.requestFocus(),
|
||||||
controller: controller.value,
|
},
|
||||||
|
icon: Icon(Icons.emoji_emotions),
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
tooltip: "Add media",
|
tooltip: "Add media",
|
||||||
|
|
|
||||||
8
lib/widgets/emoji_picker.dart
Normal file
8
lib/widgets/emoji_picker.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import "package:flutter/widgets.dart";
|
||||||
|
|
||||||
|
class EmojiPicker extends StatelessWidget {
|
||||||
|
const new({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Placeholder(); // TODO: Emoji picker
|
||||||
|
}
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import "package:emoji_text_field/emoji_text_field.dart";
|
|
||||||
import "package:material_ui/material_ui.dart";
|
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
|
||||||
import "package:nexus/controllers/emoji.dart";
|
|
||||||
|
|
||||||
final class const EmojiPickerButton({
|
|
||||||
required final BuildContext context,
|
|
||||||
final TextEditingController? controller,
|
|
||||||
final void Function(String emoji)? onSelection,
|
|
||||||
final VoidCallback? onPressed,
|
|
||||||
super.key,
|
|
||||||
}) extends HookConsumerWidget {
|
|
||||||
@override
|
|
||||||
Widget build(_, WidgetRef ref) => IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
onPressed?.call();
|
|
||||||
final controller = this.controller ?? .new();
|
|
||||||
|
|
||||||
final emojis = await ref.watch(EmojiController.provider.future);
|
|
||||||
if (context.mounted) {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => EmojiKeyboardView(
|
|
||||||
config: .new(
|
|
||||||
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
|
|
||||||
// It might be preferable to use a microtask instead of a `Future.delayed`.
|
|
||||||
Future.delayed(.zero, () {
|
|
||||||
if (context.mounted) Navigator.of(context).pop();
|
|
||||||
});
|
|
||||||
onSelection?.call(controller.text);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.emoji_emotions),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -17,7 +17,6 @@ import "package:nexus/models/content/message.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/widgets/composer/composer.dart";
|
import "package:nexus/widgets/composer/composer.dart";
|
||||||
import "package:nexus/widgets/emoji_picker_button.dart";
|
|
||||||
import "package:nexus/widgets/pinned_events_drawer.dart";
|
import "package:nexus/widgets/pinned_events_drawer.dart";
|
||||||
import "package:nexus/widgets/renderers/event.dart";
|
import "package:nexus/widgets/renderers/event.dart";
|
||||||
import "package:nexus/widgets/member_list.dart";
|
import "package:nexus/widgets/member_list.dart";
|
||||||
|
|
@ -234,11 +233,16 @@ final class const RoomChat({
|
||||||
icon: Text(emoji),
|
icon: Text(emoji),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
EmojiPickerButton(
|
IconButton(
|
||||||
context: context,
|
icon: Icon(Icons.emoji_emotions),
|
||||||
onPressed: Navigator.of(context).pop,
|
|
||||||
onSelection: (emoji) =>
|
onPressed: () {
|
||||||
notifier.sendReaction(emoji, event).onError(showError),
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// TODO: Push
|
||||||
|
// onSelection: (emoji) =>
|
||||||
|
// notifier.sendReaction(emoji, event).onError(showError),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -297,15 +297,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.8"
|
version: "0.0.8"
|
||||||
emoji_text_field:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
path: "."
|
|
||||||
ref: HEAD
|
|
||||||
resolved-ref: "5f7baaf8a6f059ec3ab8ff0f5d02339b00bf6997"
|
|
||||||
url: "https://github.com/Henry-Hiles/emoji_text_field"
|
|
||||||
source: git
|
|
||||||
version: "1.0.0"
|
|
||||||
encrypt:
|
encrypt:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,6 @@ dependencies:
|
||||||
http: 1.6.0
|
http: 1.6.0
|
||||||
flutter_linkify: 6.0.0
|
flutter_linkify: 6.0.0
|
||||||
linkify: 5.0.0
|
linkify: 5.0.0
|
||||||
emoji_text_field:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Henry-Hiles/emoji_text_field
|
|
||||||
flutter_blurhash: 0.9.1
|
flutter_blurhash: 0.9.1
|
||||||
super_sliver_list: 0.4.1
|
super_sliver_list: 0.4.1
|
||||||
media_kit: 1.2.6
|
media_kit: 1.2.6
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,29 @@
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
|
|
||||||
import "package:ffigen/ffigen.dart";
|
import "package:ffigen/ffigen.dart";
|
||||||
|
import "package:http/http.dart";
|
||||||
import "package:path/path.dart";
|
import "package:path/path.dart";
|
||||||
import "package:nexus/helpers/extensions/get_xcode_sdk.dart";
|
import "package:nexus/helpers/extensions/get_xcode_sdk.dart";
|
||||||
|
|
||||||
void main(List<String> args) async {
|
void main(List<String> args) async {
|
||||||
|
print("Downloading emoji list...");
|
||||||
|
|
||||||
|
final response = await get(
|
||||||
|
.https("github.com", "github/gemoji/raw/refs/heads/master/db/emoji.json"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("Failed to load emoji data");
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.fromUri(Platform.script.resolve("../assets/bundled/emoji.json"))
|
||||||
|
.writeAsString(response.body);
|
||||||
|
|
||||||
|
print("Emoji list written!");
|
||||||
|
|
||||||
final repoDir = Directory.fromUri(Platform.script.resolve("../gomuks"));
|
final repoDir = Directory.fromUri(Platform.script.resolve("../gomuks"));
|
||||||
|
|
||||||
print("Generating FFI Bindings...");
|
print("Generating FFI bindings...");
|
||||||
|
|
||||||
final libclangPath = Platform.environment["LIBCLANG_PATH"];
|
final libclangPath = Platform.environment["LIBCLANG_PATH"];
|
||||||
FfiGenerator(
|
FfiGenerator(
|
||||||
|
|
@ -40,5 +56,5 @@ void main(List<String> args) async {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
print("Done!");
|
print("FFI bindings generated!");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue