implemented dynamic quick reactions by surfacing emojis from message content in the chat options dialog

This commit is contained in:
2026-07-12 16:42:30 +02:00
parent 59501d3b45
commit 3f44e9302f
3 changed files with 133 additions and 12 deletions
@@ -10,6 +10,7 @@ import '../../../../routing/app_routes.dart';
import '../../../../share_intent/remote_file_ref.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../utils/clipboard_helper.dart';
import '../../../../utils/emoji_detection.dart';
import '../../../../widget/app_progress_indicator.dart';
import '../../../../widget/async_action_button.dart';
import '../../../../widget/confirm_dialog.dart';
@@ -20,7 +21,28 @@ import '../../../../widget/emoji_picker_dialog.dart';
import '../../../../widget/emoji_text.dart';
import '../data/open_direct_chat.dart';
const _commonReactions = <String>['👍', '👎', '😆', '❤️', '👀'];
const _commonReactions = <String>['👍', '👎', '❤️', '😆', '😮', '😢'];
/// Upper bound on how many of a message's own emojis we surface as reactions,
/// so an emoji-heavy message doesn't blow up the reaction row.
const _maxMessageReactions = 3;
/// The emojis contained in [bubbleData] itself, offered as extra reactions
/// next to the standard set (most-frequent first).
///
/// Deduped skin-tone-neutrally so a tone variant of an emoji already in the
/// standard set (e.g. 👍🏻 next to 👍) isn't offered a second time, while a
/// message's own tone choice is preserved for emojis not in the set.
List<String> _messageReactionsFor(GetChatResponseObject bubbleData) {
final covered = _commonReactions.map(emojiSkinToneNeutral).toSet();
final result = <String>[];
for (final emoji in extractEmojis(bubbleData.message)) {
if (!covered.add(emojiSkinToneNeutral(emoji))) continue;
result.add(emoji);
if (result.length >= _maxMessageReactions) break;
}
return result;
}
RichObjectString? _attachedFile(GetChatResponseObject bubbleData) {
final file = bubbleData.messageParameters?['file'];
@@ -57,6 +79,8 @@ void showChatMessageOptionsDialog(
_ReactionsRow(
chatToken: chatData.token,
messageId: bubbleData.id,
reactions: _commonReactions,
messageReactions: _messageReactionsFor(bubbleData),
onRefetch: onRefetch,
sheetContext: sheetCtx,
),
@@ -170,12 +194,16 @@ void showChatMessageOptionsDialog(
class _ReactionsRow extends StatefulWidget {
final String chatToken;
final int messageId;
final List<String> reactions;
final List<String> messageReactions;
final void Function({bool renew}) onRefetch;
final BuildContext sheetContext;
const _ReactionsRow({
required this.chatToken,
required this.messageId,
required this.reactions,
required this.messageReactions,
required this.onRefetch,
required this.sheetContext,
});
@@ -220,18 +248,16 @@ class _ReactionsRowState extends State<_ReactionsRow> {
children: [
Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
..._commonReactions.map(
(emoji) => TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
onPressed: busy ? null : () => _react(emoji),
child: EmojiText(emoji, size: EmojiText.sizeLarge),
...widget.reactions.map((emoji) => _emojiButton(emoji, busy)),
if (widget.messageReactions.isNotEmpty) ...[
_groupDivider(context),
...widget.messageReactions.map(
(emoji) => _emojiButton(emoji, busy),
),
),
],
_groupDivider(context),
IconButton(
onPressed: busy ? null : () => _showEmojiPicker(context),
style: IconButton.styleFrom(
@@ -263,6 +289,23 @@ class _ReactionsRowState extends State<_ReactionsRow> {
},
);
Widget _emojiButton(String emoji, bool busy) => TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
onPressed: busy ? null : () => _react(emoji),
child: EmojiText(emoji, size: EmojiText.sizeLarge),
);
Widget _groupDivider(BuildContext context) => Container(
width: 1,
height: 24,
margin: const EdgeInsets.symmetric(horizontal: 6),
color: Theme.of(context).dividerColor,
);
Future<void> _showEmojiPicker(BuildContext rowContext) async {
final emoji = await showEmojiPicker(rowContext, title: 'Reagieren');
if (emoji != null && mounted) await _react(emoji);