import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'dart:async'; import 'package:tugas_akhir_supabase/core/theme/app_colors.dart'; import 'package:tugas_akhir_supabase/screens/community/models/group.dart'; import 'package:tugas_akhir_supabase/screens/community/models/group_message.dart'; import 'package:tugas_akhir_supabase/screens/community/models/message.dart'; import 'package:tugas_akhir_supabase/screens/community/services/group_message_service.dart'; import 'package:tugas_akhir_supabase/screens/community/services/group_service.dart'; import 'package:tugas_akhir_supabase/screens/community/services/profile_service.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'package:intl/intl.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:tugas_akhir_supabase/screens/community/components/message_item.dart'; import 'package:tugas_akhir_supabase/screens/community/components/message_input.dart'; import 'package:tugas_akhir_supabase/screens/community/components/empty_state.dart'; import 'package:tugas_akhir_supabase/screens/shared/leaf_pattern_painter.dart'; class GroupChatScreen extends StatefulWidget { final String groupId; final bool isInTabView; final GlobalKey? screenKey; const GroupChatScreen({ super.key, required this.groupId, this.isInTabView = false, this.screenKey, }); @override GroupChatScreenState createState() => GroupChatScreenState(); } class GroupChatScreenState extends State with WidgetsBindingObserver, AutomaticKeepAliveClientMixin { // Services final _groupService = GroupService(); final _messageService = GroupMessageService(); final _profileService = ProfileService(); // Controllers final _messageController = TextEditingController(); final _scrollController = ScrollController(); final _messageFocusNode = FocusNode(); final _refreshIndicatorKey = GlobalKey(); StreamSubscription? _messageStreamSubscription; // State variables bool _isLoading = true; bool _isLoadingMore = false; bool _hasMoreMessages = true; bool _isUploading = false; bool _isReplying = false; bool _showEmojiKeyboard = false; final bool _isSelectMode = false; // Data Group? _currentGroup; List _messages = []; GroupMessage? _replyToMessage; File? _selectedImage; XFile? _selectedImageFile; String? _selectedImagePath; Set _deletedMessageIds = {}; final Set _selectedMessageIds = {}; // User info String? _currentUserEmail; String? _currentUsername; // Track if a refresh is in progress bool _isRefreshing = false; // Debounced refresh that prevents infinite loops void _debouncedRefresh() { if (_isRefreshing) { print('[DEBUG] Refresh already in progress, skipping'); return; } _isRefreshing = true; print('[FORCE] Performing debounced refresh'); // Do a single aggressive refresh _loadMessages(forceRefresh: true); // Reset flag after some time Future.delayed(Duration(seconds: 5), () { if (mounted) { _isRefreshing = false; print('[DEBUG] Refresh cooldown complete'); } }); } @override void initState() { super.initState(); _initialize(); } void _initialize() async { WidgetsBinding.instance.addObserver(this); _messageController.addListener(_updateSendButtonState); _scrollController.addListener(_scrollListener); // Immediately load messages without waiting for other initialization if (mounted) { _loadMessages(forceRefresh: true).catchError((e) { print('[ERROR] Initial message load failed: $e'); }); } // Subscribe to real-time message updates _setupStreamSubscription(); _safeSetupMessagesSubscription(); // Gunakan try-catch untuk menangkap error dan mencegah crash try { // Set safety timer untuk mencegah loading yang tidak berhenti Future.delayed(Duration(seconds: 5), () { if (mounted && _isLoading) { print('[WARNING] Force completing group chat loading after timeout'); setState(() => _isLoading = false); } }); // Inisialisasi dengan timeout untuk mencegah blocking await _profileService.initialize().timeout( Duration(seconds: 2), onTimeout: () { print('[WARNING] Profile service initialization timed out'); throw TimeoutException('Profile service initialization timed out'); }, ); await _getCurrentUser().timeout( Duration(seconds: 2), onTimeout: () { print('[WARNING] Get current user timed out'); // Continue anyway }, ); await _loadGroupDetails().timeout( Duration(seconds: 2), onTimeout: () { print('[WARNING] Load group details timed out'); // Continue anyway }, ); await _loadDeletedMessageIds().timeout( Duration(seconds: 2), onTimeout: () { print('[WARNING] Load deleted message IDs timed out'); // Continue anyway }, ); // Delay subscription setup to prevent blocking Future.delayed(Duration(milliseconds: 800), () { if (mounted) { _safeSetupMessagesSubscription(); } }); } catch (e) { print('[ERROR] Error in group chat initialization: $e'); if (mounted) { setState(() => _isLoading = false); } } } // Set up subscription to real-time message stream void _setupStreamSubscription() { _messageStreamSubscription?.cancel(); // Instead of using messageStream, we'll use the onNewMessage callback in setupMessagesSubscription } @override void didUpdateWidget(GroupChatScreen oldWidget) { super.didUpdateWidget(oldWidget); // If group ID changed, reload everything if (oldWidget.groupId != widget.groupId) { _loadGroupDetails(); _loadMessages(forceRefresh: true); _setupStreamSubscription(); _safeSetupMessagesSubscription(); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _messageController.removeListener(_updateSendButtonState); _messageController.dispose(); _messageFocusNode.dispose(); _scrollController.removeListener(_scrollListener); _scrollController.dispose(); _messageStreamSubscription?.cancel(); super.dispose(); } @override bool get wantKeepAlive => true; // User details Future _getCurrentUser() async { final userInfo = await _profileService.getCurrentUser(); if (mounted) { setState(() { _currentUserEmail = userInfo.email; _currentUsername = userInfo.username; }); } } // Load group details Future _loadGroupDetails() async { try { final group = await _groupService.getGroupDetails(widget.groupId); if (mounted && group != null) { setState(() { _currentGroup = group; }); } } catch (e) { print('[ERROR] Failed to load group details: $e'); } } // Load messages Future _loadMessages({ bool forceRefresh = false, bool loadNew = false, }) async { print( '[DEBUG] _loadMessages called. forceRefresh: $forceRefresh, loadNew: $loadNew', ); if (!mounted) return; setState(() { if (!loadNew) _isLoading = true; }); try { final result = await _messageService.loadMessages( widget.groupId, forceRefresh: forceRefresh, loadNew: loadNew, existingMessages: _messages, ); if (!mounted) return; setState(() { // Filter out deleted messages List filteredMessages = result.messages .where((msg) => !_deletedMessageIds.contains(msg.id)) .toList(); if (loadNew && _messages.isNotEmpty) { // Add only new messages that don't already exist final existingIds = _messages.map((m) => m.id).toSet(); final newMessages = filteredMessages .where((msg) => !existingIds.contains(msg.id)) .toList(); _messages.insertAll(0, newMessages); // Ensure no duplicates and proper sorting final uniqueMessages = {}; _messages = _messages.where((msg) { final isUnique = !uniqueMessages.contains(msg.id); uniqueMessages.add(msg.id); return isUnique; }).toList(); } else { _messages = filteredMessages; } // Always sort after modifying the list _messages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); _hasMoreMessages = result.hasMore; _isLoading = false; }); } catch (e) { if (mounted) { setState(() => _isLoading = false); _showErrorSnackBar( 'Gagal memuat pesan. Silakan coba lagi.', onRetry: () => _loadMessages(forceRefresh: true), ); } } } Future _loadMoreMessages() async { if (!mounted || _isLoadingMore || !_hasMoreMessages) return; setState(() => _isLoadingMore = true); try { final result = await _messageService.loadMoreMessages( widget.groupId, _messages, ); if (mounted) { setState(() { // Filter out deleted messages List filteredMessages = result.messages .where((msg) => !_deletedMessageIds.contains(msg.id)) .toList(); // Add only messages that don't already exist final existingIds = _messages.map((m) => m.id).toSet(); final newMessages = filteredMessages .where((msg) => !existingIds.contains(msg.id)) .toList(); _messages.addAll(newMessages); // Ensure no duplicates final uniqueMessages = {}; _messages = _messages.where((msg) { final isUnique = !uniqueMessages.contains(msg.id); uniqueMessages.add(msg.id); return isUnique; }).toList(); // Always sort after modifying the list _messages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); _hasMoreMessages = result.hasMore; _isLoadingMore = false; }); } } catch (e) { if (mounted) { setState(() => _isLoadingMore = false); _showErrorSnackBar('Gagal memuat pesan sebelumnya. Silakan coba lagi.'); } } } // Message sending Future _sendMessage() async { final messageText = _messageController.text.trim(); if (messageText.isEmpty && _selectedImage == null) return; _messageController.clear(); final imageToUpload = _selectedImage; final replyToMessage = _replyToMessage != null ? GroupMessage.copy(_replyToMessage!) : null; setState(() { _selectedImage = null; _replyToMessage = null; _isReplying = false; _isUploading = imageToUpload != null; }); // If we have an image, create a local preview message immediately if (imageToUpload != null) { final previewMessage = GroupMessage( id: 'temp-${DateTime.now().millisecondsSinceEpoch}', content: messageText, senderEmail: _currentUserEmail ?? '', senderUsername: _currentUsername ?? '', senderUserId: _messageService.currentUserId ?? '', createdAt: DateTime.now(), groupId: widget.groupId, isLocalImage: true, localImageFile: imageToUpload, ); // Add the preview message to the UI immediately setState(() { _messages.insert(0, previewMessage); _messages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); }); } try { // The image will be shown immediately with an optimistic update final result = await _messageService.sendMessage( groupId: widget.groupId, text: messageText, imageFile: imageToUpload, replyToMessage: replyToMessage, currentUsername: _currentUsername, currentEmail: _currentUserEmail, onOptimisticUpdate: (message) { if (mounted) { setState(() { // If this is an image update, find and replace the temp message if (imageToUpload != null) { final tempIndex = _messages.indexWhere( (m) => m.isLocalImage && m.localImageFile == imageToUpload, ); if (tempIndex >= 0) { _messages[tempIndex] = message; } else { // Check if the message already exists by ID final existingIndex = _messages.indexWhere( (m) => m.id == message.id, ); if (existingIndex >= 0) { // Update existing message (for image URL updates) _messages[existingIndex] = message; } else { // Add new message _messages.insert(0, message); } } } else { // For text messages, just add or update final existingIndex = _messages.indexWhere( (m) => m.id == message.id, ); if (existingIndex >= 0) { _messages[existingIndex] = message; } else { _messages.insert(0, message); } } // Remove duplicates and sort final uniqueIds = {}; _messages = _messages.where((msg) { final isUnique = !uniqueIds.contains(msg.id); uniqueIds.add(msg.id); return isUnique; }).toList(); _messages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); }); } }, ); // Message was sent successfully // The optimistic update already handled UI updates // If the message had an image, make sure we have the final version with the correct URL if (imageToUpload != null) { setState(() { _isUploading = false; }); } } catch (e) { if (mounted) { setState(() => _isUploading = false); _showErrorSnackBar( 'Pesan gagal terkirim. Coba lagi nanti.', onRetry: () => _sendMessage(), ); } } } // Read status void _markVisibleMessagesAsRead() { _messageService.markMessagesAsRead(_messages); } void _updateReadStatus() { _markVisibleMessagesAsRead(); } // New message handler void _onNewMessage(GroupMessage message) { if (!mounted) return; // Check if we already have this message final existingIndex = _messages.indexWhere((m) => m.id == message.id); setState(() { if (existingIndex >= 0) { // Update existing message (e.g., when image URL is updated) _messages[existingIndex] = message; } else { // Check if message is deleted if (!_deletedMessageIds.contains(message.id)) { // Insert without duplicating _messages.insert(0, message); } } // Remove duplicates by ID before sorting final uniqueMessages = {}; _messages = _messages.where((msg) { final isUnique = !uniqueMessages.contains(msg.id); uniqueMessages.add(msg.id); return isUnique; }).toList(); // Sort messages _messages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); }); } // Read status update handler void _onReadStatusUpdate(String messageId, String userId) { if (!mounted) return; setState(() {}); } // Scroll handling void _scrollListener() { // Load more messages when reaching the bottom if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 500 && !_isLoadingMore && _hasMoreMessages) { _loadMoreMessages(); } // Mark visible messages as read _markVisibleMessagesAsRead(); } // UI updates void _updateSendButtonState() { if (mounted) setState(() {}); } // Reply void _startReply(Message message) { // Convert Message to GroupMessage if needed final groupMessage = message is GroupMessage ? message : GroupMessage.fromMessage(message, widget.groupId); // Check message age final cutoffDate = DateTime.now().subtract(Duration(days: 30)); if (message.createdAt.isBefore(cutoffDate)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Tidak dapat membalas pesan yang lebih lama dari 30 hari', ), backgroundColor: Colors.orange, ), ); return; } try { // Give feedback HapticFeedback.lightImpact(); setState(() { _replyToMessage = groupMessage; _isReplying = true; _showEmojiKeyboard = false; }); // Focus the input field after a short delay WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { Future.delayed(Duration(milliseconds: 100), () { if (mounted) FocusScope.of(context).requestFocus(_messageFocusNode); }); } }); } catch (e) { print('[ERROR] Error starting reply: $e'); setState(() { _replyToMessage = null; _isReplying = false; }); } } void _cancelReply() { setState(() { _replyToMessage = null; _isReplying = false; }); } // Media void _showImageSourceOptions() { if (kIsWeb) { // On web, only show option to pick from gallery since camera is not supported _pickImage(); return; } showModalBottomSheet( context: context, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: Icon(Icons.camera_alt, color: AppColors.primary), title: Text('Camera'), onTap: () { Navigator.pop(context); _pickImageFromCamera(); }, ), ListTile( leading: Icon(Icons.photo_library, color: AppColors.primary), title: Text('Gallery'), onTap: () { Navigator.pop(context); _pickImage(); }, ), ], ), ), ); } Future _pickImage() async { try { final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage( source: ImageSource.gallery, maxWidth: 1024, maxHeight: 1024, imageQuality: 85, ); if (image != null && mounted) { if (kIsWeb) { // For web, we need special handling setState(() { _selectedImageFile = image; _selectedImagePath = 'Image selected'; _selectedImage = null; // Clear File-based image }); } else { // For mobile platforms setState(() { _selectedImageFile = image; _selectedImagePath = image.path; _selectedImage = File(image.path); }); } } } catch (e) { print('[ERROR] Failed to pick image: $e'); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Failed to select image: $e'))); } } } Future _pickImageFromCamera() async { final ImagePicker picker = ImagePicker(); try { final XFile? image = await picker.pickImage( source: ImageSource.camera, maxWidth: 1024, maxHeight: 1024, imageQuality: 85, ); if (image != null && mounted) { setState(() => _selectedImage = File(image.path)); } } catch (e) { print('[ERROR] Gagal mengambil gambar dari kamera: $e'); } } // Load deleted message IDs from local storage Future _loadDeletedMessageIds() async { try { final prefs = await SharedPreferences.getInstance(); final deletedIds = prefs.getStringList('deleted_message_ids_${widget.groupId}') ?? []; setState(() { _deletedMessageIds = Set.from(deletedIds); }); } catch (e) { print('[ERROR] Failed to load deleted message IDs: $e'); } } // Save deleted message IDs to local storage Future _saveDeletedMessageIds() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setStringList( 'deleted_message_ids_${widget.groupId}', _deletedMessageIds.toList(), ); } catch (e) { print('[ERROR] Failed to save deleted message IDs: $e'); } } void _showErrorSnackBar(String message, {VoidCallback? onRetry}) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red, duration: Duration(seconds: 3), action: onRetry != null ? SnackBarAction(label: 'Coba Lagi', onPressed: onRetry) : null, ), ); } Future _openLink(LinkableElement link) async { final url = Uri.parse(link.url); try { if (await canLaunchUrl(url)) { await launchUrl(url, mode: LaunchMode.externalApplication); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Tidak dapat membuka: ${link.url}')), ); } } catch (e) { print('[ERROR] Gagal membuka URL: $e'); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Gagal membuka URL: $e'))); } } bool _shouldShowDateHeader(int index) { if (index == _messages.length - 1) { return true; // Always show for first message (which is last in the list) } if (index < _messages.length - 1) { final currentDate = DateTime( _messages[index].createdAt.year, _messages[index].createdAt.month, _messages[index].createdAt.day, ); final previousDate = DateTime( _messages[index + 1].createdAt.year, _messages[index + 1].createdAt.month, _messages[index + 1].createdAt.day, ); return currentDate != previousDate; } return false; } Widget _buildDateHeader(DateTime date) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final yesterday = today.subtract(const Duration(days: 1)); final messageDate = DateTime(date.year, date.month, date.day); String dateText; if (messageDate == today) { dateText = 'Hari ini'; } else if (messageDate == yesterday) { dateText = 'Kemarin'; } else { dateText = DateFormat('EEEE, d MMMM y', 'id_ID').format(messageDate); } return Container( margin: const EdgeInsets.symmetric(vertical: 16), child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(16), ), child: Text( dateText, style: TextStyle( fontSize: 12, color: Colors.grey[800], fontWeight: FontWeight.w500, ), ), ), ), ); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { // Pastikan keyboard ditutup sebelum kembali if (_showEmojiKeyboard) { setState(() => _showEmojiKeyboard = false); } return true; }, child: Scaffold( appBar: widget.isInTabView ? null : AppBar( backgroundColor: AppColors.primary, foregroundColor: Colors.white, title: _currentGroup != null ? Text(_currentGroup!.name) : const Text('Diskusi Grup'), elevation: 0, ), body: Column( children: [ // Messages area Expanded( child: GestureDetector( // Dismiss keyboard when tapping outside of text field onTap: () { if (_showEmojiKeyboard) { setState(() => _showEmojiKeyboard = false); } }, child: Stack( children: [ // Agriculture-themed background Positioned.fill( child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.green[50]!, Colors.green[100]!.withOpacity(0.3), ], ), ), child: CustomPaint( painter: LeafPatternPainter(), child: Container(), ), ), ), // Messages content RefreshIndicator( key: _refreshIndicatorKey, color: AppColors.primary, onRefresh: () => _loadMessages(forceRefresh: true), child: _buildMessageArea(), ), ], ), ), ), // Input area _buildMessageInput(), ], ), ), ); } Widget _buildMessageArea() { if (_isLoading) { return Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(AppColors.primary), ), ); } else if (_messages.isEmpty) { return EmptyStateWidget( onTap: () { print('[FORCE] EmptyState onTap triggered'); // Use the debounced refresh _debouncedRefresh(); // Try to show the RefreshIndicator directly, but only once if (_refreshIndicatorKey.currentState != null && !_isRefreshing) { _refreshIndicatorKey.currentState!.show(); } }, ); } else { return _buildMessageList(); } } Widget _buildMessageList() { return ListView.builder( key: PageStorageKey('group_message_list_${widget.groupId}'), controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8), itemCount: _messages.length + (_isLoadingMore ? 1 : 0), reverse: true, cacheExtent: 1000, // Keep more items in memory addAutomaticKeepAlives: true, itemBuilder: (context, index) { // Show loading indicator at the end of the list if (index == _messages.length) { return const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 16.0), child: CircularProgressIndicator(), ), ); } final message = _messages[index]; final isMyMessage = message.senderUserId == _messageService.currentUserId; // Check if we need to show date header final showDateHeader = _shouldShowDateHeader(index); // Use a more efficient approach to rebuild only when needed return Column( children: [ if (showDateHeader) _buildDateHeader(_messages[index].createdAt), MessageItem( key: ValueKey(message.id), message: message, isMyMessage: isMyMessage, isReadByAll: _messageService.isMessageReadByAll(message), onReply: _startReply, onLongPress: _showMessageOptions, onOpenLink: (link) => _openLink(link), ), ], ); }, ); } void _showMessageOptions(Message message) { final bool isMyMessage = message.senderUserId == _messageService.currentUserId; showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: Icon(Icons.reply, color: Colors.green.shade700), title: const Text('Balas'), onTap: () { Navigator.pop(context); _startReply(message); }, ), // Only show delete options for user's own messages if (isMyMessage) ListTile( leading: Icon(Icons.delete_forever, color: Colors.red), title: const Text('Hapus untuk semua orang'), onTap: () { Navigator.pop(context); _showDeleteConfirmation(message); }, ), ], ), ), ); } void _showDeleteConfirmation(Message message) { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Konfirmasi Hapus'), content: Text( 'Pesan ini akan dihapus untuk semua orang. Lanjutkan?', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Batal'), ), TextButton( style: TextButton.styleFrom(foregroundColor: Colors.red), onPressed: () { Navigator.pop(context); _deleteMessage(message); }, child: Text('Hapus'), ), ], ), ); } Future _deleteMessage(Message message) async { // First remove from local list for immediate feedback setState(() { _messages.removeWhere((m) => m.id == message.id); _deletedMessageIds.add(message.id); }); _saveDeletedMessageIds(); try { // Convert to GroupMessage if needed final groupMessage = message is GroupMessage ? message : GroupMessage.fromMessage(message, widget.groupId); await _messageService.deleteMessage(groupMessage); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Pesan dihapus'), backgroundColor: Colors.green, duration: Duration(seconds: 2), ), ); } catch (e) { print('[ERROR] Failed to delete message: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal menghapus pesan dari server: $e'), backgroundColor: Colors.red, ), ); } } Widget _buildMessageInput() { return MessageInputWidget( messageController: _messageController, focusNode: _messageFocusNode, isUploading: _isUploading, selectedImage: _selectedImage, showEmojiKeyboard: _showEmojiKeyboard, isReplying: _isReplying, replyToMessage: _replyToMessage, onSend: _sendMessage, onImageOptions: _showImageSourceOptions, themeColor: AppColors.primary, onEmojiToggle: () { setState(() { _showEmojiKeyboard = !_showEmojiKeyboard; if (_showEmojiKeyboard) { _messageFocusNode.unfocus(); } else { _messageFocusNode.requestFocus(); } }); }, onClearImage: () { setState(() => _selectedImage = null); }, onCancelReply: _cancelReply, ); } // Public method to force refresh void forceRefresh() { print('[DEBUG] Force refresh called on GroupChatScreen'); // Make sure we're not already refreshing if (_isLoading) { print('[DEBUG] Already loading, skipping additional refresh'); return; } // Aggressively load messages with forceRefresh _loadMessages(forceRefresh: true); // Additional refresh after a short delay for images Future.delayed(Duration(milliseconds: 500), () { if (mounted) { setState(() { // Force UI update even if data hasn't changed // This ensures image cache is properly refreshed }); } }); } // Emergency direct database access to bypass service layer void _emergencyLoadMessages() async { try { print( '[FORCE] Emergency direct message load for group: ${widget.groupId}', ); final supabase = Supabase.instance.client; // Try to load directly from database final response = await supabase .from('messages_with_sender') .select() .eq('group_id', widget.groupId) .order('created_at', ascending: false) .limit(20); print('[FORCE] Got ${response.length} messages directly from database'); if (mounted && response.isNotEmpty) { // Process messages final messages = response.map((data) { return GroupMessage.fromMap({ 'id': data['id'], 'content': data['content'] ?? '', 'sender_user_id': data['sender_user_id'] ?? '', 'sender_username': data['profile_username'] ?? data['sender_username'] ?? '', 'sender_email': data['profile_email'] ?? data['sender_email'] ?? '', 'created_at': data['created_at'], 'image_url': data['image_url'], 'reply_to_id': data['reply_to_id'], 'reply_to_content': data['reply_to_content'], 'reply_to_sender_email': data['reply_to_sender_email'], 'avatar_url': data['avatar_url'], 'group_id': data['group_id'], }); }).toList(); // Update UI with messages setState(() { if (_messages.isEmpty) { _messages = messages.toList(); _messages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); } _isLoading = false; }); } } catch (e) { print('[ERROR] Emergency load failed: $e'); } } // Set up message subscriptions safely with timeout void _safeSetupMessagesSubscription() { print('[DEBUG] _safeSetupMessagesSubscription called'); try { // Set up real-time message subscription with timeout _messageService.setupMessagesSubscription( widget.groupId, _onNewMessage, _onReadStatusUpdate, ); // Also set up the stream listener _setupStreamSubscription(); // Mark messages as read on initial load WidgetsBinding.instance.addPostFrameCallback((_) { _markVisibleMessagesAsRead(); }); } catch (e) { print('[ERROR] Error setting up message subscription: $e'); } } // Set up real time subscription explicitly void setupRealTimeSubscription() { print('[STREAM] Setting up real-time subscription from parent call'); _safeSetupMessagesSubscription(); _setupStreamSubscription(); } // Clean up resources explicitly when called from parent void disposeResources() { print('[STREAM] Cleaning up resources from parent call'); _messageStreamSubscription?.cancel(); } // Set preloaded messages directly from the parent component void setPreloadedMessages(List messageData) { if (!mounted) return; try { print('[DIRECT] Setting ${messageData.length} preloaded messages'); final messages = messageData.map((data) { return GroupMessage.fromMap({ 'id': data['id'], 'content': data['content'] ?? '', 'sender_user_id': data['sender_user_id'] ?? '', 'sender_username': data['profile_username'] ?? data['sender_username'] ?? '', 'sender_email': data['profile_email'] ?? data['sender_email'] ?? '', 'created_at': data['created_at'], 'image_url': data['image_url'], 'reply_to_id': data['reply_to_id'], 'reply_to_content': data['reply_to_content'], 'reply_to_sender_email': data['reply_to_sender_email'], 'avatar_url': data['avatar_url'], 'group_id': data['group_id'], }); }).toList(); setState(() { if (_messages.isEmpty) { _messages = messages.toList(); _messages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); _isLoading = false; print( '[DIRECT] Successfully set ${_messages.length} preloaded messages', ); } }); } catch (e) { print('[ERROR] Failed to set preloaded messages: $e'); } } }