210 lines
7.1 KiB
Dart
210 lines
7.1 KiB
Dart
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<WhatsAppService>();
|
|
|
|
final conversations = <ConversationModel>[].obs;
|
|
final isLoading = false.obs;
|
|
final errorMessage = Rx<String?>(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 matchedName = Get.find<ContactsService>().getContactName(phoneNumber, c.name);
|
|
return c.copyWith(name: matchedName);
|
|
}
|
|
|
|
// ── Local Caching ────────────────────────────────────────────────────────
|
|
Future<void> _loadCachedConversations() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final cached = prefs.getString('cached_conversations');
|
|
if (cached != null) {
|
|
final List<dynamic> decoded = jsonDecode(cached);
|
|
conversations.assignAll(decoded.map((c) => _resolveContactNames(ConversationModel.fromJson(c as Map<String, dynamic>))));
|
|
print('[CACHE] Loaded ${conversations.length} conversations instantly.');
|
|
}
|
|
} catch (e) {
|
|
print('[CACHE ERROR] $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _saveConversationsToCache(List<dynamic> 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<void> 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<dynamic> data = res['data'] ?? [];
|
|
conversations.assignAll(data.map((c) => _resolveContactNames(ConversationModel.fromJson(c as Map<String, dynamic>))));
|
|
_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<dynamic> data = res['data'] ?? [];
|
|
conversations.assignAll(data.map((c) => ConversationModel.fromJson(c as Map<String, dynamic>)));
|
|
}
|
|
} catch (e) {
|
|
print('[SEARCH ERROR] $e');
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Handle Incoming Socket Push Events ──────────────────────────────────
|
|
void _onPushEvent(Map<String, dynamic> 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<String, dynamic>?;
|
|
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<void> loadConversationsSilently() async {
|
|
if (!_svc.isWaReady.value) return;
|
|
try {
|
|
final res = await _svc.getConversations();
|
|
if (res['type'] == 'conversations') {
|
|
final List<dynamic> data = res['data'] ?? [];
|
|
conversations.assignAll(data.map((c) => _resolveContactNames(ConversationModel.fromJson(c as Map<String, dynamic>))));
|
|
_saveConversationsToCache(data);
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
}
|