MIF_E31222656/lib/screens/community/community_screen.dart

1407 lines
41 KiB
Dart

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<CommunityScreen>
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<RefreshIndicatorState>();
// 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<Message> _messages = [];
List<Message> _searchResults = [];
Message? _replyToMessage;
File? _selectedImage;
Set<String> _deletedMessageIds = {};
Set<String> _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<void> _getCurrentUser() async {
final userInfo = await _profileService.getCurrentUser();
if (mounted) {
setState(() {
_currentUserEmail = userInfo.email;
_currentUsername = userInfo.username;
});
}
}
// Load messages
Future<void> _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<Message> 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(_messages);
if (mounted) {
setState(() {
// Filter out deleted messages
List<Message> 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
? 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 = <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(() {});
}
// Search
Future<void> _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<void> _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<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');
}
}
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<Color>(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<String>('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<String>(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<String>('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<void> _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<void> _clearChatData() async {
setState(() {
_isLoading = true;
_messages.clear();
_hasMoreMessages = true;
});
await _loadMessages(forceRefresh: true);
}
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;
}
int _getActiveUsersCount() {
if (_messages.isEmpty) return 0;
final uniqueUserIds = <String>{};
for (final message in _messages) {
uniqueUserIds.add(message.senderUserId);
}
return uniqueUserIds.length;
}
// Load deleted message IDs from local storage
Future<void> _loadDeletedMessageIds() async {
try {
final prefs = await SharedPreferences.getInstance();
final deletedIds = prefs.getStringList('deleted_message_ids') ?? [];
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',
_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<void> _deleteSelectedMessages({required bool forEveryone}) async {
final selectedIds = Set<String>.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,
),
);
}
}
}
}