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
This commit is contained in:
dahoud
2026-01-10 10:43:17 +00:00
parent 06031b01f2
commit 92612abbd7
321 changed files with 43137 additions and 4285 deletions

View File

@@ -0,0 +1,405 @@
import 'package:dartz/dartz.dart';
import '../../core/errors/exceptions.dart';
import '../../core/errors/failures.dart';
import '../../core/utils/app_logger.dart';
import '../../domain/entities/chat_message.dart';
import '../../domain/entities/conversation.dart';
import '../../domain/repositories/chat_repository.dart';
import '../datasources/chat_remote_data_source.dart';
/// Implémentation du repository de chat.
///
/// Cette classe fait le lien entre la couche domaine et la couche données,
/// en convertissant les exceptions en failures et en gérant la transformation
/// entre les modèles de données et les entités de domaine.
///
/// **Usage:**
/// ```dart
/// final repository = ChatRepositoryImpl(
/// remoteDataSource: chatRemoteDataSource,
/// );
/// final result = await repository.getConversations('userId');
/// result.fold(
/// (failure) => print('Erreur: $failure'),
/// (conversations) => print('${conversations.length} conversations'),
/// );
/// ```
class ChatRepositoryImpl implements ChatRepository {
/// Crée une nouvelle instance de [ChatRepositoryImpl].
///
/// [remoteDataSource] La source de données distante pour le chat
ChatRepositoryImpl({required this.remoteDataSource});
/// Source de données distante pour les opérations de chat
final ChatRemoteDataSource remoteDataSource;
/// Log un message si le mode debug est activé.
void _log(String message) {
AppLogger.d(message, tag: 'ChatRepositoryImpl');
}
@override
Future<Either<Failure, List<Conversation>>> getConversations(
String userId,
) async {
_log('Récupération des conversations pour $userId');
try {
if (userId.isEmpty) {
return const Left(ValidationFailure(message:
'L\'ID utilisateur ne peut pas être vide',
field: 'userId',
));
}
final conversationModels = await remoteDataSource.getConversations(userId);
final conversations = conversationModels.map((model) => model.toEntity()).toList();
_log('${conversations.length} conversations récupérées');
return Right(conversations);
} on ServerException catch (e) {
_log('Erreur serveur: ${e.message}');
return Left(ServerFailure(
message: e.message,
statusCode: e.statusCode,
));
} on UnauthorizedException catch (e) {
_log('Non autorisé: ${e.message}');
return Left(AuthenticationFailure(
message: e.message,
code: 'UNAUTHORIZED',
));
} catch (e) {
_log('Erreur inattendue: $e');
return Left(ServerFailure(
message: 'Erreur lors de la récupération des conversations: $e',
));
}
}
@override
Future<Either<Failure, Conversation>> getOrCreateConversation(
String userId,
String participantId,
) async {
_log('Récupération/création conversation entre $userId et $participantId');
try {
if (userId.isEmpty || participantId.isEmpty) {
return const Left(ValidationFailure(message:
'Les IDs utilisateur et participant sont requis',
));
}
if (userId == participantId) {
return const Left(ValidationFailure(message:
'Impossible de créer une conversation avec soi-même',
));
}
final conversationModel = await remoteDataSource.getOrCreateConversation(
userId,
participantId,
);
_log('Conversation récupérée/créée avec succès');
return Right(conversationModel.toEntity());
} on ServerException catch (e) {
_log('Erreur serveur: ${e.message}');
return Left(ServerFailure(
message: e.message,
statusCode: e.statusCode,
));
} on UnauthorizedException catch (e) {
_log('Non autorisé: ${e.message}');
return Left(AuthenticationFailure(
message: e.message,
code: 'UNAUTHORIZED',
));
} catch (e) {
_log('Erreur inattendue: $e');
return Left(ServerFailure(
message: 'Erreur lors de la récupération/création de la conversation: $e',
));
}
}
@override
Future<Either<Failure, List<ChatMessage>>> getMessages(
String conversationId, {
int page = 0,
int size = 50,
}) async {
_log('Récupération des messages de $conversationId (page: $page)');
try {
if (conversationId.isEmpty) {
return const Left(ValidationFailure(message:
'L\'ID de conversation ne peut pas être vide',
field: 'conversationId',
));
}
final messageModels = await remoteDataSource.getMessages(
conversationId,
page: page,
size: size,
);
final messages = messageModels.map((model) => model.toEntity()).toList();
_log('${messages.length} messages récupérés');
return Right(messages);
} on ServerException catch (e) {
_log('Erreur serveur: ${e.message}');
return Left(ServerFailure(
message: e.message,
statusCode: e.statusCode,
));
} on UnauthorizedException catch (e) {
_log('Non autorisé: ${e.message}');
return Left(AuthenticationFailure(
message: e.message,
code: 'UNAUTHORIZED',
));
} catch (e) {
_log('Erreur inattendue: $e');
return Left(ServerFailure(
message: 'Erreur lors de la récupération des messages: $e',
));
}
}
@override
Future<Either<Failure, ChatMessage>> sendMessage({
required String senderId,
required String recipientId,
required String content,
String? messageType,
String? mediaUrl,
}) async {
_log('Envoi d\'un message de $senderId à $recipientId');
try {
if (senderId.isEmpty || recipientId.isEmpty) {
return const Left(ValidationFailure(message:
'Les IDs expéditeur et destinataire sont requis',
));
}
if (content.isEmpty) {
return const Left(ValidationFailure(message:
'Le contenu du message ne peut pas être vide',
field: 'content',
));
}
if (senderId == recipientId) {
return const Left(ValidationFailure(message:
'Impossible d\'envoyer un message à soi-même',
));
}
final messageModel = await remoteDataSource.sendMessage(
senderId: senderId,
recipientId: recipientId,
content: content,
messageType: messageType,
mediaUrl: mediaUrl,
);
_log('Message envoyé avec succès');
return Right(messageModel.toEntity());
} on ServerException catch (e) {
_log('Erreur serveur: ${e.message}');
return Left(ServerFailure(
message: e.message,
statusCode: e.statusCode,
));
} on UnauthorizedException catch (e) {
_log('Non autorisé: ${e.message}');
return Left(AuthenticationFailure(
message: e.message,
code: 'UNAUTHORIZED',
));
} catch (e) {
_log('Erreur inattendue: $e');
return Left(ServerFailure(
message: 'Erreur lors de l\'envoi du message: $e',
));
}
}
@override
Future<Either<Failure, void>> markMessageAsRead(String messageId) async {
_log('Marquage du message $messageId comme lu');
try {
if (messageId.isEmpty) {
return const Left(ValidationFailure(message:
'L\'ID du message ne peut pas être vide',
field: 'messageId',
));
}
await remoteDataSource.markMessageAsRead(messageId);
_log('Message marqué comme lu');
return const Right(null);
} on ServerException catch (e) {
_log('Erreur serveur: ${e.message}');
return Left(ServerFailure(
message: e.message,
statusCode: e.statusCode,
));
} on UnauthorizedException catch (e) {
_log('Non autorisé: ${e.message}');
return Left(AuthenticationFailure(
message: e.message,
code: 'UNAUTHORIZED',
));
} catch (e) {
_log('Erreur inattendue: $e');
return Left(ServerFailure(
message: 'Erreur lors du marquage du message: $e',
));
}
}
@override
Future<Either<Failure, void>> markConversationAsRead(
String conversationId,
String userId,
) async {
_log('Marquage de tous les messages de $conversationId comme lus');
try {
if (conversationId.isEmpty || userId.isEmpty) {
return const Left(ValidationFailure(message:
'Les IDs conversation et utilisateur sont requis',
));
}
await remoteDataSource.markConversationAsRead(conversationId, userId);
_log('Conversation marquée comme lue');
return const Right(null);
} on ServerException catch (e) {
_log('Erreur serveur: ${e.message}');
return Left(ServerFailure(
message: e.message,
statusCode: e.statusCode,
));
} on UnauthorizedException catch (e) {
_log('Non autorisé: ${e.message}');
return Left(AuthenticationFailure(
message: e.message,
code: 'UNAUTHORIZED',
));
} catch (e) {
_log('Erreur inattendue: $e');
return Left(ServerFailure(
message: 'Erreur lors du marquage de la conversation: $e',
));
}
}
@override
Future<Either<Failure, void>> deleteMessage(String messageId) async {
_log('Suppression du message $messageId');
try {
if (messageId.isEmpty) {
return const Left(ValidationFailure(message:
'L\'ID du message ne peut pas être vide',
field: 'messageId',
));
}
await remoteDataSource.deleteMessage(messageId);
_log('Message supprimé');
return const Right(null);
} on ServerException catch (e) {
_log('Erreur serveur: ${e.message}');
return Left(ServerFailure(
message: e.message,
statusCode: e.statusCode,
));
} on UnauthorizedException catch (e) {
_log('Non autorisé: ${e.message}');
return Left(AuthenticationFailure(
message: e.message,
code: 'UNAUTHORIZED',
));
} catch (e) {
_log('Erreur inattendue: $e');
return Left(ServerFailure(
message: 'Erreur lors de la suppression du message: $e',
));
}
}
@override
Future<Either<Failure, void>> deleteConversation(String conversationId) async {
_log('Suppression de la conversation $conversationId');
try {
if (conversationId.isEmpty) {
return const Left(ValidationFailure(message:
'L\'ID de la conversation ne peut pas être vide',
field: 'conversationId',
));
}
await remoteDataSource.deleteConversation(conversationId);
_log('Conversation supprimée');
return const Right(null);
} on ServerException catch (e) {
_log('Erreur serveur: ${e.message}');
return Left(ServerFailure(
message: e.message,
statusCode: e.statusCode,
));
} on UnauthorizedException catch (e) {
_log('Non autorisé: ${e.message}');
return Left(AuthenticationFailure(
message: e.message,
code: 'UNAUTHORIZED',
));
} catch (e) {
_log('Erreur inattendue: $e');
return Left(ServerFailure(
message: 'Erreur lors de la suppression de la conversation: $e',
));
}
}
@override
Future<Either<Failure, int>> getUnreadMessagesCount(String userId) async {
_log('Récupération du nombre de messages non lus pour $userId');
try {
if (userId.isEmpty) {
return const Left(ValidationFailure(message:
'L\'ID utilisateur ne peut pas être vide',
field: 'userId',
));
}
final count = await remoteDataSource.getUnreadMessagesCount(userId);
_log('$count messages non lus');
return Right(count);
} on ServerException catch (e) {
_log('Erreur serveur: ${e.message}');
return Left(ServerFailure(
message: e.message,
statusCode: e.statusCode,
));
} on UnauthorizedException catch (e) {
_log('Non autorisé: ${e.message}');
return Left(AuthenticationFailure(
message: e.message,
code: 'UNAUTHORIZED',
));
} catch (e) {
_log('Erreur inattendue: $e');
return Left(ServerFailure(
message: 'Erreur lors de la récupération du nombre de messages non lus: $e',
));
}
}
}