make hovering a reaction show its shortcode

This commit is contained in:
Henry Hiles 2026-11-09 17:31:19 -05:00
commit ffbb0ad9e2
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
5 changed files with 73 additions and 12 deletions

View file

@ -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<IMap<String, String>> {
static final _variationSelectors = RegExp("[\uFE0E\uFE0F]");
static String normalize(String emoji) =>
emoji.replaceAll(_variationSelectors, "");
@override
IMap<String, String> 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, IMap<String, String>>(
EmojiShortcodesController.new,
);
}

View file

@ -1,14 +1,18 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/emoji_shortcodes.dart";
import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/rooms.dart";
import "package:nexus/models/configs/event.dart"; import "package:nexus/models/configs/event.dart";
import "package:nexus/models/content/reaction.dart"; import "package:nexus/models/content/reaction.dart";
import "package:nexus/models/reaction_group.dart";
class ReactionsController(final EventConfig config) class ReactionsController(final EventConfig config)
extends AsyncNotifier<IMap<String, IList<String>>> { extends AsyncNotifier<IMap<String, ReactionGroup>> {
@override @override
Future<IMap<String, IList<String>>> build() async { Future<IMap<String, ReactionGroup>> build() async {
final unicodeShortcodes = ref.watch(EmojiShortcodesController.provider);
final eventInfo = ref.watch( final eventInfo = ref.watch(
RoomsController.provider.select((value) { RoomsController.provider.select((value) {
final event = value[config.roomId]?.events[config.eventRowId]; final event = value[config.roomId]?.events[config.eventRowId];
@ -30,12 +34,25 @@ class ReactionsController(final EventConfig config)
return reactionEvents return reactionEvents
?.where((event) => event.redactedBy == null) ?.where((event) => event.redactedBy == null)
.fold<IMap<String, IList<String>>>(.new(), (acc, event) { .fold<IMap<String, ReactionGroup>>(.new(), (acc, event) {
if (event.content case ReactionContent(:final key?)) { if (event.content case ReactionContent(
:final key?,
:final shortcode,
)) {
return acc.update( return acc.update(
key, key,
(list) => list.add(event.sender), (group) => group.copyWith(
ifAbsent: () => .new([event.sender]), 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 = static final provider =
AsyncNotifierProvider.family< AsyncNotifierProvider.family<
ReactionsController, ReactionsController,
IMap<String, IList<String>>, IMap<String, ReactionGroup>,
EventConfig EventConfig
>(ReactionsController.new); >(ReactionsController.new);
} }

View file

@ -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<String> senders,
final String? shortcode,
}) with _$ReactionGroup;

View file

@ -7,6 +7,7 @@ import "package:nexus/controllers/reactions.dart";
import "package:nexus/controllers/room_chat.dart"; import "package:nexus/controllers/room_chat.dart";
import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
import "package:nexus/models/event.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/common/error_dialog.dart";
import "package:nexus/widgets/reactions/reaction_key.dart"; import "package:nexus/widgets/reactions/reaction_key.dart";
import "package:nexus/widgets/reactions/reactions_popover.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), .new(roomId: event.roomId, eventRowId: event.rowId),
), ),
)) { )) {
AsyncData(value: final IMap<String, IList<String>>? reactors) || AsyncData(value: final IMap<String, ReactionGroup>? reactors) ||
AsyncLoading(value: final reactors) => Wrap( AsyncLoading(value: final reactors) => Wrap(
spacing: 4, spacing: 4,
runSpacing: 4, runSpacing: 4,
@ -37,7 +38,9 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
final enabled = useState(true); final enabled = useState(true);
final selected = final selected =
reactors?[reaction]?.contains(clientState!.userId) ?? reactors?[reaction]?.senders.contains(
clientState!.userId,
) ??
false; false;
void showReactions(PositionedGestureDetails details) => void showReactions(PositionedGestureDetails details) =>
context.showAdaptivePopover( context.showAdaptivePopover(
@ -52,7 +55,7 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
onSecondaryTapUp: showReactions, onSecondaryTapUp: showReactions,
onLongPressStart: showReactions, onLongPressStart: showReactions,
child: Tooltip( child: Tooltip(
message: reactors?[reaction]?.join(", ") ?? "", message: reactors?[reaction]?.shortcode ?? reaction,
child: ChoiceChip( child: ChoiceChip(
showCheckmark: false, showCheckmark: false,
selected: selected, selected: selected,

View file

@ -81,7 +81,7 @@ class const ReactionsPopover(
spacing: 8, spacing: 8,
children: [ children: [
ReactionKey(reaction, height: 20), 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), padding: .symmetric(vertical: 8),
child: Column( child: Column(
crossAxisAlignment: .stretch, crossAxisAlignment: .stretch,
children: reactions[reaction]! children: reactions[reaction]!.senders
.map( .map(
(userId) => Consumer( (userId) => Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {