From ffbb0ad9e24f8d499ca850061357db030e5ee02a Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Mon, 9 Nov 2026 17:31:19 -0500 Subject: [PATCH] make hovering a reaction show its shortcode --- lib/controllers/emoji_shortcodes.dart | 31 ++++++++++++++++++++ lib/controllers/reactions.dart | 31 +++++++++++++++----- lib/models/reaction_group.dart | 10 +++++++ lib/widgets/reactions/reaction_row.dart | 9 ++++-- lib/widgets/reactions/reactions_popover.dart | 4 +-- 5 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 lib/controllers/emoji_shortcodes.dart create mode 100644 lib/models/reaction_group.dart diff --git a/lib/controllers/emoji_shortcodes.dart b/lib/controllers/emoji_shortcodes.dart new file mode 100644 index 0000000..a24ebb1 --- /dev/null +++ b/lib/controllers/emoji_shortcodes.dart @@ -0,0 +1,31 @@ +import "package:collection/collection.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:material_emoji_picker/material_emoji_picker.dart"; + +/// Maps each unicode emoji, without variation selectors, to its shortcode. +class EmojiShortcodesController extends Notifier> { + static final _variationSelectors = RegExp("[\uFE0E\uFE0F]"); + + static String normalize(String emoji) => + emoji.replaceAll(_variationSelectors, ""); + + @override + IMap build() => .fromEntries( + ref + .watch(EmojiController.provider) + .requireValue + .map((category) => category.emojis) + .flattened + .where((emoji) => emoji.aliases.isNotEmpty) + .map( + (emoji) => + MapEntry(normalize(emoji.value), ":${emoji.aliases.first}:"), + ), + ); + + static final provider = + NotifierProvider>( + EmojiShortcodesController.new, + ); +} diff --git a/lib/controllers/reactions.dart b/lib/controllers/reactions.dart index 0c30128..662661c 100644 --- a/lib/controllers/reactions.dart +++ b/lib/controllers/reactions.dart @@ -1,14 +1,18 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; +import "package:nexus/controllers/emoji_shortcodes.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/configs/event.dart"; import "package:nexus/models/content/reaction.dart"; +import "package:nexus/models/reaction_group.dart"; class ReactionsController(final EventConfig config) - extends AsyncNotifier>> { + extends AsyncNotifier> { @override - Future>> build() async { + Future> build() async { + final unicodeShortcodes = ref.watch(EmojiShortcodesController.provider); + final eventInfo = ref.watch( RoomsController.provider.select((value) { final event = value[config.roomId]?.events[config.eventRowId]; @@ -30,12 +34,25 @@ class ReactionsController(final EventConfig config) return reactionEvents ?.where((event) => event.redactedBy == null) - .fold>>(.new(), (acc, event) { - if (event.content case ReactionContent(:final key?)) { + .fold>(.new(), (acc, event) { + if (event.content case ReactionContent( + :final key?, + :final shortcode, + )) { return acc.update( key, - (list) => list.add(event.sender), - ifAbsent: () => .new([event.sender]), + (group) => group.copyWith( + senders: group.senders.add(event.sender), + shortcode: group.shortcode ?? shortcode, + ), + ifAbsent: () => .new( + senders: .new([event.sender]), + shortcode: + shortcode ?? + unicodeShortcodes[EmojiShortcodesController.normalize( + key, + )], + ), ); } @@ -47,7 +64,7 @@ class ReactionsController(final EventConfig config) static final provider = AsyncNotifierProvider.family< ReactionsController, - IMap>, + IMap, EventConfig >(ReactionsController.new); } diff --git a/lib/models/reaction_group.dart b/lib/models/reaction_group.dart new file mode 100644 index 0000000..b3ea32c --- /dev/null +++ b/lib/models/reaction_group.dart @@ -0,0 +1,10 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; + +part "reaction_group.freezed.dart"; + +@Freezed(toJson: false, fromJson: false) +class const ReactionGroup({ + required final IList senders, + final String? shortcode, +}) with _$ReactionGroup; diff --git a/lib/widgets/reactions/reaction_row.dart b/lib/widgets/reactions/reaction_row.dart index 6020358..f8336c2 100644 --- a/lib/widgets/reactions/reaction_row.dart +++ b/lib/widgets/reactions/reaction_row.dart @@ -7,6 +7,7 @@ import "package:nexus/controllers/reactions.dart"; import "package:nexus/controllers/room_chat.dart"; import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:nexus/models/event.dart"; +import "package:nexus/models/reaction_group.dart"; import "package:nexus/widgets/common/error_dialog.dart"; import "package:nexus/widgets/reactions/reaction_key.dart"; import "package:nexus/widgets/reactions/reactions_popover.dart"; @@ -25,7 +26,7 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget { .new(roomId: event.roomId, eventRowId: event.rowId), ), )) { - AsyncData(value: final IMap>? reactors) || + AsyncData(value: final IMap? reactors) || AsyncLoading(value: final reactors) => Wrap( spacing: 4, runSpacing: 4, @@ -37,7 +38,9 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget { final enabled = useState(true); final selected = - reactors?[reaction]?.contains(clientState!.userId) ?? + reactors?[reaction]?.senders.contains( + clientState!.userId, + ) ?? false; void showReactions(PositionedGestureDetails details) => context.showAdaptivePopover( @@ -52,7 +55,7 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget { onSecondaryTapUp: showReactions, onLongPressStart: showReactions, child: Tooltip( - message: reactors?[reaction]?.join(", ") ?? "", + message: reactors?[reaction]?.shortcode ?? reaction, child: ChoiceChip( showCheckmark: false, selected: selected, diff --git a/lib/widgets/reactions/reactions_popover.dart b/lib/widgets/reactions/reactions_popover.dart index 0c7660b..44c455b 100644 --- a/lib/widgets/reactions/reactions_popover.dart +++ b/lib/widgets/reactions/reactions_popover.dart @@ -81,7 +81,7 @@ class const ReactionsPopover( spacing: 8, children: [ ReactionKey(reaction, height: 20), - Text(reactions[reaction]!.length.toString()), + Text(reactions[reaction]!.senders.length.toString()), ], ), ), @@ -98,7 +98,7 @@ class const ReactionsPopover( padding: .symmetric(vertical: 8), child: Column( crossAxisAlignment: .stretch, - children: reactions[reaction]! + children: reactions[reaction]!.senders .map( (userId) => Consumer( builder: (context, ref, _) {