mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
feat: add reaction support to system events on desktop and mobile (#432)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -489,7 +489,14 @@ class _MessageList extends HookConsumerWidget {
|
||||
if (showDayDivider)
|
||||
DayDivider(label: formatDayHeading(message.createdAt)),
|
||||
if (message.isSystem)
|
||||
_SystemMessageRow(message: message)
|
||||
_SystemMessageRow(
|
||||
message: message,
|
||||
channelId: channelId,
|
||||
currentPubkey: currentPubkey,
|
||||
allMessages: null,
|
||||
isMember: isMember,
|
||||
isArchived: isArchived,
|
||||
)
|
||||
else ...[
|
||||
_MessageBubble(
|
||||
message: message,
|
||||
@@ -549,8 +556,20 @@ class _MessageList extends HookConsumerWidget {
|
||||
|
||||
class _SystemMessageRow extends ConsumerWidget {
|
||||
final TimelineMessage message;
|
||||
final String channelId;
|
||||
final String? currentPubkey;
|
||||
final List<TimelineMessage>? allMessages;
|
||||
final bool isMember;
|
||||
final bool isArchived;
|
||||
|
||||
const _SystemMessageRow({required this.message});
|
||||
const _SystemMessageRow({
|
||||
required this.message,
|
||||
required this.channelId,
|
||||
this.currentPubkey,
|
||||
this.allMessages,
|
||||
this.isMember = false,
|
||||
this.isArchived = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -569,39 +588,66 @@ class _SystemMessageRow extends ConsumerWidget {
|
||||
|
||||
final description = systemEvent.describe(resolveLabel);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: Grid.xxs),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPress: () => showMessageActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
message: message,
|
||||
channelId: channelId,
|
||||
isOwnMessage: false,
|
||||
allMessages: null,
|
||||
currentPubkey: currentPubkey,
|
||||
isMember: isMember,
|
||||
isArchived: isArchived,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: Grid.xxs),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.arrowLeftRight,
|
||||
size: 12,
|
||||
color: context.colors.outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Grid.xxs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
description,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
formatMessageTime(message.createdAt),
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colors.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.arrowLeftRight,
|
||||
size: 12,
|
||||
color: context.colors.outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Grid.xxs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
description,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
if (message.reactions.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 28),
|
||||
child: ReactionRow(
|
||||
reactions: message.reactions,
|
||||
onToggle: (emoji) => toggleReaction(ref, message, emoji),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
formatMessageTime(message.createdAt),
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colors.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -830,21 +876,7 @@ class _MessageBubble extends ConsumerWidget {
|
||||
if (message.reactions.isNotEmpty)
|
||||
ReactionRow(
|
||||
reactions: message.reactions,
|
||||
onToggle: (emoji) {
|
||||
final actions = ref.read(channelActionsProvider);
|
||||
final reaction = message.reactions.firstWhere(
|
||||
(r) => r.emoji == emoji,
|
||||
);
|
||||
if (reaction.reactedByCurrentUser &&
|
||||
reaction.currentUserReactionId != null) {
|
||||
actions.removeReaction(
|
||||
reaction.currentUserReactionId!,
|
||||
emoji,
|
||||
);
|
||||
} else {
|
||||
actions.addReaction(message.id, emoji);
|
||||
}
|
||||
},
|
||||
onToggle: (emoji) => toggleReaction(ref, message, emoji),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -97,7 +97,7 @@ void showMessageActions({
|
||||
],
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
if (allMessages != null)
|
||||
if (allMessages != null && !message.isSystem)
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.messageSquareReply),
|
||||
title: const Text('Reply in thread'),
|
||||
@@ -117,16 +117,17 @@ void showMessageActions({
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.copy),
|
||||
title: const Text('Copy text'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
// Copy to clipboard
|
||||
final data = ClipboardData(text: message.content);
|
||||
Clipboard.setData(data);
|
||||
},
|
||||
),
|
||||
if (!message.isSystem)
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.copy),
|
||||
title: const Text('Copy text'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
// Copy to clipboard
|
||||
final data = ClipboardData(text: message.content);
|
||||
Clipboard.setData(data);
|
||||
},
|
||||
),
|
||||
if (isOwnMessage) ...[
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.pencil),
|
||||
|
||||
@@ -5,8 +5,24 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import '../../shared/theme/theme.dart';
|
||||
import '../profile/user_cache_provider.dart';
|
||||
import '../profile/user_profile.dart';
|
||||
import 'channel_management_provider.dart';
|
||||
import 'timeline_message.dart';
|
||||
|
||||
/// Toggle a reaction on [message]. If the current user already reacted with
|
||||
/// [emoji], removes the reaction; otherwise adds it.
|
||||
///
|
||||
/// Used by channel detail, thread detail, and system message rows to avoid
|
||||
/// duplicating the toggle wiring.
|
||||
void toggleReaction(WidgetRef ref, TimelineMessage message, String emoji) {
|
||||
final actions = ref.read(channelActionsProvider);
|
||||
final reaction = message.reactions.firstWhere((r) => r.emoji == emoji);
|
||||
if (reaction.reactedByCurrentUser && reaction.currentUserReactionId != null) {
|
||||
actions.removeReaction(reaction.currentUserReactionId!, emoji);
|
||||
} else {
|
||||
actions.addReaction(message.id, emoji);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reaction pills row (shared between channel + thread detail pages)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -7,7 +7,6 @@ import '../../shared/widgets/frosted_app_bar.dart';
|
||||
import '../../shared/widgets/frosted_scaffold.dart';
|
||||
import '../profile/user_cache_provider.dart';
|
||||
import '../profile/user_profile.dart';
|
||||
import 'channel_management_provider.dart';
|
||||
import 'channel_messages_provider.dart';
|
||||
import 'channel_typing_provider.dart';
|
||||
import 'channels_provider.dart';
|
||||
@@ -442,21 +441,7 @@ class _ThreadMessage extends ConsumerWidget {
|
||||
if (message.reactions.isNotEmpty)
|
||||
ReactionRow(
|
||||
reactions: message.reactions,
|
||||
onToggle: (emoji) {
|
||||
final actions = ref.read(channelActionsProvider);
|
||||
final reaction = message.reactions.firstWhere(
|
||||
(r) => r.emoji == emoji,
|
||||
);
|
||||
if (reaction.reactedByCurrentUser &&
|
||||
reaction.currentUserReactionId != null) {
|
||||
actions.removeReaction(
|
||||
reaction.currentUserReactionId!,
|
||||
emoji,
|
||||
);
|
||||
} else {
|
||||
actions.addReaction(message.id, emoji);
|
||||
}
|
||||
},
|
||||
onToggle: (emoji) => toggleReaction(ref, message, emoji),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -265,6 +265,23 @@ List<TimelineMessage> formatTimeline(
|
||||
if (event.kind == EventKind.systemMessage) {
|
||||
final systemEvent = SystemEvent.fromContent(event.content);
|
||||
if (systemEvent != null) {
|
||||
final emojiMap = reactionMap[event.id];
|
||||
final reactions = <TimelineReaction>[
|
||||
if (emojiMap != null)
|
||||
for (final entry in emojiMap.entries)
|
||||
TimelineReaction(
|
||||
emoji: entry.key,
|
||||
count: entry.value.length,
|
||||
reactedByCurrentUser:
|
||||
normalizedCurrentPubkey != null &&
|
||||
entry.value.containsKey(normalizedCurrentPubkey),
|
||||
userPubkeys: entry.value.keys.toList(),
|
||||
currentUserReactionId: normalizedCurrentPubkey != null
|
||||
? entry.value[normalizedCurrentPubkey]
|
||||
: null,
|
||||
),
|
||||
];
|
||||
|
||||
result.add(
|
||||
TimelineMessage(
|
||||
id: event.id,
|
||||
@@ -274,6 +291,7 @@ List<TimelineMessage> formatTimeline(
|
||||
tags: event.tags,
|
||||
isSystem: true,
|
||||
systemEvent: systemEvent,
|
||||
reactions: reactions,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user