Files
afterwork/lib/data/providers/friends_provider.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

556 lines
21 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import '../../core/utils/app_logger.dart';
import '../../data/repositories/friends_repository_impl.dart';
import '../../data/services/realtime_notification_service.dart';
import '../../data/services/secure_storage.dart';
import '../../domain/entities/friend.dart';
import '../../domain/entities/friend_request.dart';
/// [FriendsProvider] est un `ChangeNotifier` qui gère la logique de gestion des amis.
/// Il interagit avec le [FriendsRepositoryImpl] pour effectuer des appels API et gérer
/// la liste des amis de l'utilisateur, avec une gestion avancée de la pagination,
/// du statut des amis et de la gestion des erreurs.
class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par page
/// Constructeur de [FriendsProvider] qui nécessite l'instance d'un [FriendsRepositoryImpl].
FriendsProvider({required this.friendsRepository});
final FriendsRepositoryImpl friendsRepository;
// Liste des amis
List<Friend> _friendsList = [];
bool _isLoading = false; // Indicateur de chargement
bool _hasMore = true; // Indicateur de pagination
int _currentPage = 0; // Numéro de la page actuelle pour la pagination
final int _friendsPerPage = 10;
// Liste des demandes d'amitié envoyées
List<FriendRequest> _sentRequests = [];
bool _isLoadingSentRequests = false;
int _currentSentRequestPage = 0;
// Liste des demandes d'amitié reçues
List<FriendRequest> _receivedRequests = [];
bool _isLoadingReceivedRequests = false;
int _currentReceivedRequestPage = 0;
final int _requestsPerPage = 10;
// Liste des suggestions d'amis
List<dynamic> _friendSuggestions = [];
bool _isLoadingSuggestions = false;
// Service de notifications temps réel
RealtimeNotificationService? _realtimeService;
StreamSubscription<FriendRequestNotification>? _friendRequestSubscription;
// Getters pour accéder à l'état actuel des données
bool get isLoading => _isLoading;
bool get hasMore => _hasMore;
List<Friend> get friendsList => _friendsList;
List<FriendRequest> get sentRequests => _sentRequests;
List<FriendRequest> get receivedRequests => _receivedRequests;
bool get isLoadingSentRequests => _isLoadingSentRequests;
bool get isLoadingReceivedRequests => _isLoadingReceivedRequests;
List<dynamic> get friendSuggestions => _friendSuggestions;
bool get isLoadingSuggestions => _isLoadingSuggestions;
// Pour compatibilité avec l'ancien code
List<FriendRequest> get pendingRequests => _receivedRequests;
bool get isLoadingRequests => _isLoadingReceivedRequests;
/// Récupère la liste des amis pour un utilisateur donné avec pagination.
///
/// [userId] : L'identifiant unique de l'utilisateur connecté.
/// [loadMore] : Si vrai, charge plus d'amis, sinon recharge la liste depuis le début.
///
/// Cette méthode gère :
/// - La pagination de la liste d'amis.
/// - L'exclusion de l'utilisateur lui-même.
/// - Les erreurs et les logs pour une traçabilité complète.
Future<void> fetchFriends(String userId, {bool loadMore = false}) async {
if (_isLoading) {
AppLogger.w('Une opération de chargement est déjà en cours. Annulation de la nouvelle requête.', tag: 'FriendsProvider');
return;
}
_isLoading = true;
notifyListeners();
AppLogger.i('Début du chargement des amis pour l\'utilisateur $userId.', tag: 'FriendsProvider');
// Réinitialisation de la pagination si ce n'est pas un chargement supplémentaire
if (!loadMore) {
_friendsList = [];
_currentPage = 0;
_hasMore = true;
AppLogger.i('Réinitialisation de la pagination et de la liste des amis.', tag: 'FriendsProvider');
}
try {
AppLogger.d('Chargement de la page $_currentPage des amis pour l\'utilisateur $userId.', tag: 'FriendsProvider');
final newFriends = await friendsRepository.fetchFriends(userId, _currentPage, _friendsPerPage);
// Gestion de l'absence de nouveaux amis
if (newFriends.isEmpty) {
_hasMore = false;
AppLogger.i('Plus d\'amis à charger.', tag: 'FriendsProvider');
} else {
// Ajout des amis à la liste, en excluant l'utilisateur connecté
for (final friend in newFriends) {
if (friend.friendId != userId) {
_friendsList.add(friend);
AppLogger.d('Ami ajouté : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}', tag: 'FriendsProvider');
} else {
AppLogger.w("L'utilisateur connecté est exclu de la liste des amis : ${friend.friendId}", tag: 'FriendsProvider');
}
}
_currentPage++;
AppLogger.d('Préparation de la page suivante : $_currentPage', tag: 'FriendsProvider');
}
} catch (e, stackTrace) {
AppLogger.e('Erreur lors du chargement des amis', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
} finally {
_isLoading = false;
AppLogger.d('Fin du chargement des amis.', tag: 'FriendsProvider');
notifyListeners();
}
}
/// Supprime un ami de la liste locale et de l'API.
///
/// [friendId] : Identifiant unique de l'ami à supprimer.
///
/// Cette méthode :
/// - Loggue chaque étape.
/// - Enlève l'ami de la liste locale.
Future<void> removeFriend(String friendId) async {
try {
AppLogger.i('Suppression de l\'ami avec l\'ID : $friendId', tag: 'FriendsProvider');
await friendsRepository.removeFriend(friendId); // Appel API pour supprimer l'ami
_friendsList.removeWhere((friend) => friend.friendId == friendId); // Suppression locale
AppLogger.i('Ami supprimé localement avec succès : $friendId', tag: 'FriendsProvider');
} catch (e, stackTrace) {
AppLogger.e('Erreur lors de la suppression de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
} finally {
notifyListeners();
}
}
/// Récupère les détails d'un ami via l'API.
///
/// [userId] : Identifiant de l'utilisateur connecté.
/// [friendId] : Identifiant de l'ami dont on souhaite récupérer les détails.
///
/// Retourne un `Future<Friend?>` contenant les détails de l'ami ou `null` en cas d'erreur.
Future<Friend?> fetchFriendDetails(String userId, String friendId) async {
try {
AppLogger.d('Récupération des détails de l\'ami avec l\'ID : $friendId', tag: 'FriendsProvider');
final friendDetails = await friendsRepository.getFriendDetails(friendId, userId);
if (friendDetails != null) {
AppLogger.d('Détails de l\'ami récupérés avec succès : ${friendDetails.friendId}', tag: 'FriendsProvider');
} else {
AppLogger.w('Détails de l\'ami introuvables pour l\'ID : $friendId', tag: 'FriendsProvider');
}
return friendDetails;
} catch (e, stackTrace) {
AppLogger.e('Erreur lors de la récupération des détails de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
return null;
}
}
/// Convertit un statut sous forme de chaîne en [FriendStatus].
///
/// [status] : Le statut sous forme de chaîne (par exemple, 'pending', 'accepted').
///
/// Retourne un [FriendStatus] correspondant, ou `FriendStatus.unknown` si non reconnu.
FriendStatus _convertToFriendStatus(String status) {
switch (status.toLowerCase()) {
case 'pending':
return FriendStatus.pending;
case 'accepted':
return FriendStatus.accepted;
case 'blocked':
return FriendStatus.blocked;
default:
return FriendStatus.unknown;
}
}
/// Met à jour le statut d'un ami (ex. accepter, bloquer).
///
/// [friendId] : Identifiant de l'ami dont on souhaite mettre à jour le statut.
/// [status] : Nouveau statut sous forme de chaîne de caractères.
///
/// Loggue l'action, met à jour le statut en local et appelle l'API pour mettre à jour le statut.
Future<void> updateFriendStatus(String friendId, String status) async {
try {
AppLogger.i('Mise à jour du statut de l\'ami avec l\'ID : $friendId', tag: 'FriendsProvider');
// Conversion du statut sous forme de chaîne en statut spécifique
final friendStatus = _convertToFriendStatus(status);
await friendsRepository.updateFriendStatus(friendId, status); // Mise à jour dans l'API
// Mise à jour locale de la liste des amis avec le nouveau statut
final friendIndex = _friendsList.indexWhere((friend) => friend.friendId == friendId);
if (friendIndex != -1) {
_friendsList[friendIndex] = _friendsList[friendIndex].copyWith(status: friendStatus);
AppLogger.i('Statut de l\'ami mis à jour localement pour l\'ID : $friendId', tag: 'FriendsProvider');
}
} catch (e, stackTrace) {
AppLogger.e('Erreur lors de la mise à jour du statut de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
} finally {
notifyListeners();
}
}
/// Ajoute un nouvel ami.
///
/// [friendId] : L'identifiant unique de l'ami à ajouter.
///
/// Cette méthode :
/// - Loggue chaque étape.
/// - Envoie la demande d'ami via l'API.
/// - Rafraîchit la liste des amis si l'ajout réussit.
Future<void> addFriend(String friendId) async {
try {
// Récupérer le userId de l'utilisateur actuel
final currentUserId = await _getCurrentUserId();
if (currentUserId == null || currentUserId.isEmpty) {
throw Exception('Utilisateur non connecté');
}
// VALIDATION: Empêcher l'utilisateur de s'ajouter lui-même comme ami
if (currentUserId == friendId) {
AppLogger.w('Tentative d\'ajout de soi-même comme ami bloquée', tag: 'FriendsProvider');
throw Exception('Vous ne pouvez pas vous ajouter vous-même comme ami');
}
AppLogger.i('Ajout de l\'ami: userId=$currentUserId, friendId=$friendId', tag: 'FriendsProvider');
await friendsRepository.addFriend(currentUserId, friendId);
AppLogger.i('Demande d\'ami envoyée avec succès', tag: 'FriendsProvider');
// Rafraîchir la liste des amis après l'ajout
// Note: L'ami ne sera visible qu'après acceptation de la demande
} catch (e, stackTrace) {
AppLogger.e('Erreur lors de l\'ajout de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
rethrow; // Propager l'erreur pour que l'UI puisse l'afficher
} finally {
notifyListeners();
}
}
/// Récupère l'ID de l'utilisateur actuel depuis le stockage sécurisé
Future<String?> _getCurrentUserId() async {
try {
final secureStorage = SecureStorage();
return await secureStorage.getUserId();
} catch (e) {
AppLogger.e('Erreur lors de la récupération de l\'userId', error: e, tag: 'FriendsProvider');
return null;
}
}
/// Récupère les demandes d'amitié en attente pour l'utilisateur actuel (compatibilité).
Future<void> fetchPendingRequests({bool loadMore = false}) async {
await fetchReceivedRequests(loadMore: loadMore);
}
/// Récupère les demandes d'amitié envoyées par l'utilisateur actuel.
Future<void> fetchSentRequests({bool loadMore = false}) async {
try {
final currentUserId = await _getCurrentUserId();
if (currentUserId == null || currentUserId.isEmpty) {
throw Exception('Utilisateur non connecté');
}
if (!loadMore) {
_currentSentRequestPage = 0;
_sentRequests = [];
}
_isLoadingSentRequests = true;
notifyListeners();
final page = loadMore ? _currentSentRequestPage + 1 : 0;
final requests = await friendsRepository.getSentFriendRequests(
currentUserId,
page,
_requestsPerPage,
);
// VALIDATION: Pour les demandes envoyées, currentUserId doit être l'expéditeur (userId)
for (final request in requests) {
if (request.userId != currentUserId) {
AppLogger.e(
'INCOHÉRENCE DÉTECTÉE dans fetchSentRequests: '
'currentUserId=$currentUserId mais request.userId=${request.userId}, '
'request.friendId=${request.friendId}, '
'userFullName=${request.userFullName}, '
'friendFullName=${request.friendFullName}',
tag: 'FriendsProvider'
);
}
}
if (loadMore) {
_sentRequests.addAll(requests);
_currentSentRequestPage = page;
} else {
_sentRequests = requests;
_currentSentRequestPage = 0;
}
AppLogger.i('${requests.length} demandes d\'amitié envoyées récupérées', tag: 'FriendsProvider');
} catch (e, stackTrace) {
AppLogger.e('Erreur lors de la récupération des demandes envoyées', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
rethrow;
} finally {
_isLoadingSentRequests = false;
notifyListeners();
}
}
/// Récupère les demandes d'amitié reçues par l'utilisateur actuel.
Future<void> fetchReceivedRequests({bool loadMore = false}) async {
try {
final currentUserId = await _getCurrentUserId();
if (currentUserId == null || currentUserId.isEmpty) {
throw Exception('Utilisateur non connecté');
}
if (!loadMore) {
_currentReceivedRequestPage = 0;
_receivedRequests = [];
}
_isLoadingReceivedRequests = true;
notifyListeners();
final page = loadMore ? _currentReceivedRequestPage + 1 : 0;
final requests = await friendsRepository.getReceivedFriendRequests(
currentUserId,
page,
_requestsPerPage,
);
// VALIDATION: Pour les demandes reçues, currentUserId doit être le destinataire (friendId)
for (final request in requests) {
if (request.friendId != currentUserId) {
AppLogger.e(
'INCOHÉRENCE DÉTECTÉE dans fetchReceivedRequests: '
'currentUserId=$currentUserId mais request.friendId=${request.friendId}, '
'request.userId=${request.userId}, '
'userFullName=${request.userFullName}, '
'friendFullName=${request.friendFullName}',
tag: 'FriendsProvider'
);
}
}
if (loadMore) {
_receivedRequests.addAll(requests);
_currentReceivedRequestPage = page;
} else {
_receivedRequests = requests;
_currentReceivedRequestPage = 0;
}
AppLogger.i('${requests.length} demandes d\'amitié reçues récupérées', tag: 'FriendsProvider');
} catch (e, stackTrace) {
AppLogger.e('Erreur lors de la récupération des demandes reçues', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
rethrow;
} finally {
_isLoadingReceivedRequests = false;
notifyListeners();
}
}
/// Accepte une demande d'amitié.
Future<void> acceptFriendRequest(String friendshipId) async {
try {
AppLogger.i('Acceptation de la demande d\'amitié: $friendshipId', tag: 'FriendsProvider');
await friendsRepository.acceptFriendRequest(friendshipId);
// Retirer la demande de la liste des demandes reçues
_receivedRequests.removeWhere((req) => req.friendshipId == friendshipId);
// Rafraîchir la liste des amis
final currentUserId = await _getCurrentUserId();
if (currentUserId != null) {
await fetchFriends(currentUserId);
}
AppLogger.i('Demande d\'amitié acceptée avec succès', tag: 'FriendsProvider');
} catch (e, stackTrace) {
AppLogger.e('Erreur lors de l\'acceptation de la demande', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
rethrow;
} finally {
notifyListeners();
}
}
/// Rejette une demande d'amitié.
Future<void> rejectFriendRequest(String friendshipId) async {
try {
AppLogger.i('Rejet de la demande d\'amitié: $friendshipId', tag: 'FriendsProvider');
await friendsRepository.rejectFriendRequest(friendshipId);
// Retirer la demande de la liste des demandes reçues
_receivedRequests.removeWhere((req) => req.friendshipId == friendshipId);
AppLogger.i('Demande d\'amitié rejetée avec succès', tag: 'FriendsProvider');
} catch (e, stackTrace) {
AppLogger.e('Erreur lors du rejet de la demande', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
rethrow;
} finally {
notifyListeners();
}
}
/// Annule une demande d'amitié envoyée.
Future<void> cancelFriendRequest(String friendshipId) async {
try {
AppLogger.i('Annulation de la demande d\'amitié: $friendshipId', tag: 'FriendsProvider');
await friendsRepository.cancelFriendRequest(friendshipId);
// Retirer la demande de la liste des demandes envoyées
_sentRequests.removeWhere((req) => req.friendshipId == friendshipId);
AppLogger.i('Demande d\'amitié annulée avec succès', tag: 'FriendsProvider');
} catch (e, stackTrace) {
AppLogger.e('Erreur lors de l\'annulation de la demande', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
rethrow;
} finally {
notifyListeners();
}
}
/// Récupère les suggestions d'amis pour l'utilisateur actuel.
///
/// Les suggestions sont basées sur les amis en commun et d'autres critères
/// de pertinence définis par le backend.
///
/// [limit] : Nombre maximum de suggestions à récupérer (par défaut 10).
Future<void> fetchFriendSuggestions({int limit = 10}) async {
try {
final currentUserId = await _getCurrentUserId();
if (currentUserId == null || currentUserId.isEmpty) {
throw Exception('Utilisateur non connecté');
}
_isLoadingSuggestions = true;
notifyListeners();
AppLogger.i('Récupération des suggestions d\'amis (limit: $limit)', tag: 'FriendsProvider');
_friendSuggestions = await friendsRepository.getFriendSuggestions(currentUserId, limit: limit);
AppLogger.i('${_friendSuggestions.length} suggestions d\'amis récupérées', tag: 'FriendsProvider');
} catch (e, stackTrace) {
AppLogger.e('Erreur lors de la récupération des suggestions', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
_friendSuggestions = [];
rethrow;
} finally {
_isLoadingSuggestions = false;
notifyListeners();
}
}
/// Connecte le service de notifications temps réel.
///
/// Cette méthode doit être appelée après la connexion de l'utilisateur pour
/// recevoir les notifications de demandes d'amitié en temps réel.
///
/// [service] : Le service de notifications temps réel à connecter.
void connectRealtime(RealtimeNotificationService service) {
_realtimeService = service;
// Écouter les demandes d'amitié en temps réel
_friendRequestSubscription = service.friendRequestStream.listen(
_handleFriendRequestNotification,
onError: (error) {
AppLogger.e('Erreur dans le stream de demandes d\'amitié', error: error, tag: 'FriendsProvider');
},
);
AppLogger.i('Service de notifications temps réel connecté pour les demandes d\'amitié', tag: 'FriendsProvider');
}
/// Gère les notifications de demandes d'amitié reçues en temps réel.
///
/// Cette méthode est appelée automatiquement lorsqu'une notification
/// est reçue via WebSocket.
void _handleFriendRequestNotification(FriendRequestNotification notification) {
AppLogger.i('Notification de demande d\'amitié reçue: ${notification.type}', tag: 'FriendsProvider');
switch (notification.type) {
case 'received':
// Rafraîchir les demandes reçues pour inclure la nouvelle demande
_refreshReceivedRequests();
AppLogger.i('Nouvelle demande d\'amitié de ${notification.senderName}', tag: 'FriendsProvider');
break;
case 'accepted':
// Rafraîchir la liste d'amis pour inclure le nouvel ami
_refreshFriendsList();
// Supprimer de la liste des demandes envoyées si présente
_sentRequests.removeWhere((request) => request.friendshipId == notification.requestId);
notifyListeners();
AppLogger.i('${notification.senderName} a accepté votre demande', tag: 'FriendsProvider');
break;
case 'rejected':
// Supprimer de la liste des demandes envoyées
_sentRequests.removeWhere((request) => request.friendshipId == notification.requestId);
notifyListeners();
AppLogger.i('Demande d\'amitié rejetée: ${notification.requestId}', tag: 'FriendsProvider');
break;
default:
AppLogger.w('Type de notification inconnu: ${notification.type}', tag: 'FriendsProvider');
}
}
/// Rafraîchit la liste des demandes reçues en arrière-plan.
Future<void> _refreshReceivedRequests() async {
try {
await fetchReceivedRequests(loadMore: false);
} catch (e) {
AppLogger.e('Erreur lors du rafraîchissement des demandes reçues', error: e, tag: 'FriendsProvider');
}
}
/// Rafraîchit la liste d'amis en arrière-plan.
Future<void> _refreshFriendsList() async {
try {
final currentUserId = await _getCurrentUserId();
if (currentUserId == null || currentUserId.isEmpty) return;
await fetchFriends(currentUserId, loadMore: false);
} catch (e) {
AppLogger.e('Erreur lors du rafraîchissement de la liste d\'amis', error: e, tag: 'FriendsProvider');
}
}
/// Déconnecte le service de notifications temps réel.
void disconnectRealtime() {
_friendRequestSubscription?.cancel();
_friendRequestSubscription = null;
_realtimeService = null;
AppLogger.i('Service de notifications temps réel déconnecté', tag: 'FriendsProvider');
}
@override
void dispose() {
disconnectRealtime();
super.dispose();
}
}