Chepuhagram/lib/presentation/widgets/message_bubble.dart

256 lines
9.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '/data/models/message_model.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:provider/provider.dart';
import 'dart:typed_data';
import '/core/theme_manager.dart';
import '/core/constants.dart';
class MessageBubble extends StatefulWidget {
final MessageModel message;
final VoidCallback? onTap;
final VoidCallback? onReplyTap;
final VoidCallback? onImageTap;
final Future<Uint8List?> Function(MessageModel)? onImageNeeded;
const MessageBubble({
super.key,
required this.message,
this.onTap,
this.onReplyTap,
this.onImageTap,
this.onImageNeeded,
});
@override
State<MessageBubble> createState() => _MessageBubbleState();
}
class _MessageBubbleState extends State<MessageBubble> {
Uint8List? _imageBytes;
@override
void initState() {
super.initState();
if (widget.message.localFileBytes == null &&
widget.message.messageType == MessageType.image &&
widget.onImageNeeded != null) {
_loadImage();
}
}
Future<void> _loadImage() async {
final bytes = await widget.onImageNeeded!(widget.message);
if (mounted) {
setState(() {
_imageBytes = bytes;
});
}
}
@override
Widget build(BuildContext context) {
final isMe = widget.message.isMe;
final themeProv = context.watch<ThemeProvider>();
return Align(
// Выравниваем вправо, если это мое сообщение, и влево — если чужое
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.onTap,
// На телефонах иногда удобнее/надежнее long-press (как в мессенджерах),
// поэтому поддерживаем оба жеста.
onLongPress: widget.onTap,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft: Radius.circular(isMe ? 16 : 0),
bottomRight: Radius.circular(isMe ? 0 : 16),
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
constraints: BoxConstraints(
// Чтобы баббл не растягивался на весь экран, если текста мало
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
decoration: BoxDecoration(
color: isMe
? Theme.of(context).colorScheme.primaryFixedDim
: Colors.grey[300],
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
// Скругляем углы по-разному для "хвостика" сообщения
bottomLeft: Radius.circular(isMe ? 16 : 0),
bottomRight: Radius.circular(isMe ? 0 : 16),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (widget.message.replyToText != null) ...[
GestureDetector(
onTap: widget.onReplyTap,
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
margin: const EdgeInsets.only(bottom: 4),
decoration: BoxDecoration(
color: (isMe ? Colors.white : Colors.black).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border(
left: BorderSide(
color: isMe ? Colors.black54 : Colors.black38,
width: 2,
),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.reply,
size: 14,
color: isMe ? Colors.black54 : Colors.black54,
),
const SizedBox(width: 4),
Expanded(
child: Text(
widget.message.replyToText!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: isMe ? const Color.fromARGB(221, 21, 21, 21) : const Color.fromARGB(221, 21, 21, 21),
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
),
],
),
),
),
],
if (widget.message.messageType == MessageType.image) ...[
GestureDetector(
onTap: widget.onImageTap,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: (_imageBytes ?? widget.message.localFileBytes) != null
? Image.memory(
_imageBytes ?? widget.message.localFileBytes!,
width: 200,
height: 200,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 200,
height: 200,
color: Colors.grey[300],
child: const Icon(Icons.broken_image, size: 50),
);
},
)
: Container(
width: 200,
height: 200,
color: Colors.grey[300],
child: const Center(
child: CircularProgressIndicator(),
),
),
),
),
if (widget.message.text.isNotEmpty) ...[
const SizedBox(height: 8),
],
],
if (widget.message.messageType == MessageType.text || widget.message.text.isNotEmpty) ...[
Linkify(
onOpen: (link) async {
final Uri url = Uri.parse(link.url);
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
throw Exception('Could not launch $url');
}
},
text: widget.message.text,
style: TextStyle(color: isMe ? (themeProv.isLight ? Colors.black : Colors.black) : Colors.black),
linkStyle: TextStyle(color: const Color.fromARGB(255, 10, 87, 123), fontWeight: FontWeight.bold),
),
],
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_formatTime(widget.message.createdAt),
style: TextStyle(
color: isMe ? Colors.black87 : Colors.black54,
fontSize: 10,
),
),
if (widget.message.editedAt != null) ...[
const SizedBox(width: 6),
Text(
'(изменено)',
style: TextStyle(
color: isMe ? Colors.black54 : Colors.black54,
fontSize: 10,
fontStyle: FontStyle.italic,
),
),
],
if (isMe) ...[
const SizedBox(width: 6),
Icon(
_statusIcon(widget.message.status),
size: 12,
color: _statusColor(widget.message.status, isMe),
),
],
],
),
],
),
),
),
),
);
}
IconData _statusIcon(MessageStatus status) {
switch (status) {
case MessageStatus.sending:
return Icons.access_time;
case MessageStatus.sent:
return Icons.done;
case MessageStatus.delivered:
return Icons.done_all;
case MessageStatus.read:
return Icons.done_all;
case MessageStatus.failed:
return Icons.error;
}
}
Color _statusColor(MessageStatus status, bool isMe) {
switch (status) {
case MessageStatus.read:
return isMe ? Colors.blue : Colors.blue;
case MessageStatus.failed:
return Colors.red;
default:
return isMe ? Colors.white70 : Colors.black54;
}
}
String _formatTime(DateTime time) {
final hour = time.hour.toString().padLeft(2, '0');
final minute = time.minute.toString().padLeft(2, '0');
return '$hour:$minute';
}
}