Files
afterwork/lib/data/services/notification_service.dart
dahoud 92612abbd7 fix(chat): Correction race condition + Implémentation TODOs
## Corrections Critiques

### Race Condition - Statuts de Messages
- Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas
- Cause : WebSocket delivery confirmations arrivaient avant messages locaux
- Solution : Pattern Optimistic UI dans chat_bloc.dart
  - Création message temporaire immédiate
  - Ajout à la liste AVANT requête HTTP
  - Remplacement par message serveur à la réponse
- Fichier : lib/presentation/state_management/chat_bloc.dart

## Implémentation TODOs (13/21)

### Social (social_header_widget.dart)
-  Copier lien du post dans presse-papiers
-  Partage natif via Share.share()
-  Dialogue de signalement avec 5 raisons

### Partage (share_post_dialog.dart)
-  Interface sélection d'amis avec checkboxes
-  Partage externe via Share API

### Média (media_upload_service.dart)
-  Parsing JSON réponse backend
-  Méthode deleteMedia() pour suppression
-  Génération miniature vidéo

### Posts (create_post_dialog.dart, edit_post_dialog.dart)
-  Extraction URL depuis uploads
-  Documentation chargement médias

### Chat (conversations_screen.dart)
-  Navigation vers notifications
-  ConversationSearchDelegate pour recherche

## Nouveaux Fichiers

### Configuration
- build-prod.ps1 : Script build production avec dart-define
- lib/core/constants/env_config.dart : Gestion environnements

### Documentation
- TODOS_IMPLEMENTED.md : Documentation complète TODOs

## Améliorations

### Architecture
- Refactoring injection de dépendances
- Amélioration routing et navigation
- Optimisation providers (UserProvider, FriendsProvider)

### UI/UX
- Amélioration thème et couleurs
- Optimisation animations
- Meilleure gestion erreurs

### Services
- Configuration API avec env_config
- Amélioration datasources (events, users)
- Optimisation modèles de données
2026-01-10 10:43:17 +00:00

