import 'package:flutter/material.dart'; import '../models/models.dart'; import '../theme/app_theme.dart'; /// iMessage-like bubble. Twin messages keep a warm label + soft tint (PRD §3.1). class MessageBubble extends StatelessWidget { const MessageBubble({ super.key, required this.message, required this.isMine, this.onRetract, }); final ChatMessage message; final bool isMine; final VoidCallback? onRetract; static String _time(DateTime dt) { final local = dt.toLocal(); final h = local.hour.toString().padLeft(2, '0'); final m = local.minute.toString().padLeft(2, '0'); return '$h:$m'; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final twin = message.isTwin; final retracted = message.retracted; final accent = AppTheme.twinAccent(theme.brightness); final brightness = theme.brightness; final Color bg; final Color fg; if (retracted) { bg = scheme.surfaceContainerHigh; fg = scheme.onSurfaceVariant; } else if (twin) { bg = brightness == Brightness.dark ? const Color(0xFF2A2418) : const Color(0xFFFFF6E8); fg = scheme.onSurface; } else if (isMine) { bg = AppTheme.mineBubble(brightness); fg = AppTheme.mineBubbleFg(brightness); } else { bg = AppTheme.peerBubble(brightness); fg = scheme.onSurface; } final radius = BorderRadius.only( topLeft: const Radius.circular(AppTheme.rBubble), topRight: const Radius.circular(AppTheme.rBubble), bottomLeft: Radius.circular(isMine ? AppTheme.rBubble : 6), bottomRight: Radius.circular(isMine ? 6 : AppTheme.rBubble), ); final bubble = Container( constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.74), padding: const EdgeInsets.fromLTRB(14, 9, 14, 7), decoration: BoxDecoration( color: bg, borderRadius: radius, border: twin ? Border.all(color: accent.withValues(alpha: 0.35)) : null, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ if (twin) Padding( padding: const EdgeInsets.only(bottom: 3), child: Text( '와카뷰', style: theme.textTheme.labelSmall?.copyWith( color: accent, fontWeight: FontWeight.w700, letterSpacing: 0.15, ), ), ), if (retracted) Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.replay, size: 14, color: fg), const SizedBox(width: 4), Text( '되돌린 메시지', style: theme.textTheme.bodyMedium?.copyWith(color: fg, fontStyle: FontStyle.italic), ), ], ) else Text( message.text, style: theme.textTheme.bodyMedium?.copyWith(color: fg, height: 1.3), ), const SizedBox(height: 3), Text( _time(message.createdAt), style: theme.textTheme.labelSmall?.copyWith( color: fg.withValues(alpha: isMine && !twin && !retracted ? 0.7 : 0.5), fontSize: 10, ), ), ], ), ); final content = Column( crossAxisAlignment: isMine ? CrossAxisAlignment.end : CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ bubble, if (twin && isMine && !retracted && onRetract != null) Padding( padding: const EdgeInsets.only(top: 2, right: 4, left: 4), child: TextButton.icon( onPressed: onRetract, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, foregroundColor: scheme.onSurfaceVariant, textStyle: const TextStyle(fontSize: 12), ), icon: const Icon(Icons.undo, size: 14), label: const Text('되돌리기'), ), ), ], ); return Padding( padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 10), child: Align( alignment: isMine ? Alignment.centerRight : Alignment.centerLeft, child: content, ), ); } }