1046 lines
32 KiB
Dart
1046 lines
32 KiB
Dart
// ignore_for_file: avoid_print
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/components/group_chat_screen.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/components/simple_news_tab.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/components/farming_guide_tab.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/components/group_selector.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/models/group.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/services/group_service.dart';
|
|
import 'package:tugas_akhir_supabase/core/theme/app_colors.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/components/group_card.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
class EnhancedCommunityScreen extends StatefulWidget {
|
|
const EnhancedCommunityScreen({super.key});
|
|
|
|
@override
|
|
_EnhancedCommunityScreenState createState() =>
|
|
_EnhancedCommunityScreenState();
|
|
}
|
|
|
|
class _EnhancedCommunityScreenState extends State<EnhancedCommunityScreen>
|
|
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
|
// View state
|
|
late TabController _tabController;
|
|
int _currentIndex = 0;
|
|
bool _isLoadingGroups = true;
|
|
String? _selectedGroupId;
|
|
bool _showGroupSelector = false;
|
|
|
|
// Group data
|
|
List<Group> _userGroups = [];
|
|
Set<String> _userGroupIds = {};
|
|
String? _loadingGroupId; // Track grup yang sedang di-join/leave
|
|
bool _isJoiningGroup = false;
|
|
|
|
// Tambahkan deklarasi channel realtime
|
|
RealtimeChannel? _groupRealtimeChannel;
|
|
|
|
// Add a key for the GroupChatScreen
|
|
final GlobalKey<GroupChatScreenState> _groupChatKey =
|
|
GlobalKey<GroupChatScreenState>();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_tabController = TabController(
|
|
length: 3,
|
|
vsync: this,
|
|
initialIndex: 0, // Default to first tab
|
|
);
|
|
_tabController.addListener(_handleTabChange);
|
|
|
|
// Panggil langsung tanpa Future.delayed
|
|
_safeLoadGroups();
|
|
|
|
_preloadDefaultGroupMessages();
|
|
Future.delayed(Duration(seconds: 2), () {
|
|
if (mounted) {
|
|
_forceRefreshAllMessages();
|
|
}
|
|
});
|
|
_setupGroupRealtimeSubscription();
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
// Jangan reload groups di sini, karena bisa menyebabkan multiple loads
|
|
// _loadUserGroups();
|
|
}
|
|
|
|
Future<void> _safeLoadGroups() async {
|
|
try {
|
|
// Set safety timer lebih awal
|
|
Future.delayed(Duration(seconds: 3), () {
|
|
if (mounted && _isLoadingGroups) {
|
|
print('[WARNING] Safety timer triggered - forcing load completion');
|
|
setState(() {
|
|
_isLoadingGroups = false;
|
|
if (_userGroups.isEmpty) {
|
|
_createFallbackGroup();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Coba muat grup dengan timeout
|
|
await _loadUserGroups().timeout(
|
|
Duration(seconds: 3),
|
|
onTimeout: () {
|
|
print('[WARNING] _loadUserGroups timed out');
|
|
if (mounted && _isLoadingGroups) {
|
|
setState(() {
|
|
_isLoadingGroups = false;
|
|
if (_userGroups.isEmpty) {
|
|
_createFallbackGroup();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
);
|
|
} catch (e) {
|
|
print('[ERROR] Error in _safeLoadGroups: $e');
|
|
if (mounted && _isLoadingGroups) {
|
|
setState(() {
|
|
_isLoadingGroups = false;
|
|
if (_userGroups.isEmpty) {
|
|
_createFallbackGroup();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Metode untuk membuat grup fallback
|
|
void _createFallbackGroup() {
|
|
// Gunakan UUID yang valid, bukan string timestamp
|
|
final fallbackGroup = Group(
|
|
id: '00000000-0000-0000-0000-000000000001', // UUID valid
|
|
name: 'Grup Tani Umum',
|
|
description: 'Grup diskusi umum untuk petani',
|
|
createdBy: 'system',
|
|
isDefault: true,
|
|
isPublic: true,
|
|
);
|
|
_userGroups = [fallbackGroup];
|
|
_userGroupIds = {fallbackGroup.id};
|
|
_selectedGroupId = fallbackGroup.id;
|
|
}
|
|
|
|
Future<void> _loadUserGroups() async {
|
|
// Perbaiki: hanya return jika sedang loading dan data sudah ada
|
|
if (_isLoadingGroups && _userGroups.isNotEmpty) {
|
|
print('[DEBUG] Preventing multiple loads - load already in progress');
|
|
return;
|
|
}
|
|
|
|
print('[DEBUG] Starting load process - setting loading state');
|
|
setState(() => _isLoadingGroups = true);
|
|
|
|
// Store the current group ID before refresh to maintain selection
|
|
final previouslySelectedGroupId = _selectedGroupId;
|
|
print('[DEBUG] Saving current selected group: $previouslySelectedGroupId');
|
|
|
|
try {
|
|
// Gunakan fungsi yang sudah dibuat di Supabase
|
|
final supabase = Supabase.instance.client;
|
|
|
|
// Verify user authentication dengan timeout
|
|
User? currentUser;
|
|
try {
|
|
currentUser = supabase.auth.currentUser;
|
|
if (currentUser == null) {
|
|
print('[ERROR] User not authenticated');
|
|
throw Exception('User not authenticated');
|
|
}
|
|
} catch (e) {
|
|
print('[ERROR] Error getting current user: $e');
|
|
throw Exception('Error getting current user');
|
|
}
|
|
|
|
print('[DEBUG] Using SQL functions from Supabase');
|
|
List<Group> allGroups = [];
|
|
Set<String> userGroupIds = {};
|
|
Group? defaultGroup;
|
|
|
|
// Get all groups with member count dengan timeout
|
|
try {
|
|
print('[DEBUG] Calling get_all_groups_with_member_count() function');
|
|
final result = await supabase
|
|
.rpc('get_all_groups_with_member_count')
|
|
.timeout(
|
|
Duration(seconds: 2),
|
|
onTimeout: () {
|
|
print('[WARNING] get_all_groups_with_member_count timed out');
|
|
return null;
|
|
},
|
|
);
|
|
|
|
if (result != null) {
|
|
for (final item in result) {
|
|
try {
|
|
final group = Group.fromMap(item);
|
|
allGroups.add(group);
|
|
|
|
// Mencatat grup default
|
|
if (group.isDefault) {
|
|
defaultGroup = group;
|
|
print('[DEBUG] Found default group: ${group.name}');
|
|
}
|
|
} catch (e) {
|
|
print('[ERROR] Failed to parse group: $e');
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('[ERROR] Failed to load groups with count: $e');
|
|
}
|
|
|
|
// Get user memberships dengan timeout
|
|
try {
|
|
print('[DEBUG] Getting user memberships with get_user_groups_simple()');
|
|
final userGroupsResult = await supabase
|
|
.rpc('get_user_groups_simple')
|
|
.timeout(
|
|
Duration(seconds: 2),
|
|
onTimeout: () {
|
|
print('[WARNING] get_user_groups_simple timed out');
|
|
return null;
|
|
},
|
|
);
|
|
|
|
if (userGroupsResult != null) {
|
|
for (final item in userGroupsResult) {
|
|
userGroupIds.add(item['id'] as String);
|
|
}
|
|
print('[DEBUG] User is member of ${userGroupIds.length} groups');
|
|
}
|
|
} catch (e) {
|
|
print('[ERROR] Failed to load user memberships: $e');
|
|
}
|
|
|
|
// Fallback - always ensure at least one group
|
|
if (allGroups.isEmpty) {
|
|
print('[DEBUG] No groups loaded, creating fallback group');
|
|
final fallbackGroup = Group(
|
|
id: '00000000-0000-0000-0000-000000000001', // UUID valid
|
|
name: 'Grup Tani Umum',
|
|
description: 'Grup diskusi umum untuk petani',
|
|
createdBy: 'system',
|
|
isDefault: true,
|
|
isPublic: true,
|
|
);
|
|
allGroups = [fallbackGroup];
|
|
defaultGroup = fallbackGroup;
|
|
}
|
|
|
|
// Update UI state
|
|
if (mounted) {
|
|
setState(() {
|
|
_userGroups = allGroups;
|
|
_userGroupIds = userGroupIds;
|
|
_isLoadingGroups = false;
|
|
|
|
// Check if the previously selected group still exists in the updated groups
|
|
bool previousGroupStillExists = false;
|
|
if (previouslySelectedGroupId != null) {
|
|
previousGroupStillExists = _userGroups.any(
|
|
(group) => group.id == previouslySelectedGroupId,
|
|
);
|
|
}
|
|
|
|
// Maintain the previously selected group if it still exists
|
|
if (previousGroupStillExists) {
|
|
_selectedGroupId = previouslySelectedGroupId;
|
|
print(
|
|
'[DEBUG] Maintained previously selected group: $_selectedGroupId',
|
|
);
|
|
} else {
|
|
// Otherwise select a default group
|
|
if (defaultGroup != null) {
|
|
_selectedGroupId = defaultGroup.id;
|
|
print(
|
|
'[DEBUG] Default group auto-selected: ${defaultGroup.name}',
|
|
);
|
|
} else if (allGroups.isNotEmpty) {
|
|
// Fallback: pilih grup yang user ikuti atau grup pertama
|
|
final memberGroup = allGroups.firstWhere(
|
|
(g) => userGroupIds.contains(g.id),
|
|
orElse: () => allGroups.first,
|
|
);
|
|
_selectedGroupId = memberGroup.id;
|
|
print('[DEBUG] Fallback group selected: ${memberGroup.name}');
|
|
}
|
|
}
|
|
});
|
|
print('[DEBUG] State updated with ${allGroups.length} groups');
|
|
|
|
// Immediately force message load if a group is selected
|
|
if (_selectedGroupId != null && _groupChatKey.currentState != null) {
|
|
_triggerImmediateMessageLoad();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('[ERROR] Critical error in load process: $e');
|
|
// Ensure we complete loading with at least one fallback group
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoadingGroups = false;
|
|
if (_userGroups.isEmpty) {
|
|
_createFallbackGroup();
|
|
}
|
|
// Try to restore the previously selected group if possible
|
|
if (previouslySelectedGroupId != null &&
|
|
_userGroups.any(
|
|
(group) => group.id == previouslySelectedGroupId,
|
|
)) {
|
|
_selectedGroupId = previouslySelectedGroupId;
|
|
print(
|
|
'[DEBUG] Restored previously selected group after error: $_selectedGroupId',
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void _handleGroupSelected(Group group) {
|
|
setState(() {
|
|
_selectedGroupId = group.id;
|
|
_showGroupSelector = false;
|
|
});
|
|
}
|
|
|
|
void _toggleGroupSelector() {
|
|
setState(() {
|
|
_showGroupSelector = !_showGroupSelector;
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// Unsubscribe channel realtime
|
|
_groupRealtimeChannel?.unsubscribe();
|
|
_tabController.removeListener(_handleTabChange);
|
|
_tabController.dispose();
|
|
// Clean up any active subscriptions
|
|
if (_selectedGroupId != null && _groupChatKey.currentState != null) {
|
|
_groupChatKey.currentState!.disposeResources();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
void _handleTabChange() {
|
|
if (_tabController.indexIsChanging) {
|
|
// You can add analytics or other logic here when tabs change
|
|
setState(() {
|
|
_currentIndex = _tabController.index;
|
|
|
|
// Hide group selector when switching tabs
|
|
_showGroupSelector = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: AppColors.primary,
|
|
foregroundColor: Colors.white,
|
|
toolbarHeight: 56,
|
|
elevation: 0,
|
|
title: Text(_getTabTitle(_currentIndex)),
|
|
bottom: PreferredSize(
|
|
preferredSize: const Size.fromHeight(36),
|
|
child: TabBar(
|
|
controller: _tabController,
|
|
indicatorColor: Colors.white,
|
|
indicatorWeight: 3,
|
|
labelColor: Colors.white,
|
|
unselectedLabelColor: Colors.white70,
|
|
labelStyle: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
labelPadding: const EdgeInsets.symmetric(vertical: 8),
|
|
tabs: const [
|
|
Tab(
|
|
height: 28,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.chat_bubble, size: 18),
|
|
SizedBox(width: 4),
|
|
Text('Diskusi'),
|
|
],
|
|
),
|
|
),
|
|
Tab(
|
|
height: 28,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.article, size: 18),
|
|
SizedBox(width: 4),
|
|
Text('Berita'),
|
|
],
|
|
),
|
|
),
|
|
Tab(
|
|
height: 28,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.eco, size: 18),
|
|
SizedBox(width: 4),
|
|
Text('Panduan'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions:
|
|
_currentIndex == 0
|
|
? [
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
tooltip: 'Refresh',
|
|
onPressed: _refreshGroupsPreservingSelection,
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// Main content
|
|
Expanded(
|
|
child: TabBarView(
|
|
physics: const NeverScrollableScrollPhysics(), // Mencegah swipe
|
|
controller: _tabController,
|
|
children: [
|
|
// Chat tab - Tampilkan daftar grup terlebih dahulu
|
|
_buildGroupsListTab(),
|
|
|
|
// News tab
|
|
const SimpleNewsTab(),
|
|
|
|
// Panduan Budidaya tab
|
|
const FarmingGuideTab(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Widget untuk menampilkan daftar grup
|
|
Widget _buildGroupsListTab() {
|
|
if (_isLoadingGroups) {
|
|
return const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'Memuat grup...',
|
|
style: TextStyle(fontSize: 16, color: Colors.grey),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_userGroups.isEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.groups, size: 80, color: Colors.grey.shade300),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Tidak ada grup tersedia',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Periksa koneksi internet atau coba refresh',
|
|
style: TextStyle(color: Colors.grey.shade600),
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton.icon(
|
|
onPressed: _refreshGroupsPreservingSelection,
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Refresh'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primary,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 12,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
// Pilihan grup
|
|
if (_selectedGroupId != null && _userGroups.isNotEmpty)
|
|
_buildGroupSelector(),
|
|
|
|
// Daftar grup atau chat grup
|
|
Expanded(
|
|
child:
|
|
_selectedGroupId == null
|
|
? _buildGroupsList()
|
|
: GroupChatScreen(
|
|
key: _groupChatKey,
|
|
screenKey: _groupChatKey,
|
|
isInTabView: true,
|
|
groupId: _selectedGroupId!,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildGroupsList() {
|
|
return RefreshIndicator(
|
|
onRefresh: _refreshGroupsPreservingSelection,
|
|
child: ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: _userGroups.length,
|
|
itemBuilder: (context, index) {
|
|
final group = _userGroups[index];
|
|
final isUserMember = _userGroupIds.contains(group.id);
|
|
|
|
// Gunakan GroupCard yang baru dibuat
|
|
return GroupCard(
|
|
group: group,
|
|
isUserMember: isUserMember,
|
|
onTap: () => _selectGroup(group.id),
|
|
onJoinLeave:
|
|
() =>
|
|
isUserMember ? _leaveGroup(group.id) : _joinGroup(group.id),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// Navigasi ke halaman chat grup
|
|
void _navigateToGroupChat(Group group) {
|
|
if (_selectedGroupId != null) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder:
|
|
(context) => GroupChatScreen(
|
|
isInTabView: false,
|
|
groupId: _selectedGroupId!,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Selector untuk grup yang dipilih
|
|
Widget _buildGroupSelector() {
|
|
if (_selectedGroupId == null) return const SizedBox.shrink();
|
|
|
|
// Cari grup yang dipilih
|
|
final selectedGroup = _userGroups.firstWhere(
|
|
(g) => g.id == _selectedGroupId,
|
|
orElse: () => _userGroups.first,
|
|
);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
InkWell(
|
|
onTap: () => setState(() => _selectedGroupId = null),
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: Icon(Icons.arrow_back, color: AppColors.primary),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
CircleAvatar(
|
|
backgroundColor: AppColors.primary.withOpacity(0.2),
|
|
radius: 20,
|
|
child: Text(
|
|
selectedGroup.name.isNotEmpty
|
|
? selectedGroup.name[0].toUpperCase()
|
|
: 'G',
|
|
style: const TextStyle(
|
|
color: AppColors.primary,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
selectedGroup.name,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
Text(
|
|
'${selectedGroup.memberCount} anggota',
|
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _refreshGroupsPreservingSelection,
|
|
color: AppColors.primary,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Pilih grup untuk chat
|
|
void _selectGroup(String groupId) {
|
|
setState(() {
|
|
_selectedGroupId = groupId;
|
|
_showGroupSelector = true;
|
|
});
|
|
|
|
// Immediate refresh without delay
|
|
_triggerImmediateMessageLoad();
|
|
|
|
// Also set multiple delayed refreshes for redundancy
|
|
for (var delay in [100, 500, 1000, 2000]) {
|
|
Future.delayed(Duration(milliseconds: delay), () {
|
|
if (mounted) {
|
|
_triggerImmediateMessageLoad();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Trigger immediate message loading in the chat screen
|
|
void _triggerImmediateMessageLoad() {
|
|
try {
|
|
if (_groupChatKey.currentState != null) {
|
|
print('[DEBUG] Triggering immediate message load');
|
|
_groupChatKey.currentState!.forceRefresh();
|
|
}
|
|
} catch (e) {
|
|
print('[ERROR] Error triggering message load: $e');
|
|
}
|
|
}
|
|
|
|
// Gabung ke grup
|
|
Future<void> _joinGroup(String groupId) async {
|
|
if (_isJoiningGroup) return;
|
|
|
|
setState(() {
|
|
_isJoiningGroup = true;
|
|
_loadingGroupId = groupId;
|
|
});
|
|
|
|
try {
|
|
final supabase = Supabase.instance.client;
|
|
final userId = supabase.auth.currentUser?.id;
|
|
|
|
if (userId == null) {
|
|
throw Exception('User not authenticated');
|
|
}
|
|
|
|
// Coba bergabung dengan grup
|
|
await supabase.from('group_members').insert({
|
|
'group_id': groupId,
|
|
'user_id': userId,
|
|
'role': 'member',
|
|
'is_active': true,
|
|
});
|
|
|
|
// Refresh data
|
|
await _loadUserGroups();
|
|
|
|
// Set grup yang baru diikuti sebagai grup yang dipilih
|
|
setState(() {
|
|
_selectedGroupId = groupId;
|
|
_userGroupIds.add(groupId);
|
|
});
|
|
|
|
_showSuccessSnackBar('Berhasil bergabung dengan grup');
|
|
} catch (e) {
|
|
print('[ERROR] Failed to join group: $e');
|
|
_showErrorSnackBar('Gagal bergabung dengan grup: ${e.toString()}');
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isJoiningGroup = false;
|
|
_loadingGroupId = null;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keluar dari grup
|
|
Future<void> _leaveGroup(String groupId) async {
|
|
// Tampilkan konfirmasi
|
|
final shouldLeave =
|
|
await showDialog<bool>(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
title: const Text('Keluar dari Grup'),
|
|
content: const Text(
|
|
'Apakah Anda yakin ingin keluar dari grup ini?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Batal'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Keluar'),
|
|
),
|
|
],
|
|
),
|
|
) ??
|
|
false;
|
|
|
|
if (!shouldLeave) return;
|
|
|
|
setState(() {
|
|
_isJoiningGroup = true;
|
|
_loadingGroupId = groupId;
|
|
});
|
|
|
|
try {
|
|
final supabase = Supabase.instance.client;
|
|
final userId = supabase.auth.currentUser?.id;
|
|
|
|
if (userId == null) {
|
|
throw Exception('User not authenticated');
|
|
}
|
|
|
|
// Nonaktifkan keanggotaan
|
|
await supabase
|
|
.from('group_members')
|
|
.update({'is_active': false})
|
|
.eq('group_id', groupId)
|
|
.eq('user_id', userId);
|
|
|
|
// Refresh data
|
|
await _loadUserGroups();
|
|
|
|
// Jika grup saat ini yang dikeluar, kembalikan ke daftar grup
|
|
if (_selectedGroupId == groupId) {
|
|
setState(() {
|
|
_selectedGroupId = null;
|
|
});
|
|
}
|
|
|
|
_showSuccessSnackBar('Berhasil keluar dari grup');
|
|
} catch (e) {
|
|
print('[ERROR] Failed to leave group: $e');
|
|
_showErrorSnackBar('Gagal keluar dari grup: ${e.toString()}');
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isJoiningGroup = false;
|
|
_loadingGroupId = null;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
String _getTabTitle(int index) {
|
|
switch (index) {
|
|
case 0:
|
|
return 'Diskusi';
|
|
case 1:
|
|
return 'Berita';
|
|
case 2:
|
|
return 'Panduan Pertanian';
|
|
default:
|
|
return 'Komunitas';
|
|
}
|
|
}
|
|
|
|
void _showLoadingDialog(String message) {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const CircularProgressIndicator(),
|
|
const SizedBox(height: 16),
|
|
Text(message),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showErrorSnackBar(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message), backgroundColor: Colors.red),
|
|
);
|
|
}
|
|
|
|
void _showSuccessSnackBar(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message), backgroundColor: Colors.green),
|
|
);
|
|
}
|
|
|
|
Future<void> _emergencyDisableRLS() async {
|
|
try {
|
|
final supabase = Supabase.instance.client;
|
|
|
|
_showLoadingDialog('Mencoba perbaiki database...');
|
|
|
|
// Try to disable RLS on problematic tables
|
|
final sql = '''
|
|
ALTER TABLE group_members DISABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE groups DISABLE ROW LEVEL SECURITY;
|
|
''';
|
|
|
|
await supabase.rpc('exec_sql', params: {'sql': sql});
|
|
print('[DEBUG] Emergency disabled RLS on tables');
|
|
|
|
Navigator.of(context).pop(); // Close loading dialog
|
|
_showSuccessSnackBar('Database diperbaiki, mencoba ulang...');
|
|
|
|
// Reload groups
|
|
_loadUserGroups();
|
|
} catch (e) {
|
|
Navigator.of(context).pop(); // Close loading dialog
|
|
print('[ERROR] Failed to disable RLS: $e');
|
|
_showErrorSnackBar('Gagal memperbaiki database: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// Make a direct database query to preload messages as early as possible
|
|
Future<void> _preloadDefaultGroupMessages() async {
|
|
try {
|
|
print('[PRELOAD] Attempting to preload default group messages');
|
|
final supabase = Supabase.instance.client;
|
|
|
|
// First check if there are any messages in the default group
|
|
final response = await supabase
|
|
.rpc(
|
|
'count_messages_in_group',
|
|
params: {'group_id_param': '00000000-0000-0000-0000-000000000001'},
|
|
)
|
|
.timeout(Duration(seconds: 2));
|
|
|
|
final count = response as int? ?? 0;
|
|
|
|
// If no messages, create a welcome message
|
|
if (count == 0) {
|
|
print('[PRELOAD] No messages found, creating welcome message');
|
|
final userId = supabase.auth.currentUser?.id;
|
|
|
|
if (userId != null) {
|
|
// Get username from profiles
|
|
String username = 'User';
|
|
String email = '';
|
|
|
|
try {
|
|
final profileData = await supabase
|
|
.from('profiles')
|
|
.select('username, email')
|
|
.eq('user_id', userId)
|
|
.single()
|
|
.timeout(Duration(seconds: 2));
|
|
|
|
username = profileData['username'] ?? 'User';
|
|
email = profileData['email'] ?? '';
|
|
} catch (e) {
|
|
print('[ERROR] Failed to get profile: $e');
|
|
}
|
|
|
|
// Create welcome message using an RPC function that bypasses RLS
|
|
try {
|
|
await supabase.rpc(
|
|
'create_welcome_message',
|
|
params: {
|
|
'message_content':
|
|
'Selamat datang di Grup TaniSM4RT! Ini adalah pesan otomatis untuk memulai percakapan.',
|
|
'user_id_param': userId,
|
|
'username_param': username,
|
|
'email_param': email,
|
|
'group_id_param': '00000000-0000-0000-0000-000000000001',
|
|
},
|
|
);
|
|
print('[PRELOAD] Created welcome message successfully via RPC');
|
|
} catch (e) {
|
|
print('[ERROR] Failed to create welcome message via RPC: $e');
|
|
|
|
// Fallback to direct query with service role if available
|
|
try {
|
|
final adminClient = Supabase.instance.client;
|
|
await adminClient.from('messages').insert({
|
|
'id': Uuid().v4(),
|
|
'content':
|
|
'Selamat datang di Grup TaniSM4RT! Ini adalah pesan otomatis untuk memulai percakapan.',
|
|
'sender_user_id': userId,
|
|
'sender_username': username,
|
|
'sender_email': email,
|
|
'group_id': '00000000-0000-0000-0000-000000000001',
|
|
'created_at': DateTime.now().toIso8601String(),
|
|
});
|
|
print('[PRELOAD] Created welcome message via admin client');
|
|
} catch (e) {
|
|
print(
|
|
'[ERROR] Failed to create welcome message via admin client: $e',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
print(
|
|
'[PRELOAD] Found $count existing messages, no need to create welcome message',
|
|
);
|
|
}
|
|
} catch (e) {
|
|
print('[ERROR] Error in preloading messages: $e');
|
|
}
|
|
}
|
|
|
|
// Force refresh to fix issues with message loading
|
|
Future<void> _forceRefreshAllMessages() async {
|
|
try {
|
|
if (!mounted) return;
|
|
|
|
print('[FORCE] Attempting force refresh on GroupChatScreen');
|
|
|
|
// Wait a second to make sure GroupChatScreen is mounted
|
|
Future.delayed(Duration(seconds: 2), () {
|
|
if (_groupChatKey.currentState != null) {
|
|
print('[FORCE] Executing force refresh on GroupChatScreen');
|
|
_groupChatKey.currentState?.setupRealTimeSubscription();
|
|
// This is a public method that exists in GroupChatScreenState
|
|
_groupChatKey.currentState?.forceRefresh();
|
|
} else {
|
|
print('[FORCE] GroupChatScreen not yet mounted, delaying refresh');
|
|
// Try once more after a delay
|
|
Future.delayed(Duration(seconds: 1), () {
|
|
if (_groupChatKey.currentState != null) {
|
|
_groupChatKey.currentState?.setupRealTimeSubscription();
|
|
_groupChatKey.currentState?.forceRefresh();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} catch (e) {
|
|
print('[ERROR] Error in force refresh: $e');
|
|
}
|
|
}
|
|
|
|
// This method refreshes groups but preserves the current selection
|
|
Future<void> _refreshGroupsPreservingSelection() async {
|
|
// Store the current group ID before refresh to maintain selection
|
|
final previouslySelectedGroupId = _selectedGroupId;
|
|
print(
|
|
'[DEBUG] Refreshing while preserving selection: $previouslySelectedGroupId',
|
|
);
|
|
|
|
// Call the regular load method
|
|
await _loadUserGroups();
|
|
|
|
// If the previously selected group exists but wasn't selected, select it now
|
|
if (previouslySelectedGroupId != null && mounted) {
|
|
final stillExists = _userGroups.any(
|
|
(g) => g.id == previouslySelectedGroupId,
|
|
);
|
|
if (stillExists && _selectedGroupId != previouslySelectedGroupId) {
|
|
setState(() {
|
|
_selectedGroupId = previouslySelectedGroupId;
|
|
print('[DEBUG] Restored selection to: $_selectedGroupId');
|
|
});
|
|
|
|
// Force refresh the messages in the restored group
|
|
if (_groupChatKey.currentState != null) {
|
|
_triggerImmediateMessageLoad();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tambahkan fungsi setup subscription realtime
|
|
void _setupGroupRealtimeSubscription() {
|
|
final supabase = Supabase.instance.client;
|
|
_groupRealtimeChannel = supabase.channel('public:groups_and_members');
|
|
|
|
_groupRealtimeChannel!
|
|
.onPostgresChanges(
|
|
event: PostgresChangeEvent.all,
|
|
schema: 'public',
|
|
table: 'groups',
|
|
callback: (payload) {
|
|
print('[REALTIME] Groups table changed: $payload');
|
|
_loadUserGroups();
|
|
},
|
|
)
|
|
.onPostgresChanges(
|
|
event: PostgresChangeEvent.all,
|
|
schema: 'public',
|
|
table: 'group_members',
|
|
callback: (payload) {
|
|
print('[REALTIME] Group members table changed: $payload');
|
|
_loadUserGroups();
|
|
},
|
|
);
|
|
|
|
_groupRealtimeChannel!.subscribe();
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|