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 createState() => _MessageItemState(); } class _MessageItemState extends State { 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(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(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(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 navigatorKey = GlobalKey();