247 lines
7.8 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import '../../domain/entities/notification.dart' as domain;
import '../datasources/notification_remote_data_source.dart';
import '../services/realtime_notification_service.dart';
import '../services/secure_storage.dart';
/// Service centralisé de gestion des notifications.
///
/// Ce service gère:
/// - Le compteur de notifications non lues
/// - Le chargement des notifications depuis l'API
/// - Les notifications in-app
/// - Les listeners pour les changements
///
/// **Usage avec Provider:**
/// ```dart
/// // Dans main.dart
/// ChangeNotifierProvider(
/// create: (_) => NotificationService(
/// NotificationRemoteDataSource(http.Client()),
/// SecureStorage(),
/// )..initialize(),
/// ),
///
/// // Dans un widget
/// final notificationService = Provider.of<NotificationService>(context);
/// final unreadCount = notificationService.unreadCount;
/// ```
class NotificationService extends ChangeNotifier {
NotificationService(this._dataSource, this._secureStorage);
final NotificationRemoteDataSource _dataSource;
final SecureStorage _secureStorage;
List<domain.Notification> _notifications = [];
bool _isLoading = false;
Timer? _refreshTimer;
// Service de notifications temps réel
RealtimeNotificationService? _realtimeService;
StreamSubscription<SystemNotification>? _systemNotificationSubscription;
/// Liste de toutes les notifications
List<domain.Notification> get notifications => List.unmodifiable(_notifications);
/// Nombre total de notifications
int get totalCount => _notifications.length;
/// Nombre de notifications non lues
int get unreadCount => _notifications.where((n) => !n.isRead).length;
/// Indique si les notifications sont en cours de chargement
bool get isLoading => _isLoading;
/// Initialise le service (à appeler au démarrage de l'app).
Future<void> initialize() async {
await loadNotifications();
// Actualise les notifications toutes les 2 minutes
_startPeriodicRefresh();
}
/// Charge les notifications depuis l'API.
Future<void> loadNotifications() async {
_isLoading = true;
notifyListeners();
try {
final userId = await _secureStorage.getUserId();
if (userId == null || userId.isEmpty) {
_isLoading = false;
notifyListeners();
return;
}
final notificationModels = await _dataSource.getNotifications(userId);
_notifications = notificationModels.map((model) => model.toEntity()).toList();
// Trie par timestamp décroissant (les plus récentes en premier)
_notifications.sort((a, b) => b.timestamp.compareTo(a.timestamp));
} catch (e) {
debugPrint('[NotificationService] Erreur chargement: $e');
} finally {
_isLoading = false;
notifyListeners();
}
}
/// Marque une notification comme lue.
Future<void> markAsRead(String notificationId) async {
try {
await _dataSource.markAsRead(notificationId);
// Mise à jour locale
final index = _notifications.indexWhere((n) => n.id == notificationId);
if (index != -1) {
_notifications[index] = _notifications[index].copyWith(isRead: true);
notifyListeners();
}
} catch (e) {
debugPrint('[NotificationService] Erreur marquage lu: $e');
rethrow;
}
}
/// Marque toutes les notifications comme lues.
Future<void> markAllAsRead() async {
try {
final userId = await _secureStorage.getUserId();
if (userId == null) return;
await _dataSource.markAllAsRead(userId);
// Mise à jour locale
_notifications = _notifications.map((n) => n.copyWith(isRead: true)).toList();
notifyListeners();
} catch (e) {
debugPrint('[NotificationService] Erreur marquage tout lu: $e');
rethrow;
}
}
/// Supprime une notification.
Future<void> deleteNotification(String notificationId) async {
try {
await _dataSource.deleteNotification(notificationId);
// Mise à jour locale
_notifications.removeWhere((n) => n.id == notificationId);
notifyListeners();
} catch (e) {
debugPrint('[NotificationService] Erreur suppression: $e');
rethrow;
}
}
/// Ajoute une nouvelle notification (simulation ou depuis push notification).
///
/// Utilisé pour afficher une notification in-app quand une nouvelle
/// notification arrive via Firebase Cloud Messaging.
void addNotification(domain.Notification notification) {
_notifications.insert(0, notification);
notifyListeners();
}
/// Démarre l'actualisation périodique des notifications.
void _startPeriodicRefresh() {
_refreshTimer?.cancel();
_refreshTimer = Timer.periodic(
const Duration(minutes: 2),
(_) => loadNotifications(),
);
}
/// Arrête l'actualisation périodique.
void stopPeriodicRefresh() {
_refreshTimer?.cancel();
_refreshTimer = null;
}
/// Connecte le service de notifications temps réel.
///
/// Cette méthode remplace le polling par des notifications push en temps réel.
/// Le polling est automatiquement désactivé lorsque le service temps réel est connecté.
///
/// [service] : Le service de notifications temps réel à connecter.
void connectRealtime(RealtimeNotificationService service) {
_realtimeService = service;
// IMPORTANT : Arrêter le polling puisqu'on passe en temps réel
stopPeriodicRefresh();
debugPrint('[NotificationService] Polling arrêté, passage en mode temps réel');
// Écouter les notifications système en temps réel
_systemNotificationSubscription = service.systemNotificationStream.listen(
_handleSystemNotification,
onError: (error) {
debugPrint('[NotificationService] Erreur dans le stream de notifications système: $error');
},
);
debugPrint('[NotificationService] Service de notifications temps réel connecté');
}
/// Gère les notifications système reçues en temps réel.
///
/// Cette méthode est appelée automatiquement lorsqu'une notification
/// est reçue via WebSocket.
void _handleSystemNotification(SystemNotification notification) {
debugPrint('[NotificationService] Notification système reçue: ${notification.title}');
// Convertir en entité domain
final domainNotification = domain.Notification(
id: notification.notificationId,
title: notification.title,
message: notification.message,
type: _parseNotificationType(notification.type),
timestamp: notification.timestamp,
isRead: false,
eventId: null,
userId: '', // Le userId sera récupéré du contexte
metadata: null,
);
// Ajouter à la liste locale (en tête de liste pour avoir les plus récentes en premier)
addNotification(domainNotification);
debugPrint('[NotificationService] Notification ajoutée à la liste locale: ${notification.title}');
}
/// Parse le type de notification depuis une chaîne.
domain.NotificationType _parseNotificationType(String type) {
switch (type.toLowerCase()) {
case 'event':
return domain.NotificationType.event;
case 'friend':
return domain.NotificationType.friend;
case 'reminder':
return domain.NotificationType.reminder;
default:
return domain.NotificationType.other;
}
}
/// Déconnecte le service de notifications temps réel.
///
/// Le polling est automatiquement redémarré lorsque le service est déconnecté.
void disconnectRealtime() {
_systemNotificationSubscription?.cancel();
_systemNotificationSubscription = null;
_realtimeService = null;
// Redémarrer le polling si déconnecté du temps réel
_startPeriodicRefresh();
debugPrint('[NotificationService] Service temps réel déconnecté, reprise du polling');
}
@override
void dispose() {
disconnectRealtime();
stopPeriodicRefresh();
super.dispose();
}
}