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

1258 lines
38 KiB
Dart

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<GroupChatScreenState>? screenKey;
const GroupChatScreen({
super.key,
required this.groupId,
this.isInTabView = false,
this.screenKey,
});
@override
GroupChatScreenState createState() => GroupChatScreenState();
}
class GroupChatScreenState extends State<GroupChatScreen>
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<RefreshIndicatorState>();
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<GroupMessage> _messages = [];
GroupMessage? _replyToMessage;
File? _selectedImage;
XFile? _selectedImageFile;
String? _selectedImagePath;
Set<String> _deletedMessageIds = {};
final Set<String> _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<void> _getCurrentUser() async {
final userInfo = await _profileService.getCurrentUser();
if (mounted) {
setState(() {
_currentUserEmail = userInfo.email;
_currentUsername = userInfo.username;
});
}
}
// Load group details
Future<void> _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<void> _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<GroupMessage> 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 = <String>{};
_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<void> _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<GroupMessage> 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 = <String>{};
_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<void> _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 = <String>{};
_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 = <String>{};
_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<void> _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<void> _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<void> _loadDeletedMessageIds() async {
try {
final prefs = await SharedPreferences.getInstance();
final deletedIds =
prefs.getStringList('deleted_message_ids_${widget.groupId}') ?? [];
setState(() {
_deletedMessageIds = Set<String>.from(deletedIds);
});
} catch (e) {
print('[ERROR] Failed to load deleted message IDs: $e');
}
}
// Save deleted message IDs to local storage
Future<void> _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<void> _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<Color>(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<String>('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<String>(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<void> _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<dynamic> 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');
}
}
}