Files
geo/app/lib/chat/services/chat_service.dart
pierre 570a1fa1f0 feat: Version 3.3.4 - Nouvelle architecture pages, optimisations widgets Flutter et API
- Mise à jour VERSION vers 3.3.4
- Optimisations et révisions architecture API (deploy-api.sh, scripts de migration)
- Ajout documentation Stripe Tap to Pay complète
- Migration vers polices Inter Variable pour Flutter
- Optimisations build Android et nettoyage fichiers temporaires
- Amélioration système de déploiement avec gestion backups
- Ajout scripts CRON et migrations base de données

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 20:11:15 +02:00

827 lines
30 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
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);
debugPrint('✅ 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 {
debugPrint('⚠️ Format inattendu pour /chat/recipients: ${response.data.runtimeType}');
return [];
}
} catch (e) {
debugPrint('⚠️ 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) {
debugPrint('📵 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
debugPrint('🔄 Synchronisation complète des rooms...');
response = await _dio.get('/chat/rooms');
_lastFullSync = now;
} else {
// Synchronisation incrémentale
final isoTimestamp = _lastSyncTimestamp!.toUtc().toIso8601String();
// debugPrint('🔄 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']);
// debugPrint('⏰ 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
debugPrint('⚠️ 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) {
// debugPrint('✅ 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;
debugPrint('✅ 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) {
debugPrint('❌ 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);
debugPrint('📩 Nouveau message ajouté depuis sync: ${message.id} dans room ${room.id}');
} else if (message.id.isEmpty) {
debugPrint('⚠️ Message avec ID vide ignoré');
}
} catch (e) {
debugPrint('⚠️ Erreur lors du traitement d\'un message récent: $e');
}
}
}
}
debugPrint('💾 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,
);
debugPrint('💾 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);
debugPrint('📩 Nouveau message ajouté depuis sync: ${message.id} dans room ${room.id}');
} else if (message.id.isEmpty) {
debugPrint('⚠️ Message avec ID vide ignoré');
}
} catch (e) {
debugPrint('⚠️ 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);
}
debugPrint('🗑️ Room $roomId supprimée avec ${messagesToDelete.length} messages');
}
debugPrint('💾 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) {
debugPrint('❌ 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);
debugPrint('💾 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) {
debugPrint('📵 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);
debugPrint('✅ Room temporaire $tempId remplacée par ${realRoom.id}');
return realRoom;
}
return tempRoom;
} catch (e) {
debugPrint('⚠️ 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) {
debugPrint('$markedAsRead messages marqués comme lus automatiquement');
}
} else {
debugPrint('⚠️ Format inattendu pour les messages: ${response.data.runtimeType}');
messagesData = [];
}
final messages = messagesData
.map((json) => Message.fromJson(json, _currentUserId, roomId: roomId))
.toList();
debugPrint('📨 Messages reçus pour room $roomId: ${messages.length}');
for (final msg in messages) {
debugPrint(' - ${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) {
debugPrint('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);
debugPrint('💾 Message sauvé: ${message.id} dans room ${message.roomId}');
addedCount++;
} else if (_messagesBox.containsKey(message.id)) {
debugPrint('⚠️ Message ${message.id} existe déjà, ignoré pour éviter duplication');
}
}
debugPrint('📊 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();
debugPrint('🗑️ 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);
}
debugPrint('✅ Room $roomId supprimée avec succès');
return true;
} catch (e) {
debugPrint('❌ 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);
debugPrint('💾 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) {
debugPrint('📵 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
);
debugPrint('📨 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);
debugPrint('✅ Message temporaire $tempId remplacé par ${realMessage.id}');
return realMessage;
}
// Cas par défaut : retourner le message temporaire
return tempMessage;
} catch (e) {
debugPrint('⚠️ 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);
debugPrint('📅 Dernier timestamp de sync restauré: $_lastSyncTimestamp');
}
}
} catch (e) {
debugPrint('⚠️ 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) {
debugPrint('⚠️ 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) {
debugPrint('📵 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()
debugPrint('⏰ 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();
debugPrint('⏸️ 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();
debugPrint('▶️ 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((_) {
debugPrint('✅ Sync de rattrapage effectuée');
}).catchError((e) {
debugPrint('⚠️ 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 '';
}
}
}