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

397 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
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:tugas_akhir_supabase/screens/community/models/group_message.dart';
import 'package:intl/intl.dart';
import 'package:tugas_akhir_supabase/screens/community/components/image_detail_screen.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
class MessageItem extends StatefulWidget {
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,
this.isReadByAll = false,
required this.onReply,
required this.onLongPress,
this.onOpenLink,
});
@override
State<MessageItem> createState() => _MessageItemState();
}
class _MessageItemState extends State<MessageItem> {
Message get message => widget.message;
bool get isMyMessage => widget.isMyMessage;
bool get isReadByAll => widget.isReadByAll;
@override
Widget build(BuildContext context) {
return Align(
alignment: isMyMessage ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: EdgeInsets.only(
left: isMyMessage ? 64 : 8,
right: isMyMessage ? 8 : 64,
bottom: 4,
top: 4,
),
child: Column(
crossAxisAlignment:
isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
// Reply bubble if replying to another message
if (message.replyToId != null && message.replyToContent != null)
_buildReplyBubble(context),
// Message bubble
GestureDetector(
onTap:
message.imageUrl != null
? () => _openImageDetail(context)
: null,
onLongPress: () => widget.onLongPress(message),
child: _buildMessageBubble(context),
),
// Timestamp and status
_buildMessageFooter(),
],
),
),
);
}
Widget _buildMessageBubble(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: isMyMessage ? const Color(0xFF056839) : Colors.white,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft: Radius.circular(isMyMessage ? 16 : 4),
bottomRight: Radius.circular(isMyMessage ? 4 : 16),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 1),
blurRadius: 3,
),
],
),
padding:
message.imageUrl != null
? const EdgeInsets.all(3) // Smaller padding for image messages
: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Sender name
if (!isMyMessage)
Padding(
padding:
message.imageUrl != null
? const EdgeInsets.only(
left: 8,
right: 8,
top: 6,
bottom: 2,
)
: const EdgeInsets.only(bottom: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
message.senderUsername.isNotEmpty
? message.senderUsername
: message.senderEmail.split('@').first,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: isMyMessage ? Colors.white70 : Colors.blueAccent,
),
),
],
),
),
// Image if available
if (message.imageUrl != null) _buildMessageImage(context),
// Message content - always show text content even with images
if (message.content.isNotEmpty)
Padding(
padding:
message.imageUrl != null
? const EdgeInsets.only(
left: 8,
right: 8,
top: 6,
bottom: 6,
)
: EdgeInsets.zero,
child: Linkify(
onOpen: widget.onOpenLink,
text: message.content,
style: TextStyle(
color: isMyMessage ? Colors.white : Colors.black87,
fontSize: 15,
),
linkStyle: TextStyle(
color: isMyMessage ? Colors.lightBlue[100] : Colors.blue,
decoration: TextDecoration.underline,
),
),
),
],
),
);
}
void _openImageDetail(BuildContext context) {
try {
// Precache the image to improve performance
if (message.imageUrl != null && message.imageUrl!.isNotEmpty) {
precacheImage(CachedNetworkImageProvider(message.imageUrl!), context);
}
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => ImageDetailScreen(
imageUrl: message.imageUrl ?? '',
senderName: message.senderUsername,
timestamp: message.createdAt,
heroTag: 'message-image-${message.id}',
),
),
);
} catch (e) {
print('[ERROR] Failed to open image: $e');
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to open image: $e')));
}
}
Widget _buildMessageImage(BuildContext context) {
// No image
if (message.imageUrl == null || message.imageUrl!.isEmpty) {
return const SizedBox.shrink();
}
// If it's a local image, show it directly with a more WhatsApp-like loading indicator
if (message is GroupMessage &&
(message as GroupMessage).isLocalImage &&
(message as GroupMessage).localImageFile != null) {
return GestureDetector(
onTap: () => _openImageDetail(context),
child: Hero(
tag: 'message-image-${message.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
alignment: Alignment.center,
children: [
// Show the local image
Image.file(
(message as GroupMessage).localImageFile!,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
),
// Semi-transparent overlay
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
),
),
// WhatsApp-style circular progress indicator
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
strokeWidth: 3,
),
),
),
],
),
),
),
);
}
// Create a unique key for the image that includes both the URL and ID
// This forces the CachedNetworkImage to reload when the URL changes
final imageKey = '${message.id}-${message.imageUrl!}';
// Remote image with CachedNetworkImage
return GestureDetector(
onTap: () => _openImageDetail(context),
child: Hero(
tag: 'message-image-${message.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: message.imageUrl!,
fadeInDuration: const Duration(milliseconds: 200),
fadeOutDuration: const Duration(milliseconds: 200),
memCacheWidth: 800,
maxWidthDiskCache: 1280,
placeholderFadeInDuration: const Duration(milliseconds: 200),
fit: BoxFit.cover,
// Use a unique key that changes when the image URL changes
key: ValueKey<String>(imageKey),
// Force cache refresh for images
cacheManager: DefaultCacheManager(),
progressIndicatorBuilder:
(context, url, downloadProgress) => Container(
height: 200,
width: double.infinity,
color: Colors.grey[200],
child: Center(
child: CircularProgressIndicator(
value: downloadProgress.progress ?? 0.0,
valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
strokeWidth: 3,
),
),
),
errorWidget:
(context, url, error) => Container(
height: 200,
width: double.infinity,
color: Colors.grey[300],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.broken_image, color: Colors.red, size: 40),
SizedBox(height: 8),
Text(
'Failed to load image',
style: TextStyle(color: Colors.red),
),
TextButton(
onPressed: () {
// Force clear the cache for this image and try to reload
CachedNetworkImage.evictFromCache(url);
setState(() {});
},
child: Text('Retry'),
),
],
),
),
),
),
),
);
}
Widget _buildReplyBubble(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 3),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color:
isMyMessage
? Colors.green.shade100.withOpacity(0.7)
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
isMyMessage
? const Color(0xFF056839).withOpacity(0.4)
: Colors.grey.withOpacity(0.3),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
// Use username if available, otherwise fallback to email but hide domain
message.replyToSenderUsername ??
(message.replyToSenderEmail?.split('@').first ?? 'Unknown'),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
color: Color(0xFF056839),
),
),
const SizedBox(height: 2),
Text(
message.replyToContent ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 12, color: Colors.grey[800]),
),
],
),
);
}
Widget _buildMessageFooter() {
final timeFormat = DateFormat('HH:mm');
return Padding(
padding: const EdgeInsets.only(top: 2, right: 4, left: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
timeFormat.format(message.createdAt.toLocal()),
style: TextStyle(fontSize: 10, color: Colors.grey[600]),
),
if (isMyMessage) ...[
const SizedBox(width: 4),
Icon(
isReadByAll ? Icons.done_all : Icons.done,
size: 12,
color: isReadByAll ? const Color(0xFF056839) : Colors.grey[600],
),
],
const Spacer(),
GestureDetector(
onTap: () => widget.onReply(message),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Balas',
style: TextStyle(fontSize: 10, color: Colors.grey[700]),
),
),
),
],
),
);
}
}
// Global navigator key for context access
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();