import 'dart:async'; import 'dart:convert'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../services/whatsapp_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(); } // ── 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) => 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) => 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, ); // Find existing conversation and update it final index = conversations.indexWhere((c) => c.id == chatId); if (index != -1) { final existing = conversations[index]; final updated = existing.copyWith( lastMessage: lastMsg, timestamp: lastMsg.timestamp, unreadCount: lastMsg.fromMe ? existing.unreadCount : 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) => ConversationModel.fromJson(c as Map))); _saveConversationsToCache(data); } } catch (_) {} } }