feat: synchronisation mode deconnecte fin chat et stats
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../models/room.dart';
|
||||
import '../models/message.dart';
|
||||
import 'chat_config_loader.dart';
|
||||
import 'chat_info_service.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder à connectivityService
|
||||
|
||||
/// Service de chat avec règles métier configurables via YAML
|
||||
/// Les permissions sont définies dans chat_config.yaml
|
||||
@@ -21,9 +24,12 @@ class ChatService {
|
||||
late String _currentUserName;
|
||||
late int _currentUserRole;
|
||||
late int? _currentUserEntite;
|
||||
String? _authToken;
|
||||
|
||||
Timer? _syncTimer;
|
||||
DateTime? _lastSyncTimestamp;
|
||||
DateTime? _lastFullSync;
|
||||
static const Duration _syncInterval = Duration(seconds: 15); // Changé à 15 secondes comme suggéré par l'API
|
||||
static const Duration _fullSyncInterval = Duration(minutes: 5);
|
||||
|
||||
/// Initialisation avec gestion des rôles et configuration YAML
|
||||
static Future<void> init({
|
||||
@@ -39,21 +45,24 @@ class ChatService {
|
||||
// Charger la configuration depuis le YAML
|
||||
await ChatConfigLoader.instance.loadConfig();
|
||||
|
||||
// Initialiser Hive
|
||||
await Hive.initFlutter();
|
||||
Hive.registerAdapter(RoomAdapter());
|
||||
Hive.registerAdapter(MessageAdapter());
|
||||
// Les boxes sont déjà ouvertes par HiveService dans splash_page
|
||||
// On vérifie juste qu'elles sont disponibles et on les récupère
|
||||
if (!Hive.isBoxOpen(AppKeys.chatRoomsBoxName)) {
|
||||
throw Exception('Chat rooms box not open. Please ensure HiveService is initialized.');
|
||||
}
|
||||
if (!Hive.isBoxOpen(AppKeys.chatMessagesBoxName)) {
|
||||
throw Exception('Chat messages box not open. Please ensure HiveService is initialized.');
|
||||
}
|
||||
|
||||
// Ouvrir les boxes en utilisant les constantes centralisées
|
||||
_instance!._roomsBox = await Hive.openBox<Room>(AppKeys.chatRoomsBoxName);
|
||||
_instance!._messagesBox = await Hive.openBox<Message>(AppKeys.chatMessagesBoxName);
|
||||
// Récupérer les boxes déjà ouvertes
|
||||
_instance!._roomsBox = Hive.box<Room>(AppKeys.chatRoomsBoxName);
|
||||
_instance!._messagesBox = Hive.box<Message>(AppKeys.chatMessagesBoxName);
|
||||
|
||||
// Configurer l'utilisateur
|
||||
_instance!._currentUserId = userId;
|
||||
_instance!._currentUserName = userName;
|
||||
_instance!._currentUserRole = userRole;
|
||||
_instance!._currentUserEntite = userEntite;
|
||||
_instance!._authToken = authToken;
|
||||
|
||||
// Configurer Dio
|
||||
_instance!._dio = Dio(BaseOptions(
|
||||
@@ -63,7 +72,14 @@ class ChatService {
|
||||
headers: authToken != null ? {'Authorization': 'Bearer $authToken'} : {},
|
||||
));
|
||||
|
||||
// Démarrer la synchronisation
|
||||
// Charger le dernier timestamp de sync depuis Hive
|
||||
await _instance!._loadSyncTimestamp();
|
||||
|
||||
// Faire la sync initiale complète au login
|
||||
await _instance!.getRooms(forceFullSync: true);
|
||||
print('✅ Sync initiale complète effectuée au login');
|
||||
|
||||
// Démarrer la synchronisation incrémentale périodique
|
||||
_instance!._startSync();
|
||||
}
|
||||
|
||||
@@ -117,64 +133,216 @@ class ChatService {
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtenir les rooms filtrées selon les permissions
|
||||
Future<List<Room>> getRooms() async {
|
||||
/// Obtenir les rooms avec synchronisation incrémentale
|
||||
Future<List<Room>> getRooms({bool forceFullSync = false}) async {
|
||||
// Vérifier la connectivité
|
||||
if (!connectivityService.isConnected) {
|
||||
print('📵 Pas de connexion réseau - utilisation du cache');
|
||||
return _roomsBox.values.toList()
|
||||
..sort((a, b) => (b.lastMessageAt ?? b.createdAt)
|
||||
.compareTo(a.lastMessageAt ?? a.createdAt));
|
||||
}
|
||||
|
||||
try {
|
||||
// L'API filtre automatiquement selon le token Bearer
|
||||
final response = await _dio.get('/chat/rooms');
|
||||
// Déterminer si on fait une sync complète ou incrémentale
|
||||
final now = DateTime.now();
|
||||
final needsFullSync = forceFullSync ||
|
||||
_lastFullSync == null ||
|
||||
now.difference(_lastFullSync!).compareTo(_fullSyncInterval) > 0;
|
||||
|
||||
// Debug : afficher le type et le contenu de la réponse
|
||||
print('📊 Type de réponse /chat/rooms: ${response.data.runtimeType}');
|
||||
if (response.data is Map) {
|
||||
print('📊 Clés de la réponse: ${(response.data as Map).keys.toList()}');
|
||||
Response response;
|
||||
|
||||
if (needsFullSync || _lastSyncTimestamp == null) {
|
||||
// Synchronisation complète
|
||||
print('🔄 Synchronisation complète des rooms...');
|
||||
response = await _dio.get('/chat/rooms');
|
||||
_lastFullSync = now;
|
||||
} else {
|
||||
// Synchronisation incrémentale
|
||||
final isoTimestamp = _lastSyncTimestamp!.toUtc().toIso8601String();
|
||||
print('🔄 Synchronisation incrémentale depuis $isoTimestamp');
|
||||
response = await _dio.get('/chat/rooms', queryParameters: {
|
||||
'updated_after': isoTimestamp,
|
||||
});
|
||||
}
|
||||
|
||||
// Extraire le timestamp de synchronisation fourni par l'API
|
||||
if (response.data is Map && response.data['sync_timestamp'] != null) {
|
||||
_lastSyncTimestamp = DateTime.parse(response.data['sync_timestamp']);
|
||||
print('⏰ Timestamp de sync reçu de l\'API: $_lastSyncTimestamp');
|
||||
|
||||
// Sauvegarder le timestamp pour la prochaine session
|
||||
await _saveSyncTimestamp();
|
||||
} else {
|
||||
// L'API doit toujours retourner un sync_timestamp
|
||||
print('⚠️ Attention: L\'API n\'a pas retourné de sync_timestamp');
|
||||
// On utilise le timestamp actuel comme fallback mais ce n'est pas idéal
|
||||
_lastSyncTimestamp = now;
|
||||
}
|
||||
|
||||
// Vérifier s'il y a des changements (pour sync incrémentale)
|
||||
if (!needsFullSync && response.data is Map && response.data['has_changes'] == false) {
|
||||
print('✅ Aucun changement depuis la dernière sync');
|
||||
return _roomsBox.values.toList()
|
||||
..sort((a, b) => (b.lastMessageAt ?? b.createdAt)
|
||||
.compareTo(a.lastMessageAt ?? a.createdAt));
|
||||
}
|
||||
|
||||
// Gérer différents formats de réponse API
|
||||
List<dynamic> roomsData;
|
||||
if (response.data is Map) {
|
||||
// La plupart du temps, l'API retourne un objet avec status et rooms
|
||||
if (response.data['rooms'] != null) {
|
||||
roomsData = response.data['rooms'] as List;
|
||||
print('✅ Réponse API: status=${response.data['status']}, ${roomsData.length} rooms');
|
||||
final hasChanges = response.data['has_changes'] ?? true;
|
||||
print('✅ Réponse API: ${roomsData.length} rooms, has_changes: $hasChanges');
|
||||
} else if (response.data['data'] != null) {
|
||||
roomsData = response.data['data'] as List;
|
||||
print('✅ Réponse avec propriété "data" (${roomsData.length} rooms)');
|
||||
} else {
|
||||
// Pas de propriété rooms ou data, liste vide
|
||||
print('⚠️ Réponse sans rooms ni data: ${response.data}');
|
||||
roomsData = [];
|
||||
}
|
||||
} else if (response.data is List) {
|
||||
// Si c'est directement une liste (moins courant)
|
||||
roomsData = response.data as List;
|
||||
print('✅ Réponse est directement une liste avec ${roomsData.length} rooms');
|
||||
} else {
|
||||
// Format complètement inattendu
|
||||
print('⚠️ Format de réponse inattendu pour /chat/rooms: ${response.data.runtimeType}');
|
||||
roomsData = [];
|
||||
}
|
||||
|
||||
final rooms = roomsData
|
||||
.map((json) => Room.fromJson(json))
|
||||
.toList();
|
||||
// Parser les rooms
|
||||
final rooms = <Room>[];
|
||||
final deletedRoomIds = <String>[];
|
||||
|
||||
// Sauvegarder dans Hive
|
||||
await _roomsBox.clear();
|
||||
for (final room in rooms) {
|
||||
await _roomsBox.put(room.id, room);
|
||||
for (final json in roomsData) {
|
||||
try {
|
||||
// Vérifier si la room est marquée comme supprimée
|
||||
if (json['deleted'] == true) {
|
||||
deletedRoomIds.add(json['id']);
|
||||
continue;
|
||||
}
|
||||
|
||||
final room = Room.fromJson(json);
|
||||
rooms.add(room);
|
||||
} catch (e) {
|
||||
print('❌ Erreur parsing room: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Appliquer les modifications à Hive
|
||||
if (needsFullSync) {
|
||||
// Sync complète : remplacer tout mais préserver certaines données locales
|
||||
final existingRooms = Map.fromEntries(
|
||||
_roomsBox.values.map((r) => MapEntry(r.id, r))
|
||||
);
|
||||
|
||||
await _roomsBox.clear();
|
||||
for (final room in rooms) {
|
||||
final existingRoom = existingRooms[room.id];
|
||||
final roomToSave = Room(
|
||||
id: room.id,
|
||||
title: room.title,
|
||||
type: room.type,
|
||||
createdAt: room.createdAt,
|
||||
lastMessage: room.lastMessage,
|
||||
lastMessageAt: room.lastMessageAt,
|
||||
unreadCount: room.unreadCount,
|
||||
recentMessages: room.recentMessages,
|
||||
updatedAt: room.updatedAt,
|
||||
// Préserver createdBy existant si la nouvelle room n'en a pas
|
||||
createdBy: room.createdBy ?? existingRoom?.createdBy,
|
||||
);
|
||||
await _roomsBox.put(roomToSave.id, roomToSave);
|
||||
|
||||
// Traiter les messages récents de la room
|
||||
if (room.recentMessages != null && room.recentMessages!.isNotEmpty) {
|
||||
for (final msgData in room.recentMessages!) {
|
||||
try {
|
||||
final message = Message.fromJson(msgData, _currentUserId, roomId: room.id);
|
||||
// Sauvegarder uniquement si le message n'existe pas déjà
|
||||
if (!_messagesBox.containsKey(message.id) && message.id.isNotEmpty) {
|
||||
await _messagesBox.put(message.id, message);
|
||||
print('📩 Nouveau message ajouté depuis sync: ${message.id} dans room ${room.id}');
|
||||
} else if (message.id.isEmpty) {
|
||||
print('⚠️ Message avec ID vide ignoré');
|
||||
}
|
||||
} catch (e) {
|
||||
print('⚠️ Erreur lors du traitement d\'un message récent: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
print('💾 Sync complète: ${rooms.length} rooms sauvegardées');
|
||||
} else {
|
||||
// Sync incrémentale : mettre à jour uniquement les changements
|
||||
for (final room in rooms) {
|
||||
// Préserver le createdBy existant si non fourni par l'API
|
||||
final existingRoom = _roomsBox.get(room.id);
|
||||
final roomToSave = Room(
|
||||
id: room.id,
|
||||
title: room.title,
|
||||
type: room.type,
|
||||
createdAt: room.createdAt,
|
||||
lastMessage: room.lastMessage,
|
||||
lastMessageAt: room.lastMessageAt,
|
||||
unreadCount: room.unreadCount,
|
||||
recentMessages: room.recentMessages,
|
||||
updatedAt: room.updatedAt,
|
||||
// Préserver createdBy existant si la nouvelle room n'en a pas
|
||||
createdBy: room.createdBy ?? existingRoom?.createdBy,
|
||||
);
|
||||
|
||||
print('💾 Sauvegarde room ${roomToSave.title} (${roomToSave.id}): createdBy=${roomToSave.createdBy}');
|
||||
await _roomsBox.put(roomToSave.id, roomToSave);
|
||||
|
||||
// Traiter les messages récents de la room
|
||||
if (room.recentMessages != null && room.recentMessages!.isNotEmpty) {
|
||||
for (final msgData in room.recentMessages!) {
|
||||
try {
|
||||
final message = Message.fromJson(msgData, _currentUserId, roomId: room.id);
|
||||
// Sauvegarder uniquement si le message n'existe pas déjà
|
||||
if (!_messagesBox.containsKey(message.id) && message.id.isNotEmpty) {
|
||||
await _messagesBox.put(message.id, message);
|
||||
print('📩 Nouveau message ajouté depuis sync: ${message.id} dans room ${room.id}');
|
||||
} else if (message.id.isEmpty) {
|
||||
print('⚠️ Message avec ID vide ignoré');
|
||||
}
|
||||
} catch (e) {
|
||||
print('⚠️ Erreur lors du traitement d\'un message récent: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Supprimer les rooms marquées comme supprimées
|
||||
for (final roomId in deletedRoomIds) {
|
||||
// Supprimer la room
|
||||
await _roomsBox.delete(roomId);
|
||||
|
||||
// Supprimer tous les messages de cette room
|
||||
final messagesToDelete = _messagesBox.values
|
||||
.where((msg) => msg.roomId == roomId)
|
||||
.map((msg) => msg.id)
|
||||
.toList();
|
||||
|
||||
for (final msgId in messagesToDelete) {
|
||||
await _messagesBox.delete(msgId);
|
||||
}
|
||||
|
||||
print('🗑️ Room $roomId supprimée avec ${messagesToDelete.length} messages');
|
||||
}
|
||||
print('💾 Sync incrémentale: ${rooms.length} rooms mises à jour, ${deletedRoomIds.length} supprimées');
|
||||
}
|
||||
|
||||
// Mettre à jour les stats globales
|
||||
final totalUnread = rooms.fold<int>(0, (sum, room) => sum + room.unreadCount);
|
||||
final allRooms = _roomsBox.values.toList();
|
||||
final totalUnread = allRooms.fold<int>(0, (sum, room) => sum + room.unreadCount);
|
||||
ChatInfoService.instance.updateStats(
|
||||
totalRooms: rooms.length,
|
||||
totalRooms: allRooms.length,
|
||||
unreadMessages: totalUnread,
|
||||
);
|
||||
|
||||
return rooms;
|
||||
return allRooms
|
||||
..sort((a, b) => (b.lastMessageAt ?? b.createdAt)
|
||||
.compareTo(a.lastMessageAt ?? a.createdAt));
|
||||
} catch (e) {
|
||||
print('Erreur lors de la récupération des rooms (utilisation du cache): $e');
|
||||
// Fallback sur le cache local en cas d'erreur API (404, etc.)
|
||||
print('❌ Erreur sync rooms: $e');
|
||||
// Fallback sur le cache local
|
||||
return _roomsBox.values.toList()
|
||||
..sort((a, b) => (b.lastMessageAt ?? b.createdAt)
|
||||
.compareTo(a.lastMessageAt ?? a.createdAt));
|
||||
@@ -188,6 +356,27 @@ class ChatService {
|
||||
String? type,
|
||||
String? initialMessage,
|
||||
}) async {
|
||||
// Générer un ID temporaire pour la room
|
||||
final tempId = 'temp_room_${const Uuid().v4()}';
|
||||
final now = DateTime.now();
|
||||
|
||||
// Créer la room locale immédiatement
|
||||
final tempRoom = Room(
|
||||
id: tempId,
|
||||
title: title,
|
||||
type: type ?? (_currentUserRole == 9 ? 'broadcast' : 'private'),
|
||||
createdAt: now,
|
||||
lastMessage: initialMessage,
|
||||
lastMessageAt: initialMessage != null ? now : null,
|
||||
unreadCount: 0,
|
||||
createdBy: _currentUserId,
|
||||
isSynced: false, // Room non synchronisée
|
||||
);
|
||||
|
||||
// Sauvegarder immédiatement dans Hive
|
||||
await _roomsBox.put(tempId, tempRoom);
|
||||
print('💾 Room temporaire sauvée: $tempId');
|
||||
|
||||
try {
|
||||
// Vérifier les permissions localement d'abord
|
||||
// L'API fera aussi une vérification
|
||||
@@ -204,14 +393,37 @@ class ChatService {
|
||||
data['initial_message'] = initialMessage;
|
||||
}
|
||||
|
||||
final response = await _dio.post('/chat/rooms', data: data);
|
||||
// Utiliser ApiService qui gère automatiquement la queue offline
|
||||
final response = await ApiService.instance.post(
|
||||
'/chat/rooms',
|
||||
data: data,
|
||||
tempId: tempId, // Passer le tempId pour le mapping après sync
|
||||
);
|
||||
|
||||
final room = Room.fromJson(response.data);
|
||||
await _roomsBox.put(room.id, room);
|
||||
// Vérifier si la room a été mise en queue (offline)
|
||||
if (response.data != null && response.data['queued'] == true) {
|
||||
print('📵 Room mise en file d\'attente pour synchronisation: $tempId');
|
||||
return tempRoom; // Retourner la room temporaire
|
||||
}
|
||||
|
||||
// Si online et succès immédiat
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final realRoom = Room.fromJson(response.data);
|
||||
|
||||
// Remplacer la room temporaire par la room réelle
|
||||
await _roomsBox.delete(tempId);
|
||||
await _roomsBox.put(realRoom.id, realRoom);
|
||||
print('✅ Room temporaire $tempId remplacée par ${realRoom.id}');
|
||||
|
||||
return realRoom;
|
||||
}
|
||||
|
||||
return tempRoom;
|
||||
|
||||
return room;
|
||||
} catch (e) {
|
||||
return null;
|
||||
print('⚠️ Erreur création room: $e - La room sera synchronisée plus tard');
|
||||
// La room reste en local avec isSynced = false
|
||||
return tempRoom;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,8 +467,8 @@ class ChatService {
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtenir les messages d'une room avec pagination
|
||||
Future<Map<String, dynamic>> getMessages(String roomId, {String? beforeMessageId}) async {
|
||||
/// Obtenir les messages d'une room (marque automatiquement comme lu)
|
||||
Future<Map<String, dynamic>> getMessages(String roomId, {String? beforeMessageId, bool isInitialLoad = false}) async {
|
||||
try {
|
||||
final params = <String, dynamic>{
|
||||
'limit': 50,
|
||||
@@ -271,29 +483,64 @@ class ChatService {
|
||||
// Gérer différents formats de réponse
|
||||
List<dynamic> messagesData;
|
||||
bool hasMore = false;
|
||||
int markedAsRead = 0;
|
||||
int unreadRemaining = 0;
|
||||
|
||||
if (response.data is List) {
|
||||
// Si c'est directement une liste de messages
|
||||
messagesData = response.data as List;
|
||||
} else if (response.data is Map) {
|
||||
// Si c'est un objet avec messages et has_more
|
||||
// Format avec métadonnées
|
||||
messagesData = response.data['messages'] ?? response.data['data'] ?? [];
|
||||
hasMore = response.data['has_more'] ?? false;
|
||||
markedAsRead = response.data['marked_as_read'] ?? 0;
|
||||
unreadRemaining = response.data['unread_count'] ?? 0;
|
||||
|
||||
if (markedAsRead > 0) {
|
||||
print('✅ $markedAsRead messages marqués comme lus automatiquement');
|
||||
}
|
||||
} else {
|
||||
print('⚠️ Format inattendu pour les messages: ${response.data.runtimeType}');
|
||||
messagesData = [];
|
||||
}
|
||||
|
||||
final messages = messagesData
|
||||
.map((json) => Message.fromJson(json, _currentUserId))
|
||||
.map((json) => Message.fromJson(json, _currentUserId, roomId: roomId))
|
||||
.toList();
|
||||
|
||||
print('📨 Messages reçus pour room $roomId: ${messages.length}');
|
||||
for (final msg in messages) {
|
||||
print(' - ${msg.id}: "${msg.content}" de ${msg.senderName} (${msg.senderId}) isMe: ${msg.isMe}');
|
||||
}
|
||||
|
||||
// Sauvegarder dans Hive (en limitant à 100 messages par room)
|
||||
await _saveMessagesToCache(roomId, messages);
|
||||
// Si c'est le chargement initial, on remplace tous les messages
|
||||
await _saveMessagesToCache(roomId, messages, replaceAll: isInitialLoad && beforeMessageId == null);
|
||||
|
||||
// Mettre à jour le unreadCount de la room localement
|
||||
final room = _roomsBox.get(roomId);
|
||||
if (room != null && unreadRemaining == 0) {
|
||||
final updatedRoom = Room(
|
||||
id: room.id,
|
||||
title: room.title,
|
||||
type: room.type,
|
||||
createdAt: room.createdAt,
|
||||
lastMessage: room.lastMessage,
|
||||
lastMessageAt: room.lastMessageAt,
|
||||
unreadCount: 0, // Mis à 0 car tout est lu
|
||||
recentMessages: room.recentMessages,
|
||||
updatedAt: room.updatedAt,
|
||||
);
|
||||
await _roomsBox.put(roomId, updatedRoom);
|
||||
|
||||
// Mettre à jour les stats globales
|
||||
ChatInfoService.instance.decrementUnread(markedAsRead);
|
||||
}
|
||||
|
||||
return {
|
||||
'messages': messages,
|
||||
'has_more': hasMore,
|
||||
'marked_as_read': markedAsRead,
|
||||
};
|
||||
} catch (e) {
|
||||
print('Erreur getMessages: $e');
|
||||
@@ -306,120 +553,231 @@ class ChatService {
|
||||
return {
|
||||
'messages': cachedMessages,
|
||||
'has_more': false,
|
||||
'marked_as_read': 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Sauvegarder les messages dans le cache en limitant à 100 par room
|
||||
Future<void> _saveMessagesToCache(String roomId, List<Message> newMessages) async {
|
||||
// Obtenir tous les messages existants pour cette room
|
||||
final existingMessages = _messagesBox.values
|
||||
Future<void> _saveMessagesToCache(String roomId, List<Message> newMessages, {bool replaceAll = false}) async {
|
||||
// Ajouter les nouveaux messages (en évitant les doublons)
|
||||
int addedCount = 0;
|
||||
for (final message in newMessages) {
|
||||
// Vérifier si le message n'existe pas déjà
|
||||
if (!_messagesBox.containsKey(message.id) && message.id.isNotEmpty) {
|
||||
await _messagesBox.put(message.id, message);
|
||||
print('💾 Message sauvé: ${message.id} dans room ${message.roomId}');
|
||||
addedCount++;
|
||||
} else if (_messagesBox.containsKey(message.id)) {
|
||||
print('⚠️ Message ${message.id} existe déjà, ignoré pour éviter duplication');
|
||||
}
|
||||
}
|
||||
|
||||
print('📊 Résumé: ${addedCount} nouveaux messages ajoutés sur ${newMessages.length} reçus');
|
||||
|
||||
// Après l'ajout, récupérer TOUS les messages de la room pour le nettoyage
|
||||
final allRoomMessages = _messagesBox.values
|
||||
.where((m) => m.roomId == roomId)
|
||||
.toList()
|
||||
..sort((a, b) => b.sentAt.compareTo(a.sentAt)); // Plus récent en premier
|
||||
|
||||
// Ajouter les nouveaux messages
|
||||
for (final message in newMessages) {
|
||||
await _messagesBox.put(message.id, message);
|
||||
}
|
||||
|
||||
// Si on dépasse 100 messages, supprimer les plus anciens
|
||||
final allMessages = [...existingMessages, ...newMessages]
|
||||
..sort((a, b) => b.sentAt.compareTo(a.sentAt));
|
||||
|
||||
if (allMessages.length > 100) {
|
||||
final messagesToDelete = allMessages.skip(100).toList();
|
||||
if (allRoomMessages.length > 100) {
|
||||
final messagesToDelete = allRoomMessages.skip(100).toList();
|
||||
print('🗑️ Suppression de ${messagesToDelete.length} anciens messages');
|
||||
for (final message in messagesToDelete) {
|
||||
await _messagesBox.delete(message.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprimer une room (seulement pour le créateur)
|
||||
Future<bool> deleteRoom(String roomId) async {
|
||||
try {
|
||||
// Appeler l'API pour supprimer la room
|
||||
await _dio.delete('/chat/rooms/$roomId');
|
||||
|
||||
// Supprimer la room localement
|
||||
await _roomsBox.delete(roomId);
|
||||
|
||||
// Supprimer tous les messages de cette room
|
||||
final messagesToDelete = _messagesBox.values
|
||||
.where((msg) => msg.roomId == roomId)
|
||||
.map((msg) => msg.id)
|
||||
.toList();
|
||||
|
||||
for (final msgId in messagesToDelete) {
|
||||
await _messagesBox.delete(msgId);
|
||||
}
|
||||
|
||||
print('✅ Room $roomId supprimée avec succès');
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('❌ Erreur suppression room: $e');
|
||||
throw Exception('Impossible de supprimer la conversation');
|
||||
}
|
||||
}
|
||||
|
||||
/// Envoyer un message
|
||||
Future<Message?> sendMessage(String roomId, String content) async {
|
||||
final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
|
||||
// Générer un ID temporaire pour le message
|
||||
final tempId = 'temp_msg_${const Uuid().v4()}';
|
||||
final now = DateTime.now();
|
||||
|
||||
// Créer le message local immédiatement (optimistic UI)
|
||||
final tempMessage = Message(
|
||||
id: tempId,
|
||||
roomId: roomId,
|
||||
content: content,
|
||||
senderId: _currentUserId,
|
||||
senderName: _currentUserName,
|
||||
sentAt: DateTime.now(),
|
||||
sentAt: now,
|
||||
isMe: true,
|
||||
isRead: false,
|
||||
isSynced: false, // Message non synchronisé
|
||||
);
|
||||
|
||||
// Sauvegarder immédiatement dans Hive pour affichage instantané
|
||||
await _messagesBox.put(tempId, tempMessage);
|
||||
print('💾 Message temporaire sauvé: $tempId');
|
||||
|
||||
// Mettre à jour la room localement pour affichage immédiat
|
||||
final room = _roomsBox.get(roomId);
|
||||
if (room != null) {
|
||||
final updatedRoom = room.copyWith(
|
||||
lastMessage: content,
|
||||
lastMessageAt: now,
|
||||
unreadCount: 0,
|
||||
updatedAt: now,
|
||||
);
|
||||
await _roomsBox.put(roomId, updatedRoom);
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
// Utiliser ApiService qui gère automatiquement la queue offline
|
||||
final response = await ApiService.instance.post(
|
||||
'/chat/rooms/$roomId/messages',
|
||||
data: {
|
||||
'content': content,
|
||||
// L'API récupère le sender depuis le token
|
||||
},
|
||||
tempId: tempId, // Passer le tempId pour le mapping après sync
|
||||
);
|
||||
|
||||
final finalMessage = Message.fromJson(response.data, _currentUserId);
|
||||
|
||||
await _messagesBox.delete(tempId);
|
||||
await _messagesBox.put(finalMessage.id, finalMessage);
|
||||
|
||||
// Mettre à jour la room
|
||||
final room = _roomsBox.get(roomId);
|
||||
if (room != null) {
|
||||
final updatedRoom = Room(
|
||||
id: room.id,
|
||||
title: room.title,
|
||||
type: room.type,
|
||||
createdAt: room.createdAt,
|
||||
lastMessage: content,
|
||||
lastMessageAt: DateTime.now(),
|
||||
unreadCount: 0,
|
||||
);
|
||||
await _roomsBox.put(roomId, updatedRoom);
|
||||
// Vérifier si le message a été mis en queue (offline)
|
||||
if (response.data != null && response.data['queued'] == true) {
|
||||
print('📵 Message mis en file d\'attente pour synchronisation: $tempId');
|
||||
return tempMessage; // Retourner le message temporaire
|
||||
}
|
||||
|
||||
return finalMessage;
|
||||
// Si online et succès immédiat
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
// Récupérer le message complet depuis la réponse API
|
||||
final realMessage = Message.fromJson(
|
||||
response.data['message'] ?? response.data,
|
||||
_currentUserId,
|
||||
roomId: roomId
|
||||
);
|
||||
|
||||
print('📨 Message envoyé avec ID réel: ${realMessage.id}');
|
||||
|
||||
// Remplacer le message temporaire par le message réel
|
||||
await _messagesBox.delete(tempId);
|
||||
await _messagesBox.put(realMessage.id, realMessage);
|
||||
print('✅ Message temporaire $tempId remplacé par ${realMessage.id}');
|
||||
|
||||
return realMessage;
|
||||
}
|
||||
|
||||
// Cas par défaut : retourner le message temporaire
|
||||
return tempMessage;
|
||||
|
||||
} catch (e) {
|
||||
print('⚠️ Erreur envoi message: $e - Le message sera synchronisé plus tard');
|
||||
// Le message reste en local avec isSynced = false
|
||||
return tempMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/// Marquer comme lu
|
||||
Future<void> markAsRead(String roomId) async {
|
||||
// La méthode markAsRead n'est plus nécessaire car l'API marque automatiquement
|
||||
// les messages comme lus lors de l'appel GET /api/chat/rooms/{roomId}/messages
|
||||
|
||||
/// Charger le timestamp de dernière sync depuis Hive
|
||||
Future<void> _loadSyncTimestamp() async {
|
||||
try {
|
||||
await _dio.post('/chat/rooms/$roomId/read');
|
||||
|
||||
final room = _roomsBox.get(roomId);
|
||||
if (room != null) {
|
||||
// Décrémenter les messages non lus dans ChatInfoService
|
||||
if (room.unreadCount > 0) {
|
||||
ChatInfoService.instance.decrementUnread(room.unreadCount);
|
||||
// Utiliser une box générique pour stocker les métadonnées
|
||||
if (Hive.isBoxOpen('chat_metadata')) {
|
||||
final metaBox = Hive.box('chat_metadata');
|
||||
final timestamp = metaBox.get('last_sync_timestamp');
|
||||
if (timestamp != null) {
|
||||
_lastSyncTimestamp = DateTime.parse(timestamp);
|
||||
print('📅 Dernier timestamp de sync restauré: $_lastSyncTimestamp');
|
||||
}
|
||||
|
||||
final updatedRoom = Room(
|
||||
id: room.id,
|
||||
title: room.title,
|
||||
type: room.type,
|
||||
createdAt: room.createdAt,
|
||||
lastMessage: room.lastMessage,
|
||||
lastMessageAt: room.lastMessageAt,
|
||||
unreadCount: 0,
|
||||
);
|
||||
await _roomsBox.put(roomId, updatedRoom);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignorer
|
||||
print('⚠️ Impossible de charger le timestamp de sync: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronisation périodique
|
||||
/// Sauvegarder le timestamp de dernière sync dans Hive
|
||||
Future<void> _saveSyncTimestamp() async {
|
||||
if (_lastSyncTimestamp == null) return;
|
||||
|
||||
try {
|
||||
// Ouvrir ou créer la box de métadonnées si nécessaire
|
||||
Box metaBox;
|
||||
if (!Hive.isBoxOpen('chat_metadata')) {
|
||||
metaBox = await Hive.openBox('chat_metadata');
|
||||
} else {
|
||||
metaBox = Hive.box('chat_metadata');
|
||||
}
|
||||
|
||||
await metaBox.put('last_sync_timestamp', _lastSyncTimestamp!.toIso8601String());
|
||||
} catch (e) {
|
||||
print('⚠️ Impossible de sauvegarder le timestamp de sync: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronisation périodique avec vérification de connectivité
|
||||
void _startSync() {
|
||||
_syncTimer?.cancel();
|
||||
_syncTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
getRooms();
|
||||
_syncTimer = Timer.periodic(_syncInterval, (_) async {
|
||||
// Vérifier la connectivité avant de synchroniser
|
||||
if (!connectivityService.isConnected) {
|
||||
print('📵 Pas de connexion - sync ignorée');
|
||||
return;
|
||||
}
|
||||
|
||||
// Synchroniser les rooms (incrémentale)
|
||||
await getRooms();
|
||||
});
|
||||
|
||||
// Pas de sync immédiate ici car déjà faite dans init()
|
||||
print('⏰ Timer de sync incrémentale démarré (toutes les 15 secondes)');
|
||||
}
|
||||
|
||||
/// Mettre en pause les synchronisations (app en arrière-plan)
|
||||
void pauseSyncs() {
|
||||
_syncTimer?.cancel();
|
||||
print('⏸️ Timer de sync arrêté (app en arrière-plan)');
|
||||
}
|
||||
|
||||
/// Reprendre les synchronisations (app au premier plan)
|
||||
void resumeSyncs() {
|
||||
if (_syncTimer == null || !_syncTimer!.isActive) {
|
||||
_startSync();
|
||||
print('▶️ Timer de sync redémarré (app au premier plan)');
|
||||
|
||||
// Faire une sync immédiate au retour au premier plan
|
||||
// pour rattraper les messages manqués
|
||||
if (connectivityService.isConnected) {
|
||||
getRooms().then((_) {
|
||||
print('✅ Sync de rattrapage effectuée');
|
||||
}).catchError((e) {
|
||||
print('⚠️ Erreur sync de rattrapage: $e');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Nettoyer les ressources
|
||||
|
||||
Reference in New Issue
Block a user