Files
mywhatsapp/whatsapp_app/lib/controllers/conversations_controller.dart
2026-05-19 23:27:14 +03:00

228 lines
7.9 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 contactsService = Get.find<ContactsService>();
// 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<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,
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<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 (_) {}
}
}