Initial commit with Flutter and Node.js code

This commit is contained in:
Hamza-Ayed
2026-05-18 14:04:39 +03:00
commit a60a173b51
21 changed files with 3107 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
import 'dart:async';
import 'package:get/get.dart';
import '../services/whatsapp_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 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();
}
// ── Load Conversations ───────────────────────────────────────────────────
Future<void> loadConversations() async {
if (!_svc.isWaReady.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) => ConversationModel.fromJson(c as Map<String, dynamic>)));
} 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) => ConversationModel.fromJson(c as Map<String, dynamic>)));
}
} catch (_) {}
}
}