import 'package:flutter/material.dart'; import 'package:friendly_map/modules/app/app.dart'; import 'package:friendly_map/modules/storage/storage.dart'; import 'package:friendly_map/modules/work/work.dart'; import '../theme.dart'; /// 대화 한 줄: 보통 말풍선, 업무 카드, 긴급 호출 띠. [sender] 이름은 [showSender] 일 때만 말풍선 위에 보인다. class MessageBubble extends StatelessWidget { const MessageBubble(this.m, {super.key, this.sender, this.showSender = false, this.showTime = true, this.inTasks = false, this.onLongPress, this.onAddTask}); final Message m; final String? sender; final bool showSender; // 여럿이 있는 방에서 이어 보낸 첫 대화에만 final bool showTime; final bool inTasks; // 이 업무가 이미 내 업무 목록에 있음 final VoidCallback? onLongPress; final VoidCallback? onAddTask; bool get mine => m.senderId == ChatService.me; @override Widget build(BuildContext context) { final note = WorkService.noteOf(m.body); final time = Text(clockText(DateTime.fromMillisecondsSinceEpoch(m.sentAt * 10)), style: const TextStyle(fontSize: 10, color: Pal.muted)); if (note is UrgentNote) return _urgent(note, time); final maxW = MediaQuery.of(context).size.width * (note is TaskNote ? 0.8 : 0.72); final body = note is TaskNote ? _task(note) : _text(); return Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Column(crossAxisAlignment: mine ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ if (showSender && sender != null && !mine) Padding(padding: const EdgeInsets.only(left: 4, bottom: 3), child: Text(sender!, style: const TextStyle(fontSize: 12, color: Pal.muted))), Row(mainAxisAlignment: mine ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (mine && showTime) Padding(padding: const EdgeInsets.only(right: 6), child: time), GestureDetector( onLongPress: onLongPress, child: ConstrainedBox(constraints: BoxConstraints(maxWidth: maxW), child: body), ), if (!mine && showTime) Padding(padding: const EdgeInsets.only(left: 6), child: time), ]), ]), ); } Widget _text() => Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: mine ? Pal.accent : Pal.raised, borderRadius: BorderRadius.only( topLeft: const Radius.circular(18), topRight: const Radius.circular(18), bottomLeft: Radius.circular(mine ? 18 : 4), bottomRight: Radius.circular(mine ? 4 : 18), ), ), child: Text(m.body, style: TextStyle(fontSize: 15, height: 1.4, color: mine ? Colors.black : Pal.text)), ); Widget _task(TaskNote n) => Container( decoration: BoxDecoration(color: Pal.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: Pal.accent.withValues(alpha: 0.6))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(14, 8, 14, 8), decoration: BoxDecoration(color: Pal.accent.withValues(alpha: 0.14), borderRadius: const BorderRadius.vertical(top: Radius.circular(15))), child: const Row(children: [ Icon(Icons.task_alt, size: 16, color: Pal.accent), SizedBox(width: 6), Text('업무', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: Pal.accent)), ]), ), Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 4), child: Text(n.title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, height: 1.35)), ), Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 10), child: Text( [ '담당 ${n.assignee ?? '없음'}', if (n.due != null) '마감 ${n.due!.month}/${n.due!.day} (${dueText(n.due!)})', ].join(' · '), style: const TextStyle(fontSize: 12, color: Pal.muted), ), ), if (!mine) Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 6), child: inTasks ? const Padding( padding: EdgeInsets.all(6), child: Row(children: [Icon(Icons.check, size: 15, color: Pal.ok), SizedBox(width: 4), Text('업무 목록에 있어요', style: TextStyle(fontSize: 12, color: Pal.ok))]), ) : TextButton.icon(onPressed: onAddTask, icon: const Icon(Icons.add, size: 16), label: const Text('업무 목록에 넣기')), ), ]), ); Widget _urgent(UrgentNote n, Widget time) => Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Container( padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), decoration: BoxDecoration(color: Pal.urgent.withValues(alpha: 0.16), borderRadius: BorderRadius.circular(14), border: Border.all(color: Pal.urgent)), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.campaign, color: Pal.urgent), const SizedBox(width: 10), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(mine ? '긴급 호출을 보냈어요' : '${sender ?? '상대'}님의 긴급 호출', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: Pal.urgent)), const SizedBox(height: 4), Text(n.text.isEmpty ? '확인이 필요해요' : n.text, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), ]), ), time, ]), ), ); } /// 날짜 구분선. class DaySeparator extends StatelessWidget { const DaySeparator(this.day, {super.key}); final DateTime day; @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration(color: Pal.surface, borderRadius: BorderRadius.circular(12)), child: Text(dayLabel(day), style: const TextStyle(fontSize: 12, color: Pal.muted)), ), ), ); }