import 'dart:convert'; import 'package:flutter/material.dart'; import '../services/theme_service.dart'; import '../models/message.dart'; class MessageBubble extends StatelessWidget { final Message message; final bool showAuthor; final void Function(Message)? onReply; const MessageBubble({ super.key, required this.message, this.showAuthor = true, this.onReply, }); @override Widget build(BuildContext context) { final text = message.content != null ? utf8.decode(message.content!) : ''; return Padding( padding: const EdgeInsets.only(top: 2, bottom: 2), child: InkWell( onLongPress: () => _showContextMenu(context), borderRadius: BorderRadius.circular(4), child: Padding( padding: EdgeInsets.only(left: showAuthor ? 0 : 48), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (showAuthor) ...[ const SizedBox(height: 12), Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: ThemeService.primary.withOpacity(0.8), borderRadius: BorderRadius.circular(16), ), child: Center( child: Text( (message.authorUsername ?? '?')[0].toUpperCase(), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14, ), ), ), ), const SizedBox(width: 8), Text( message.authorUsername ?? 'Unknown', style: const TextStyle( color: ThemeService.textPrimary, fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 8), Text( _formatTime(message.createdAt), style: const TextStyle( color: ThemeService.textMuted, fontSize: 11, ), ), ], ), const SizedBox(height: 4), ], Padding( padding: const EdgeInsets.only(left: 40), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (message.replyTo != null) Container( padding: const EdgeInsets.all(6), margin: const EdgeInsets.only(bottom: 4), decoration: BoxDecoration( color: ThemeService.surfaceTertiary, borderRadius: BorderRadius.circular(4), border: Border( left: BorderSide( color: ThemeService.primary.withOpacity(0.5), width: 3, ), ), ), child: Text( 'Replying to a message', style: const TextStyle( color: ThemeService.textMuted, fontSize: 12, ), ), ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: message.messageType == 'voice' ? _buildVoiceMessage() : message.messageType == 'image' ? _buildImageMessage(text) : Text( text, style: const TextStyle( color: ThemeService.textPrimary, fontSize: 15, height: 1.4, ), ), ), ], ), if (message.editedAt != null) Text( '(edited)', style: const TextStyle( color: ThemeService.textMuted, fontSize: 11, ), ), ], ), ), ], ), ), ), ); } Widget _buildVoiceMessage() { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: ThemeService.surfaceTertiary, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.play_arrow, color: ThemeService.primary, size: 24), const SizedBox(width: 8), Container( width: 120, height: 4, decoration: BoxDecoration( color: ThemeService.textMuted.withOpacity(0.3), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), const Text( '0:12', style: TextStyle( color: ThemeService.textMuted, fontSize: 12, ), ), ], ), ); } Widget _buildImageMessage(String url) { return ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( url, width: 300, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container( height: 100, color: ThemeService.surfaceTertiary, child: const Center( child: Icon(Icons.broken_image, color: ThemeService.textMuted), ), ), loadingBuilder: (_, child, progress) { if (progress == null) return child; return Container( width: 300, height: 200, color: ThemeService.surfaceTertiary, child: Center( child: CircularProgressIndicator( value: progress.expectedTotalBytes != null ? progress.cumulativeBytesLoaded / progress.expectedTotalBytes! : null, strokeWidth: 2, ), ), ); }, ), ); } void _showContextMenu(BuildContext context) { showModalBottomSheet( context: context, backgroundColor: ThemeService.surfaceSecondary, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), ), builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.reply, color: ThemeService.textSecondary), title: const Text('Reply', style: TextStyle(color: ThemeService.textPrimary)), onTap: () { Navigator.pop(ctx); onReply?.call(message); }, ), ListTile( leading: const Icon(Icons.copy, color: ThemeService.textSecondary), title: const Text('Copy', style: TextStyle(color: ThemeService.textPrimary)), onTap: () { Navigator.pop(ctx); }, ), ListTile( leading: const Icon(Icons.push_pin, color: ThemeService.textSecondary), title: const Text('Pin', style: TextStyle(color: ThemeService.textPrimary)), onTap: () => Navigator.pop(ctx), ), ListTile( leading: const Icon(Icons.delete, color: ThemeService.danger), title: const Text('Delete', style: TextStyle(color: ThemeService.danger)), onTap: () => Navigator.pop(ctx), ), ], ), ), ); } String _formatTime(DateTime dt) { final now = DateTime.now(); final diff = now.difference(dt); if (diff.inDays == 0) { return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } else if (diff.inDays == 1) { return 'Yesterday'; } else { return '${dt.day}/${dt.month}/${dt.year}'; } } }