part of '../message_group.dart'; class _Avatar extends ConsumerStatefulWidget { final String initial; final Color color; final PyramidTheme pt; final double size; final Uri? avatarUri; const _Avatar({ required this.initial, required this.color, required this.pt, required this.size, this.avatarUri, }); @override ConsumerState<_Avatar> createState() => _AvatarState(); } class _AvatarState extends ConsumerState<_Avatar> { bool _hovered = false; @override Widget build(BuildContext context) { final client = ref.watch(matrixClientProvider).valueOrNull; final radius = BorderRadius.circular(widget.size / 2); Widget child; if (client != null && widget.avatarUri != null) { child = MxcAvatar( mxcUri: widget.avatarUri, client: client, size: widget.size, borderRadius: radius, placeholder: (_) => _InitialBox( initial: widget.initial, color: widget.color, size: widget.size, radius: radius, ), ); } else { child = _InitialBox( initial: widget.initial, color: widget.color, size: widget.size, radius: radius, ); } return MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: AnimatedContainer( duration: const Duration(milliseconds: 180), curve: Curves.easeOutBack, transform: _hovered ? Matrix4.diagonal3Values(1.06, 1.06, 1.0) : Matrix4.identity(), transformAlignment: Alignment.center, width: widget.size, height: widget.size, child: child, ), ); } } class _InitialBox extends StatelessWidget { final String initial; final Color color; final double size; final BorderRadius radius; const _InitialBox({required this.initial, required this.color, required this.size, required this.radius}); @override Widget build(BuildContext context) { return Container( width: size, height: size, decoration: BoxDecoration(color: color, borderRadius: radius), child: Center( child: Text(initial, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)), ), ); } } // Inline "Neue Nachrichten" divider rendered inside a MessageGroup when the // fullyRead event is in the middle of the group (same sender, <5 min window). class _InlineUnreadDivider extends StatelessWidget { final PyramidTheme pt; const _InlineUnreadDivider({required this.pt}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(0, 8, 0, 4), child: Row( children: [ Expanded(child: Divider(color: pt.accent.withAlpha(160), height: 1)), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Text( 'Neue Nachrichten', style: TextStyle(color: pt.accent, fontSize: 11, fontWeight: FontWeight.w600), ), ), Expanded(child: Divider(color: pt.accent.withAlpha(160), height: 1)), ], ), ); } } // Date divider between message groups class DateDivider extends StatelessWidget { final DateTime date; const DateDivider({super.key, required this.date}); @override Widget build(BuildContext context) { final pt = PyramidTheme.of(context); return Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), child: Row( children: [ Expanded(child: Divider(color: pt.border, height: 1)), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( _formatDate(date), style: TextStyle( color: pt.fgDim, fontSize: 11, fontWeight: FontWeight.w600, letterSpacing: 0.06 * 11, ), ), ), Expanded(child: Divider(color: pt.border, height: 1)), ], ), ); } String _formatDate(DateTime d) { final now = DateTime.now(); if (d.year == now.year && d.month == now.month && d.day == now.day) { return 'Heute · ${d.day}. ${_monthName(d.month)}'; } final yesterday = now.subtract(const Duration(days: 1)); if (d.year == yesterday.year && d.month == yesterday.month && d.day == yesterday.day) { return 'Gestern'; } return '${d.day}. ${_monthName(d.month)} ${d.year}'; } String _monthName(int m) => const [ '', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' ][m]; } class _MessageStatus extends StatelessWidget { final Event lastEvent; final PyramidTheme pt; const _MessageStatus({required this.lastEvent, required this.pt}); @override Widget build(BuildContext context) { final status = lastEvent.status; if (status == EventStatus.error) { return Icon(Icons.error_outline_rounded, size: 12, color: pt.danger); } if (status == EventStatus.sending) { return Icon(Icons.access_time_rounded, size: 12, color: pt.fgDim); } // Sent (delivered). "Read" is shown by the per-event ✓✓ marker. return Icon(Icons.done_rounded, size: 13, color: pt.fgDim); } } // Read receipt: a ✓✓ shown at the partner's last-read message (any sender), // so it moves next to their own message when they reply. class _ReadMarker extends StatelessWidget { final PyramidTheme pt; const _ReadMarker({required this.pt}); // Goldener Doppelhaken — bewusst nicht die Akzentfarbe, damit "gelesen" // unabhängig vom Theme sofort erkennbar ist. static const _gold = Color(0xFFE6B422); @override Widget build(BuildContext context) { return const Padding( padding: EdgeInsets.only(right: 24, top: 1, bottom: 1), child: Align( alignment: Alignment.centerRight, child: Icon(Icons.done_all_rounded, size: 13, color: _gold), ), ); } }