iykyka/mobile/lib/widgets/message_bubble.dart

124 lines
3.8 KiB
Dart

import 'package:flutter/material.dart';
import '../models/models.dart';
/// Twin messages use a dashed border + badge (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;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final twin = message.isTwin;
final bg = isMine
? theme.colorScheme.primaryContainer
: theme.colorScheme.surfaceContainerHighest;
final bubble = Container(
constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.78),
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(14),
border: twin
? Border.all(color: theme.colorScheme.tertiary, width: 1.5, strokeAlign: BorderSide.strokeAlignOutside)
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (twin)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'분신',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.tertiary,
fontWeight: FontWeight.w700,
),
),
),
Text(
message.retracted ? '(되돌린 메시지)' : message.text,
style: theme.textTheme.bodyMedium?.copyWith(
fontStyle: message.retracted ? FontStyle.italic : FontStyle.normal,
color: message.retracted ? theme.disabledColor : null,
),
),
if (twin && isMine && !message.retracted && onRetract != null)
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: onRetract,
child: const Text('되돌리기'),
),
),
],
),
);
// Dashed look for twin: overlay a custom painter border when twin.
if (!twin) {
return Align(
alignment: isMine ? Alignment.centerRight : Alignment.centerLeft,
child: bubble,
);
}
return Align(
alignment: isMine ? Alignment.centerRight : Alignment.centerLeft,
child: CustomPaint(
painter: _DashedRRectPainter(color: theme.colorScheme.tertiary),
child: bubble,
),
);
}
}
class _DashedRRectPainter extends CustomPainter {
_DashedRRectPainter({required this.color});
final Color color;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
final rrect = RRect.fromRectAndRadius(
Rect.fromLTWH(1, 1, size.width - 2, size.height - 2),
const Radius.circular(14),
);
final path = Path()..addRRect(rrect);
final dashed = _dashPath(path, dashLength: 5, gapLength: 4);
canvas.drawPath(dashed, paint);
}
Path _dashPath(Path source, {required double dashLength, required double gapLength}) {
final metrics = source.computeMetrics();
final out = Path();
for (final metric in metrics) {
var distance = 0.0;
while (distance < metric.length) {
final next = distance + dashLength;
out.addPath(metric.extractPath(distance, next.clamp(0, metric.length)), Offset.zero);
distance = next + gapLength;
}
}
return out;
}
@override
bool shouldRepaint(covariant _DashedRRectPainter oldDelegate) => oldDelegate.color != color;
}