part of '../message_group.dart'; class _ReactionsRow extends StatelessWidget { final Event event; final Timeline timeline; final Room? room; final String currentUserId; final PyramidTheme pt; const _ReactionsRow({ required this.event, required this.timeline, required this.currentUserId, required this.pt, this.room, }); @override Widget build(BuildContext context) { final reactionEvents = event.aggregatedEvents(timeline, RelationshipTypes.reaction); if (reactionEvents.isEmpty) return const SizedBox.shrink(); // Group by emoji key → list of senders final Map> grouped = {}; for (final r in reactionEvents) { final key = r.content.tryGetMap('m.relates_to')?['key'] as String?; if (key == null || key.isEmpty) continue; (grouped[key] ??= []).add(r.senderId); } if (grouped.isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(top: 4, bottom: 2), child: Wrap( spacing: 4, runSpacing: 4, children: grouped.entries.map((e) { final myReaction = e.value.contains(currentUserId); return _ReactionChip( emoji: e.key, count: e.value.length, mine: myReaction, pt: pt, onTap: () { if (myReaction) { // Find own reaction event to redact for (final r in reactionEvents) { final key = r.content.tryGetMap('m.relates_to')?['key'] as String?; if (key == e.key && r.senderId == currentUserId) { r.redactEvent().catchError((_) => null); break; } } } else { room?.sendReaction(event.eventId, e.key).catchError((_) => ''); ReactionService.recordUsed(e.key); } }, ); }).toList(), ), ); } } class _ReactionChip extends StatefulWidget { final String emoji; final int count; final bool mine; final PyramidTheme pt; final VoidCallback onTap; const _ReactionChip({ required this.emoji, required this.count, required this.mine, required this.pt, required this.onTap, }); @override State<_ReactionChip> createState() => _ReactionChipState(); } class _ReactionChipState extends State<_ReactionChip> { bool _hovered = false; @override Widget build(BuildContext context) { final pt = widget.pt; return MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: GestureDetector( onTap: widget.onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 120), padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: widget.mine ? pt.accentSoft : (_hovered ? pt.bgHover : pt.bg2), borderRadius: BorderRadius.circular(pt.rBase), border: Border.all( color: widget.mine ? pt.accent : (_hovered ? pt.borderStrong : pt.border), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(widget.emoji, style: const TextStyle(fontSize: 13)), const SizedBox(width: 4), Text( '${widget.count}', style: TextStyle( color: widget.mine ? pt.accent : pt.fgMuted, fontSize: 12, fontWeight: FontWeight.w500, ), ), ], ), ), ), ); } } class _QuickReactionBtn extends StatefulWidget { final String emoji; final PyramidTheme pt; final VoidCallback? onTap; const _QuickReactionBtn({required this.emoji, required this.pt, this.onTap}); @override State<_QuickReactionBtn> createState() => _QuickReactionBtnState(); } class _QuickReactionBtnState extends State<_QuickReactionBtn> { bool _hovered = false; @override Widget build(BuildContext context) { return MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: GestureDetector( onTap: widget.onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 100), width: 28, height: 28, decoration: BoxDecoration( color: _hovered ? widget.pt.bgHover : Colors.transparent, borderRadius: BorderRadius.circular(widget.pt.rSm), ), child: Center( child: Text(widget.emoji, style: const TextStyle(fontSize: 15)), ), ), ), ); } }