frindlymap/lib/modules/ui/src/widgets/message_bubble.dart

137 lines
6.5 KiB
Dart

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)),
),
),
);
}