import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'dart:math'; import 'dart:async'; import 'package:url_launcher/url_launcher.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:uuid/uuid.dart'; import 'package:flutter/services.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:shared_preferences/shared_preferences.dart'; // Import separated components import 'package:tugas_akhir_supabase/screens/community/models/message.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/reply_bar.dart'; import 'package:tugas_akhir_supabase/screens/community/components/empty_state.dart'; import 'package:tugas_akhir_supabase/screens/community/components/search_app_bar.dart'; import 'package:tugas_akhir_supabase/screens/community/services/message_service.dart'; import 'package:tugas_akhir_supabase/screens/community/services/profile_service.dart'; import 'package:tugas_akhir_supabase/screens/community/utils/message_utils.dart'; import 'package:tugas_akhir_supabase/screens/shared/leaf_pattern_painter.dart'; import 'package:tugas_akhir_supabase/core/theme/app_colors.dart'; class CommunityScreen extends StatefulWidget { const CommunityScreen({super.key}); @override _CommunityScreenState createState() => _CommunityScreenState(); } class _CommunityScreenState extends State with WidgetsBindingObserver { // Services final _messageService = MessageService(); final _profileService = ProfileService(); // Controllers final _messageController = TextEditingController(); final _searchController = TextEditingController(); final _scrollController = ScrollController(); final _messageFocusNode = FocusNode(); final _refreshIndicatorKey = GlobalKey(); // State variables bool _isLoading = true; bool _isLoadingMore = false; bool _isLoadingSearch = false; bool _hasMoreMessages = true; bool _isUploading = false; bool _isSearching = false; bool _isReplying = false; bool _showEmojiKeyboard = false; bool _isSelectMode = false; // Data List _messages = []; List _searchResults = []; Message? _replyToMessage; File? _selectedImage; Set _deletedMessageIds = {}; Set _selectedMessageIds = {}; // User info String? _currentUserEmail; String? _currentUsername; @override void initState() { super.initState(); _initialize(); } void _initialize() async { WidgetsBinding.instance.addObserver(this); _messageController.addListener(_updateSendButtonState); _scrollController.addListener(_scrollListener); await _profileService.initialize(); await _getCurrentUser(); await _loadDeletedMessageIds(); await _loadMessages(); _setupTimers(); } void _setupTimers() { // Set up message subscription with slight delay to prevent UI blocking Future.delayed(Duration(milliseconds: 500), () { if (mounted) _messageService.setupMessagesSubscription( _onNewMessage, _onReadStatusUpdate, ); }); // Setup periodic refresh timer _messageService.setupRefreshTimer( onRefresh: () { if (mounted) _loadMessages(loadNew: true); }, ); // Setup read status timer _messageService.setupReadStatusTimer( onUpdate: () { if (mounted) _updateReadStatus(); }, ); // Mark messages as read on initial load WidgetsBinding.instance.addPostFrameCallback((_) { _markVisibleMessagesAsRead(); }); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _updateReadStatus(); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _messageController.removeListener(_updateSendButtonState); _messageController.dispose(); _searchController.dispose(); _messageFocusNode.dispose(); _scrollController.removeListener(_scrollListener); _scrollController.dispose(); _messageService.dispose(); _profileService.dispose(); super.dispose(); } // User details Future _getCurrentUser() async { final userInfo = await _profileService.getCurrentUser(); if (mounted) { setState(() { _currentUserEmail = userInfo.email; _currentUsername = userInfo.username; }); } } // Load messages Future _loadMessages({ bool forceRefresh = false, bool loadNew = false, }) async { if (!mounted) return; setState(() { if (!loadNew) _isLoading = true; }); try { final result = await _messageService.loadMessages( 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(_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 ? Message.copy( _replyToMessage!, ) // Create deep copy to avoid reference issues : null; setState(() { _selectedImage = null; _replyToMessage = null; _isReplying = false; _isUploading = true; }); try { final result = await _messageService.sendMessage( text: messageText, imageFile: imageToUpload, replyToMessage: replyToMessage, currentUsername: _currentUsername, currentEmail: _currentUserEmail, onOptimisticUpdate: (message) { if (mounted) { setState(() { _messages.insert(0, message); }); } }, ); // Success case - no need to show a notification } catch (e) { if (mounted) { _showErrorSnackBar( 'Pesan gagal terkirim. Coba lagi nanti.', onRetry: () => _sendMessage(), ); } } finally { if (mounted) { setState(() => _isUploading = false); } } } // Read status void _markVisibleMessagesAsRead() { _messageService.markVisibleMessagesAsRead(_messages); } void _updateReadStatus() { _markVisibleMessagesAsRead(); _messageService.fetchReadReceipts(_messages); } // New message handler void _onNewMessage(Message message) { if (!mounted) return; // Check if we already have this message final existingIndex = _messages.indexWhere((m) => m.id == message.id); if (existingIndex >= 0) return; // Check if message is deleted if (_deletedMessageIds.contains(message.id)) return; setState(() { // 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(() {}); } // Search Future _searchMessages() async { final query = _searchController.text.trim(); if (query.isEmpty) { setState(() { _isSearching = false; _searchResults = []; }); return; } setState(() => _isLoadingSearch = true); try { final results = await _messageService.searchMessages(query); if (mounted) { setState(() { _searchResults = results; _isLoadingSearch = false; }); } } catch (e) { if (mounted) { setState(() => _isLoadingSearch = false); _showErrorSnackBar('Gagal mencari pesan: $e'); } } } void _exitSearchMode() { setState(() { _isSearching = false; _searchResults = []; _searchController.clear(); }); } // Reply void _startReply(Message message) { // 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 = Message.copy(message); // Create deep copy _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 Future _pickImage() async { final ImagePicker picker = ImagePicker(); try { final XFile? image = await picker.pickImage( source: ImageSource.gallery, maxWidth: 1024, maxHeight: 1024, imageQuality: 85, ); if (image != null && mounted) { setState(() => _selectedImage = File(image.path)); } } catch (e) { print('[ERROR] Gagal mengambil gambar: $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'); } } void _showImageSourceOptions() { 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: const Color(0xFF00A884), ), title: Text('Kamera'), onTap: () { Navigator.pop(context); _pickImageFromCamera(); }, ), ListTile( leading: Icon( Icons.photo_library, color: const Color(0xFF00A884), ), title: Text('Galeri'), onTap: () { Navigator.pop(context); _pickImage(); }, ), ], ), ), ); } // Error handling 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, ), ); } // UI Building @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { // Exit select mode if active if (_isSelectMode) { setState(() { _isSelectMode = false; _selectedMessageIds.clear(); }); return false; } // Ensure keyboard and emoji picker are dismissed before popping if (_showEmojiKeyboard) { setState(() => _showEmojiKeyboard = false); } // Hapus unfocus yang menyebabkan masalah keyboard // FocusScope.of(context).unfocus(); return true; }, child: Scaffold( backgroundColor: AppColors.scaffoldBackground, appBar: _isSearching ? _buildSearchAppBar() : _isSelectMode ? _buildSelectModeAppBar() : _buildAppBar(), // Set resizeToAvoidBottomInset true to ensure the keyboard doesn't overflow resizeToAvoidBottomInset: true, body: Column( children: [ // Messages area Expanded( child: GestureDetector( // Dismiss keyboard when tapping outside of text field onTap: () { // Hapus unfocus yang menyebabkan masalah keyboard // FocusScope.of(context).unfocus(); 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: AppColors.backgroundGradient, ), ), child: CustomPaint( painter: LeafPatternPainter(), child: Container(), ), ), ), // Messages content RefreshIndicator( key: _refreshIndicatorKey, color: AppColors.primary, onRefresh: () => _isSearching ? _searchMessages() : _loadMessages(forceRefresh: true), child: _buildMessageArea(), ), ], ), ), ), // Input area - Hide when searching or in select mode if (!_isSearching && !_isSelectMode) _buildMessageInput(), ], ), ), ); } Widget _buildMessageArea() { if (_isLoading || _isLoadingSearch) { return Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(AppColors.primary), ), ); } else if (_isSearching) { return _buildSearchResults(); } else if (_messages.isEmpty) { return EmptyStateWidget(onTap: () => _loadMessages(forceRefresh: true)); } else { return _buildMessageList(); } } Widget _buildMessageList() { return ListView.builder( key: PageStorageKey('message_list'), 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; final isSelected = _selectedMessageIds.contains(message.id); // 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), GestureDetector( onLongPress: _isSelectMode ? null : () { setState(() { _isSelectMode = true; _selectedMessageIds.add(message.id); }); HapticFeedback.mediumImpact(); }, onTap: _isSelectMode ? () { setState(() { if (isSelected) { _selectedMessageIds.remove(message.id); if (_selectedMessageIds.isEmpty) { _isSelectMode = false; } } else { _selectedMessageIds.add(message.id); } }); } : null, child: Stack( children: [ MessageItem( key: ValueKey(message.id), message: message, isMyMessage: isMyMessage, isReadByAll: _messageService.isMessageReadByAll(message), onReply: _isSelectMode ? (_) {} : _startReply, onLongPress: _isSelectMode ? (_) {} : (msg) => _showMessageOptions(msg), onOpenLink: _openLink, ), if (_isSelectMode) Positioned( top: 8, right: isMyMessage ? 8 : null, left: isMyMessage ? null : 8, child: Container( width: 24, height: 24, decoration: BoxDecoration( shape: BoxShape.circle, color: isSelected ? AppColors.primary : Colors.grey.shade300, border: Border.all( color: isSelected ? AppColors.primary : Colors.grey.shade400, width: 2, ), ), child: isSelected ? Icon( Icons.check, color: Colors.white, size: 16, ) : null, ), ), ], ), ), ], ); }, ); } Widget _buildSearchResults() { if (_searchResults.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.search_off, size: 64, color: Colors.grey), SizedBox(height: 16), Text( 'Tidak ada hasil', style: TextStyle( fontSize: 16, color: Colors.grey[700], fontWeight: FontWeight.w500, ), ), SizedBox(height: 8), Text( 'Coba kata kunci lain', style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), ], ), ); } return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), itemCount: _searchResults.length, itemBuilder: (context, index) { final message = _searchResults[index]; final isMyMessage = message.senderUserId == _messageService.currentUserId; return Column( children: [ _buildDateHeader(message.createdAt), MessageItem( key: ValueKey('search-${message.id}'), message: message, isMyMessage: isMyMessage, isReadByAll: _messageService.isMessageReadByAll(message), onReply: _startReply, onLongPress: (msg) => _showMessageOptions(msg), onOpenLink: _openLink, ), ], ); }, ); } 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, ); } PreferredSizeWidget _buildAppBar() { return AppBar( backgroundColor: AppColors.primary, foregroundColor: AppColors.appBarForeground, elevation: 0, titleSpacing: 0, title: Row( children: [ // Add padding to the left of the group icon SizedBox(width: 16), CircleAvatar( backgroundColor: Colors.white24, radius: 20, child: Icon(Icons.group, color: Colors.white, size: 22), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'TaniSM4RT', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), Text( '${_getActiveUsersCount()} anggota', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w400), ), ], ), ), ], ), actions: [ // Select mode icon IconButton( icon: Icon(Icons.select_all), onPressed: () { setState(() { _isSelectMode = true; }); }, tooltip: 'Pilih pesan', ), // Search icon IconButton( icon: Icon(Icons.search), onPressed: () { setState(() { _isSearching = true; }); }, tooltip: 'Cari pesan', ), // Refresh icon IconButton( icon: Icon(Icons.refresh), onPressed: () { // Cancel any active subscriptions _messageService.dispose(); // Clear messages and reload _clearChatData(); // Reinitialize subscriptions Future.delayed(Duration(milliseconds: 500), () { if (mounted) { _messageService.setupMessagesSubscription( _onNewMessage, _onReadStatusUpdate, ); } }); }, tooltip: 'Refresh pesan', ), ], ); } PreferredSizeWidget _buildSelectModeAppBar() { return AppBar( backgroundColor: AppColors.primary, foregroundColor: AppColors.appBarForeground, elevation: 0, leading: IconButton( icon: Icon(Icons.close), onPressed: () { setState(() { _isSelectMode = false; _selectedMessageIds.clear(); }); }, ), title: Text( _selectedMessageIds.isEmpty ? 'Pilih pesan' : '${_selectedMessageIds.length} pesan dipilih', ), actions: [ if (_selectedMessageIds.isNotEmpty) ...[ IconButton( icon: Icon(Icons.delete), onPressed: () => _showDeleteSelectedConfirmation(), tooltip: 'Hapus pesan yang dipilih', ), ], IconButton( icon: Icon(Icons.select_all), onPressed: () => _toggleSelectAll(), tooltip: 'Pilih semua', ), ], ); } PreferredSizeWidget _buildSearchAppBar() { return AppBar( backgroundColor: AppColors.primary, foregroundColor: AppColors.appBarForeground, elevation: 0, automaticallyImplyLeading: false, titleSpacing: 0, title: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back), onPressed: _exitSearchMode, ), Expanded( child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Cari pesan...', hintStyle: TextStyle(color: Colors.white70), border: InputBorder.none, ), style: TextStyle(color: Colors.black), autofocus: true, textInputAction: TextInputAction.search, onSubmitted: (_) => _searchMessages(), ), ), IconButton( icon: Icon(Icons.clear), onPressed: () { _searchController.clear(); }, ), ], ), actions: [ IconButton(icon: Icon(Icons.search), onPressed: _searchMessages), ], ); } 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, ), ), ), ), ); } 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_outline, color: Colors.red), // title: const Text('Hapus untuk saya'), // onTap: () { // Navigator.pop(context); // _deleteMessageForMe(message); // }, // ), ListTile( leading: Icon(Icons.delete_forever, color: Colors.red), title: const Text('Hapus untuk semua orang'), onTap: () { Navigator.pop(context); _showDeleteConfirmation(message, forEveryone: true); }, ), ], ], ), ), ); } void _showDeleteConfirmation(Message message, {required bool forEveryone}) { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Konfirmasi Hapus'), content: Text( forEveryone ? 'Pesan ini akan dihapus untuk semua orang. Lanjutkan?' : 'Pesan ini akan dihapus hanya untuk Anda. Lanjutkan?', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Batal'), ), TextButton( style: TextButton.styleFrom(foregroundColor: Colors.red), onPressed: () { Navigator.pop(context); if (forEveryone) { _deleteMessageForEveryone(message); } else { _deleteMessageForMe(message); } }, child: Text('Hapus'), ), ], ), ); } void _deleteMessageForMe(Message message) { setState(() { _messages.removeWhere((m) => m.id == message.id); _deletedMessageIds.add(message.id); }); _saveDeletedMessageIds(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Pesan dihapus'), backgroundColor: Colors.green, duration: Duration(seconds: 2), ), ); } Future _deleteMessageForEveryone(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 { await _messageService.deleteMessage(message); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Pesan dihapus untuk semua orang'), backgroundColor: Colors.green, duration: Duration(seconds: 2), ), ); } catch (e) { print('[ERROR] Failed to delete message for everyone: $e'); // Don't reload messages, just show error ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal menghapus pesan dari server: $e'), backgroundColor: Colors.red, ), ); } } Future _clearChatData() async { setState(() { _isLoading = true; _messages.clear(); _hasMoreMessages = true; }); await _loadMessages(forceRefresh: true); } 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; } int _getActiveUsersCount() { if (_messages.isEmpty) return 0; final uniqueUserIds = {}; for (final message in _messages) { uniqueUserIds.add(message.senderUserId); } return uniqueUserIds.length; } // Load deleted message IDs from local storage Future _loadDeletedMessageIds() async { try { final prefs = await SharedPreferences.getInstance(); final deletedIds = prefs.getStringList('deleted_message_ids') ?? []; 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', _deletedMessageIds.toList(), ); } catch (e) { print('[ERROR] Failed to save deleted message IDs: $e'); } } // Select mode methods void _toggleSelectAll() { setState(() { if (_selectedMessageIds.length == _messages.length) { // If all are selected, unselect all _selectedMessageIds.clear(); } else { // Select all messages _selectedMessageIds = _messages.map((m) => m.id).toSet(); } }); } void _showDeleteSelectedConfirmation() { final currentUserId = _messageService.currentUserId; final allMine = _selectedMessageIds.every((id) { final message = _messages.firstWhere((m) => m.id == id); return message.senderUserId == currentUserId; }); showDialog( context: context, builder: (context) => AlertDialog( title: Text('Konfirmasi Hapus'), content: Text( allMine ? 'Hapus ${_selectedMessageIds.length} pesan yang dipilih?\n\nAnda dapat menghapus untuk semua orang karena semua pesan adalah milik Anda.' : 'Hapus ${_selectedMessageIds.length} pesan yang dipilih?\n\nPesan hanya akan dihapus untuk Anda.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Batal'), ), TextButton( style: TextButton.styleFrom(foregroundColor: Colors.red), onPressed: () { Navigator.pop(context); if (allMine) { _showDeleteForEveryoneOption(); } else { _deleteSelectedMessages(forEveryone: false); } }, child: Text('Hapus'), ), ], ), ); } void _showDeleteForEveryoneOption() { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Hapus pesan'), content: Text('Bagaimana Anda ingin menghapus pesan ini?'), actions: [ TextButton( onPressed: () { Navigator.pop(context); _deleteSelectedMessages(forEveryone: false); }, child: Text('Hapus untuk saya'), ), TextButton( style: TextButton.styleFrom(foregroundColor: Colors.red), onPressed: () { Navigator.pop(context); _deleteSelectedMessages(forEveryone: true); }, child: Text('Hapus untuk semua orang'), ), ], ), ); } Future _deleteSelectedMessages({required bool forEveryone}) async { final selectedIds = Set.from(_selectedMessageIds); final selectedMessages = _messages.where((m) => selectedIds.contains(m.id)).toList(); // First remove from UI for immediate feedback setState(() { _messages.removeWhere((m) => selectedIds.contains(m.id)); _deletedMessageIds.addAll(selectedIds); _selectedMessageIds.clear(); _isSelectMode = false; }); // Save to local storage _saveDeletedMessageIds(); // Show feedback ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${selectedIds.length} pesan dihapus'), backgroundColor: Colors.green, duration: Duration(seconds: 2), ), ); // If deleting for everyone, call API for each message if (forEveryone) { int successCount = 0; int failCount = 0; for (final message in selectedMessages) { try { await _messageService.deleteMessage(message); successCount++; } catch (e) { print('[ERROR] Failed to delete message for everyone: $e'); failCount++; } } if (failCount > 0) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal menghapus $failCount pesan dari server'), backgroundColor: Colors.red, ), ); } } } }