import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; class MessageBubble extends StatelessWidget { final Event event; final bool isOwn; final Event? replyEvent; const MessageBubble({ super.key, required this.event, required this.isOwn, this.replyEvent, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final bgColor = isOwn ? scheme.primaryContainer : scheme.surfaceContainerHigh; final textColor = isOwn ? scheme.onPrimaryContainer : scheme.onSurface; return Align( alignment: isOwn ? Alignment.centerRight : Alignment.centerLeft, child: ConstrainedBox( constraints: BoxConstraints( maxWidth: MediaQuery.sizeOf(context).width * 0.75, ), child: Container( margin: EdgeInsets.only( left: isOwn ? 48 : 8, right: isOwn ? 8 : 48, top: 2, bottom: 2, ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.only( topLeft: const Radius.circular(16), topRight: const Radius.circular(16), bottomLeft: Radius.circular(isOwn ? 16 : 4), bottomRight: Radius.circular(isOwn ? 4 : 16), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isOwn) ...[ Text( event.senderFromMemoryOrFallback.calcDisplayname(), style: theme.textTheme.labelSmall?.copyWith( color: scheme.primary, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 2), ], if (replyEvent != null) _ReplyPreview(event: replyEvent!), _MessageContent(event: event, textColor: textColor), const SizedBox(height: 2), Align( alignment: Alignment.bottomRight, child: Text( _formatTime(event.originServerTs), style: theme.textTheme.labelSmall?.copyWith( color: textColor.withAlpha(160), fontSize: 10, ), ), ), ], ), ), ), ); } String _formatTime(DateTime t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}'; } class _MessageContent extends StatelessWidget { final Event event; final Color textColor; const _MessageContent({required this.event, required this.textColor}); @override Widget build(BuildContext context) { final body = event.body; return Text( body, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: textColor), ); } } class _ReplyPreview extends StatelessWidget { final Event event; const _ReplyPreview({required this.event}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( border: Border( left: BorderSide(color: theme.colorScheme.primary, width: 3), ), color: theme.colorScheme.surface.withAlpha(120), borderRadius: const BorderRadius.only( topRight: Radius.circular(4), bottomRight: Radius.circular(4), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.senderFromMemoryOrFallback.calcDisplayname(), style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, ), ), Text( event.body, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall, ), ], ), ); } } class DateSeparator extends StatelessWidget { final DateTime date; const DateSeparator({super.key, required this.date}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ const Expanded(child: Divider()), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( _formatDate(date), style: Theme.of(context).textTheme.labelSmall, ), ), const Expanded(child: Divider()), ], ), ); } String _formatDate(DateTime d) { final now = DateTime.now(); if (d.year == now.year && d.month == now.month && d.day == now.day) { return 'Heute'; } 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.toString().padLeft(2, '0')}.${d.month.toString().padLeft(2, '0')}.${d.year}'; } }