826 lines
30 KiB
Dart
826 lines
30 KiB
Dart
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
|
|
class ChatService {
|
|
static ChatService? _instance;
|
|
static ChatService get instance => _instance!;
|
|
|
|
late final Dio _dio;
|
|
late final Box<Room> _roomsBox;
|
|
late final Box<Message> _messagesBox;
|
|
|
|
late int _currentUserId;
|
|
late String _currentUserName;
|
|
late int _currentUserRole;
|
|
late int? _currentUserEntite;
|
|
|
|
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({
|
|
required String apiUrl,
|
|
required int userId,
|
|
required String userName,
|
|
required int userRole,
|
|
int? userEntite,
|
|
String? authToken,
|
|
}) async {
|
|
_instance = ChatService._();
|
|
|
|
// Charger la configuration depuis le YAML
|
|
await ChatConfigLoader.instance.loadConfig();
|
|
|
|
// 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.');
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Configurer Dio
|
|
_instance!._dio = Dio(BaseOptions(
|
|
baseUrl: apiUrl,
|
|
connectTimeout: const Duration(seconds: 10),
|
|
receiveTimeout: const Duration(seconds: 10),
|
|
headers: authToken != null ? {'Authorization': 'Bearer $authToken'} : {},
|
|
));
|
|
|
|
// 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();
|
|
}
|
|
|
|
ChatService._();
|
|
|
|
/// Obtenir les destinataires possibles selon le rôle
|
|
Future<List<Map<String, dynamic>>> getPossibleRecipients({String? search}) async {
|
|
try {
|
|
// L'API utilise le token pour identifier l'utilisateur et son rôle
|
|
String endpoint = '/chat/recipients';
|
|
final params = <String, dynamic>{};
|
|
|
|
if (search != null && search.isNotEmpty) {
|
|
params['search'] = search;
|
|
}
|
|
|
|
final response = await _dio.get(endpoint, queryParameters: params);
|
|
|
|
// Gérer différents formats de réponse
|
|
if (response.data is List) {
|
|
return List<Map<String, dynamic>>.from(response.data);
|
|
} else if (response.data is Map && response.data['recipients'] != null) {
|
|
return List<Map<String, dynamic>>.from(response.data['recipients']);
|
|
} else if (response.data is Map && response.data['data'] != null) {
|
|
return List<Map<String, dynamic>>.from(response.data['data']);
|
|
} else {
|
|
print('⚠️ Format inattendu pour /chat/recipients: ${response.data.runtimeType}');
|
|
return [];
|
|
}
|
|
} catch (e) {
|
|
print('⚠️ Erreur getPossibleRecipients: $e');
|
|
// Fallback sur logique locale selon le rôle
|
|
return _getLocalRecipients();
|
|
}
|
|
}
|
|
|
|
/// Logique locale de récupération des destinataires
|
|
List<Map<String, dynamic>> _getLocalRecipients() {
|
|
// Cette méthode devrait accéder à la box membres de l'app principale
|
|
// Pour l'instant on retourne une liste vide
|
|
return [];
|
|
}
|
|
|
|
/// Vérifier si l'utilisateur peut créer une conversation avec un destinataire
|
|
bool canCreateConversationWith(int recipientRole, {int? recipientEntite}) {
|
|
return ChatConfigLoader.instance.canSendMessageTo(
|
|
senderRole: _currentUserRole,
|
|
recipientRole: recipientRole,
|
|
senderEntite: _currentUserEntite,
|
|
recipientEntite: recipientEntite,
|
|
);
|
|
}
|
|
|
|
/// 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 {
|
|
// 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;
|
|
|
|
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) {
|
|
if (response.data['rooms'] != null) {
|
|
roomsData = response.data['rooms'] as List;
|
|
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;
|
|
} else {
|
|
roomsData = [];
|
|
}
|
|
} else if (response.data is List) {
|
|
roomsData = response.data as List;
|
|
} else {
|
|
roomsData = [];
|
|
}
|
|
|
|
// Parser les rooms
|
|
final rooms = <Room>[];
|
|
final deletedRoomIds = <String>[];
|
|
|
|
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 allRooms = _roomsBox.values.toList();
|
|
final totalUnread = allRooms.fold<int>(0, (sum, room) => sum + room.unreadCount);
|
|
ChatInfoService.instance.updateStats(
|
|
totalRooms: allRooms.length,
|
|
unreadMessages: totalUnread,
|
|
);
|
|
|
|
return allRooms
|
|
..sort((a, b) => (b.lastMessageAt ?? b.createdAt)
|
|
.compareTo(a.lastMessageAt ?? a.createdAt));
|
|
} catch (e) {
|
|
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));
|
|
}
|
|
}
|
|
|
|
/// Créer une room avec vérification des permissions
|
|
Future<Room?> createRoom({
|
|
required String title,
|
|
required List<int> participantIds,
|
|
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
|
|
|
|
final data = {
|
|
'title': title,
|
|
'type': type ?? (_currentUserRole == 9 ? 'broadcast' : 'private'),
|
|
'participants': participantIds,
|
|
// L'API récupère le rôle et l'entité depuis le token
|
|
};
|
|
|
|
// Ajouter le message initial s'il est fourni
|
|
if (initialMessage != null && initialMessage.isNotEmpty) {
|
|
data['initial_message'] = initialMessage;
|
|
}
|
|
|
|
// 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
|
|
);
|
|
|
|
// 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;
|
|
|
|
} catch (e) {
|
|
print('⚠️ Erreur création room: $e - La room sera synchronisée plus tard');
|
|
// La room reste en local avec isSynced = false
|
|
return tempRoom;
|
|
}
|
|
}
|
|
|
|
/// Créer une conversation one-to-one
|
|
Future<Room?> createPrivateRoom({
|
|
required int recipientId,
|
|
required String recipientName,
|
|
required int recipientRole,
|
|
int? recipientEntite,
|
|
String? initialMessage,
|
|
}) async {
|
|
// Vérifier les permissions via la configuration
|
|
if (!canCreateConversationWith(recipientRole, recipientEntite: recipientEntite)) {
|
|
final messages = ChatConfigLoader.instance.getUIMessages();
|
|
throw Exception(messages['no_permission'] ?? 'Permission refusée');
|
|
}
|
|
|
|
return createRoom(
|
|
title: recipientName,
|
|
participantIds: [recipientId],
|
|
type: 'private',
|
|
initialMessage: initialMessage,
|
|
);
|
|
}
|
|
|
|
/// Créer une conversation de groupe (pour superadmin)
|
|
Future<Room?> createGroupRoom({
|
|
required String title,
|
|
required List<int> adminIds,
|
|
String? initialMessage,
|
|
}) async {
|
|
if (_currentUserRole != 9) {
|
|
throw Exception('Seuls les superadmins peuvent créer des groupes');
|
|
}
|
|
|
|
return createRoom(
|
|
title: title,
|
|
participantIds: adminIds,
|
|
type: 'broadcast',
|
|
initialMessage: initialMessage,
|
|
);
|
|
}
|
|
|
|
/// 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,
|
|
};
|
|
|
|
if (beforeMessageId != null) {
|
|
params['before'] = beforeMessageId;
|
|
}
|
|
|
|
final response = await _dio.get('/chat/rooms/$roomId/messages', queryParameters: params);
|
|
|
|
// 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) {
|
|
// 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, 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)
|
|
// 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');
|
|
// Fallback sur le cache local
|
|
final cachedMessages = _messagesBox.values
|
|
.where((m) => m.roomId == roomId)
|
|
.toList()
|
|
..sort((a, b) => a.sentAt.compareTo(b.sentAt));
|
|
|
|
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, {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
|
|
|
|
// Si on dépasse 100 messages, supprimer les plus anciens
|
|
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 {
|
|
// 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: 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 {
|
|
// 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
|
|
);
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
// 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');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('⚠️ Impossible de charger le timestamp de sync: $e');
|
|
}
|
|
}
|
|
|
|
/// 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(_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
|
|
void dispose() {
|
|
_syncTimer?.cancel();
|
|
_dio.close();
|
|
}
|
|
|
|
// Getters
|
|
Box<Room> get roomsBox => _roomsBox;
|
|
Box<Message> get messagesBox => _messagesBox;
|
|
int get currentUserId => _currentUserId;
|
|
|
|
/// Obtenir le rôle de l'utilisateur actuel
|
|
int getUserRole() => _currentUserRole;
|
|
int get currentUserRole => _currentUserRole;
|
|
String get currentUserName => _currentUserName;
|
|
|
|
/// Obtenir le label du rôle
|
|
String getRoleLabel(int role) {
|
|
switch (role) {
|
|
case 1:
|
|
return 'Membre';
|
|
case 2:
|
|
return 'Admin Amicale';
|
|
case 9:
|
|
return 'Super Admin';
|
|
default:
|
|
return 'Utilisateur';
|
|
}
|
|
}
|
|
|
|
/// Obtenir la description des permissions
|
|
String getPermissionsDescription() {
|
|
switch (_currentUserRole) {
|
|
case 1:
|
|
return 'Vous pouvez discuter avec les membres de votre amicale';
|
|
case 2:
|
|
return 'Vous pouvez discuter avec les membres et les super admins';
|
|
case 9:
|
|
return 'Vous pouvez envoyer des messages aux administrateurs d\'amicale';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
} |