MIF_E31222656/lib/screens/community/components/message_item.dart

304 lines
9.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:tugas_akhir_supabase/screens/community/models/message.dart';
import 'package:intl/intl.dart';
import 'package:tugas_akhir_supabase/screens/community/components/image_detail_screen.dart';
class MessageItem extends StatelessWidget {
final Message message;
final bool isMyMessage;
final bool isReadByAll;
final Function(Message) onReply;
final Function(Message) onLongPress;
final Function(LinkableElement) onOpenLink;
const MessageItem({
super.key,
required this.message,
required this.isMyMessage,
required this.isReadByAll,
required this.onReply,
required this.onLongPress,
required this.onOpenLink,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
child: GestureDetector(
onLongPress: () => onLongPress(message),
child: Row(
mainAxisAlignment:
isMyMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Only show avatar for messages from others
if (!isMyMessage) _buildAvatar(),
// Message bubble with swipe to reply
Flexible(
child: Dismissible(
key: Key('dismissible-${message.id}'),
direction: DismissDirection.startToEnd,
dismissThresholds: const {DismissDirection.startToEnd: 0.2},
confirmDismiss: (_) async {
onReply(message);
return false;
},
background: Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 8),
color: Colors.green.shade100,
width: 80,
child: Icon(
Icons.reply,
color: Colors.green.shade700,
size: 18,
),
),
child: _buildMessageBubble(context),
),
),
// Space after my messages
if (isMyMessage) const SizedBox(width: 12),
],
),
),
);
}
Widget _buildAvatar() {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: CircleAvatar(
radius: 16,
backgroundColor: Colors.grey[300],
backgroundImage:
message.avatarUrl != null && message.avatarUrl!.isNotEmpty
? CachedNetworkImageProvider(
message.avatarUrl!,
maxHeight: 64,
maxWidth: 64,
)
as ImageProvider
: null,
child:
(message.avatarUrl == null || message.avatarUrl!.isEmpty)
? Text(
message.senderUsername.isNotEmpty
? message.senderUsername[0].toUpperCase()
: '?',
style: TextStyle(
color: Colors.black54,
fontWeight: FontWeight.bold,
),
)
: null,
),
);
}
Widget _buildMessageBubble(BuildContext context) {
return Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
decoration: BoxDecoration(
color: isMyMessage ? Color(0xFFDCF8C6) : Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(isMyMessage ? 8 : 0),
topRight: Radius.circular(isMyMessage ? 0 : 8),
bottomLeft: const Radius.circular(8),
bottomRight: const Radius.circular(8),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
margin: EdgeInsets.only(
bottom: 1,
left: isMyMessage ? 40 : 0,
right: isMyMessage ? 0 : 40,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Sender name (only for messages from others)
if (!isMyMessage)
Text(
message.senderUsername,
style: TextStyle(
color: Colors.green[700],
fontWeight: FontWeight.bold,
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
// Show reply preview if this is a reply
if (message.replyToId != null && message.replyToContent != null)
_buildReplyPreview(),
// Message content
if (message.imageUrl != null && message.imageUrl!.isNotEmpty)
_buildImagePreview(context),
if (message.content.isNotEmpty)
Linkify(
onOpen: onOpenLink,
text: message.content,
style: const TextStyle(color: Colors.black87, fontSize: 15),
linkStyle: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
options: LinkifyOptions(humanize: false),
),
// Time indicator with read status
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: _buildTimeWithStatus(),
),
),
],
),
);
}
Widget _buildReplyPreview() {
// Extract username from reply
String replyUsername = message.replyToSenderEmail ?? 'Unknown';
if (replyUsername.contains('@')) {
replyUsername = replyUsername.split('@')[0];
}
final replyContent = message.replyToContent ?? 'No content';
return Container(
margin: const EdgeInsets.only(bottom: 4),
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
border: Border(left: BorderSide(color: Colors.blue.shade400, width: 2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
replyUsername,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 10,
color: Colors.blue.shade700,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
replyContent.length > 40
? '${replyContent.substring(0, 40)}...'
: replyContent,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 10, color: Colors.grey.shade800),
),
],
),
);
}
Widget _buildImagePreview(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 4, top: 2),
child: GestureDetector(
onTap: () => _openImageDetail(context),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: message.imageUrl!,
placeholder:
(context, url) => Container(
height: 200,
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget:
(context, url, error) => Container(
height: 200,
color: Colors.grey[200],
child: const Center(child: Icon(Icons.error)),
),
fit: BoxFit.cover,
),
),
),
);
}
void _openImageDetail(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => ImageDetailScreen(
imageUrl: message.imageUrl!,
senderName: message.senderUsername,
timestamp: message.createdAt,
heroTag: 'message-image-${message.id}',
),
),
);
}
Widget _buildTimeWithStatus() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_formatTime(message.createdAt),
style: TextStyle(color: Colors.grey[600], fontSize: 11),
),
if (isMyMessage)
Padding(
padding: const EdgeInsets.only(left: 3),
child: Icon(
isReadByAll ? Icons.done_all : Icons.done,
size: 14,
color: isReadByAll ? Colors.blue[400] : Colors.grey[400],
),
),
],
);
}
String _formatTime(DateTime dateTime) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(Duration(days: 1));
final messageDate = DateTime(dateTime.year, dateTime.month, dateTime.day);
if (messageDate == today) {
return DateFormat('HH:mm').format(dateTime);
} else if (messageDate == yesterday) {
return 'Kemarin ${DateFormat('HH:mm').format(dateTime)}';
} else {
return DateFormat('dd/MM HH:mm').format(dateTime);
}
}
}
// Global navigator key for context access
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();