5bf45a9366
Gleiche Technik wie bei settings_modal.dart: reine Verschiebung per Dart part/part-of, keine Logikänderung. message_group.dart schrumpft von 2775 auf 30 Zeilen (nur noch Bibliothekskopf). Verifiziert per automatisiertem Blockvergleich gegen das Original und flutter analyze (Baseline unverändert). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
168 lines
4.9 KiB
Dart
168 lines
4.9 KiB
Dart
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<String, List<String>> grouped = {};
|
|
for (final r in reactionEvents) {
|
|
final key = r.content.tryGetMap<String, dynamic>('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<String, dynamic>('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)),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|