import 'dart:async'; import 'dart:convert'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../services/whatsapp_service.dart'; import '../services/contacts_service.dart'; import '../models/conversation_model.dart'; class ConversationsController extends GetxController { final WhatsAppService _svc = Get.find(); final conversations = [].obs; final isLoading = false.obs; final errorMessage = Rx(null); StreamSubscription? _eventSub; StreamSubscription? _readySub; Timer? _searchDebounce; @override void onInit() { super.onInit(); // Load local cached conversations first for instant UI response _loadCachedConversations(); // Load conversations initially if already ready if (_svc.isWaReady.value) { loadConversations(); } // React to WhatsApp ready status changes _readySub = _svc.isWaReady.listen((ready) { if (ready) { loadConversations(); } else { conversations.clear(); } }); // Listen to push events from the server _eventSub = _svc.events.listen(_onPushEvent); } @override void onClose() { _eventSub?.cancel(); _readySub?.cancel(); _searchDebounce?.cancel(); super.onClose(); } // Helper to resolve contact names from the local address book ConversationModel _resolveContactNames(ConversationModel c) { if (c.isGroup) return c; // Skip group chats final parts = c.id.split('@'); final phoneNumber = parts[0]; final contactsService = Get.find(); // Try matching using c.name (which has the formatted number string, e.g. "+962 7 8152 3783") String matchedName = contactsService.getContactName(c.name, c.name); // If it didn't match (i.e. returned c.name), try matching using phoneNumber if (matchedName == c.name) { matchedName = contactsService.getContactName(phoneNumber, c.name); } return c.copyWith(name: matchedName); } void clearUnreadCount(String chatId) { final index = conversations.indexWhere((c) => c.id == chatId); if (index != -1) { conversations[index] = conversations[index].copyWith(unreadCount: 0); _saveConversationsToCache(conversations.map((c) => c.toJson()).toList()); } } // ── Local Caching ──────────────────────────────────────────────────────── Future _loadCachedConversations() async { try { final prefs = await SharedPreferences.getInstance(); final cached = prefs.getString('cached_conversations'); if (cached != null) { final List decoded = jsonDecode(cached); conversations.assignAll(decoded.map((c) => _resolveContactNames(ConversationModel.fromJson(c as Map)))); print('[CACHE] Loaded ${conversations.length} conversations instantly.'); } } catch (e) { print('[CACHE ERROR] $e'); } } Future _saveConversationsToCache(List rawData) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('cached_conversations', jsonEncode(rawData)); print('[CACHE] Saved ${rawData.length} conversations to local cache.'); } catch (e) { print('[CACHE SAVE ERROR] $e'); } } // ── Load Conversations ─────────────────────────────────────────────────── Future loadConversations() async { if (!_svc.isWaReady.value || isLoading.value) return; isLoading.value = true; errorMessage.value = null; try { final res = await _svc.getConversations(); if (res['type'] == 'conversations') { final List data = res['data'] ?? []; conversations.assignAll(data.map((c) => _resolveContactNames(ConversationModel.fromJson(c as Map)))); _saveConversationsToCache(data); } else { errorMessage.value = res['message'] ?? 'Failed to load conversations'; } } catch (e) { errorMessage.value = e.toString(); } finally { isLoading.value = false; } } // ── Search Conversations ────────────────────────────────────────────────── void search(String query) { _searchDebounce?.cancel(); if (query.trim().isEmpty) { loadConversations(); return; } _searchDebounce = Timer(const Duration(milliseconds: 400), () async { isLoading.value = true; try { final res = await _svc.searchConversations(query); if (res['type'] == 'conversations') { final List data = res['data'] ?? []; conversations.assignAll(data.map((c) => ConversationModel.fromJson(c as Map))); } } catch (e) { print('[SEARCH ERROR] $e'); } finally { isLoading.value = false; } }); } // ── Handle Incoming Socket Push Events ────────────────────────────────── void _onPushEvent(Map event) { final type = event['type'] as String?; if (type == null) return; switch (type) { case 'new_message': final chatId = event['chatId'] as String?; final msgData = event['data'] as Map?; if (chatId == null || msgData == null) return; // Create the LastMessage object final lastMsg = LastMessageModel( body: msgData['body'] ?? '', timestamp: msgData['timestamp'] ?? 0, fromMe: msgData['fromMe'] ?? false, hasMedia: msgData['hasMedia'] ?? false, ack: msgData['ack'] ?? 0, ); // Find existing conversation and update it final index = conversations.indexWhere((c) => c.id == chatId); if (index != -1) { final existing = conversations[index]; final isCurrentActiveChat = _svc.activeChatId.value == chatId; final updated = existing.copyWith( lastMessage: lastMsg, timestamp: lastMsg.timestamp, unreadCount: (lastMsg.fromMe || isCurrentActiveChat) ? 0 : existing.unreadCount + 1, ); conversations.removeAt(index); conversations.insert(0, updated); } else { // If conversation is not loaded, trigger a silent full reload to fetch it loadConversationsSilently(); } break; case 'message_ack': final messageId = event['messageId'] as String?; final chatId = event['chatId'] as String?; final ack = event['ack'] as int?; if (chatId == null || messageId == null || ack == null) return; // If the last message in a conversation was acknowledged, update it final index = conversations.indexWhere((c) => c.id == chatId); if (index != -1) { // We can refresh silently if it is the current conversation's last message. // Since ack is simple, a quick silent refresh guarantees correct ack state. loadConversationsSilently(); } break; case 'ready': case 'authenticated': loadConversationsSilently(); break; case 'disconnected': conversations.clear(); break; } } Future loadConversationsSilently() async { if (!_svc.isWaReady.value) return; try { final res = await _svc.getConversations(); if (res['type'] == 'conversations') { final List data = res['data'] ?? []; conversations.assignAll(data.map((c) => _resolveContactNames(ConversationModel.fromJson(c as Map)))); _saveConversationsToCache(data); } } catch (_) {} } }