## 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
581 lines
18 KiB
Dart
581 lines
18 KiB
Dart
import 'package:equatable/equatable.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../../core/utils/app_logger.dart';
|
|
import '../../domain/entities/chat_message.dart';
|
|
import '../../domain/entities/conversation.dart';
|
|
import '../../domain/repositories/chat_repository.dart';
|
|
|
|
/// Bloc pour la gestion des événements et états liés au chat.
|
|
///
|
|
/// Ce bloc gère:
|
|
/// - Le chargement des conversations
|
|
/// - Le chargement des messages d'une conversation
|
|
/// - L'envoi de messages
|
|
/// - Le marquage des messages comme lus
|
|
/// - La suppression de messages et conversations
|
|
/// - Le compteur de messages non lus
|
|
class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|
/// Constructeur avec injection du repository de chat.
|
|
ChatBloc({required this.chatRepository}) : super(const ChatState()) {
|
|
on<LoadConversations>(_onLoadConversations);
|
|
on<LoadMessages>(_onLoadMessages);
|
|
on<SendMessage>(_onSendMessage);
|
|
on<MarkMessageAsRead>(_onMarkMessageAsRead);
|
|
on<MarkMessageAsDelivered>(_onMarkMessageAsDelivered);
|
|
on<MarkConversationAsRead>(_onMarkConversationAsRead);
|
|
on<DeleteMessage>(_onDeleteMessage);
|
|
on<DeleteConversation>(_onDeleteConversation);
|
|
on<LoadUnreadCount>(_onLoadUnreadCount);
|
|
on<AddMessageToConversation>(_onAddMessageToConversation);
|
|
on<UpdateTypingStatus>(_onUpdateTypingStatus);
|
|
}
|
|
|
|
final ChatRepository chatRepository;
|
|
|
|
void _log(String message) {
|
|
AppLogger.d(message, tag: 'ChatBloc');
|
|
}
|
|
|
|
/// Charge toutes les conversations d'un utilisateur.
|
|
Future<void> _onLoadConversations(
|
|
LoadConversations event,
|
|
Emitter<ChatState> emit,
|
|
) async {
|
|
_log('Chargement des conversations pour ${event.userId}');
|
|
emit(state.copyWith(conversationsStatus: ConversationsStatus.loading));
|
|
|
|
final result = await chatRepository.getConversations(event.userId);
|
|
result.fold(
|
|
(failure) {
|
|
_log('Erreur: ${failure.message}');
|
|
emit(state.copyWith(
|
|
conversationsStatus: ConversationsStatus.error,
|
|
errorMessage: failure.message,
|
|
));
|
|
},
|
|
(conversations) {
|
|
_log('${conversations.length} conversations chargées');
|
|
emit(state.copyWith(
|
|
conversationsStatus: ConversationsStatus.success,
|
|
conversations: conversations,
|
|
));
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Charge les messages d'une conversation.
|
|
Future<void> _onLoadMessages(
|
|
LoadMessages event,
|
|
Emitter<ChatState> emit,
|
|
) async {
|
|
_log('Chargement des messages de ${event.conversationId}');
|
|
emit(state.copyWith(messagesStatus: MessagesStatus.loading));
|
|
|
|
final result = await chatRepository.getMessages(
|
|
event.conversationId,
|
|
page: event.page,
|
|
size: event.size,
|
|
);
|
|
result.fold(
|
|
(failure) {
|
|
_log('Erreur: ${failure.message}');
|
|
emit(state.copyWith(
|
|
messagesStatus: MessagesStatus.error,
|
|
errorMessage: failure.message,
|
|
));
|
|
},
|
|
(messages) {
|
|
_log('${messages.length} messages chargés');
|
|
emit(state.copyWith(
|
|
messagesStatus: MessagesStatus.success,
|
|
messages: messages,
|
|
currentConversationId: event.conversationId,
|
|
));
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Envoie un nouveau message.
|
|
Future<void> _onSendMessage(
|
|
SendMessage event,
|
|
Emitter<ChatState> emit,
|
|
) async {
|
|
_log('Envoi d\'un message');
|
|
|
|
// 🚀 OPTIMISTIC UI: Créer un message temporaire IMMÉDIATEMENT
|
|
final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
|
|
final tempMessage = ChatMessage(
|
|
id: tempId,
|
|
conversationId: state.currentConversationId ?? '',
|
|
senderId: event.senderId,
|
|
senderFirstName: '', // Sera mis à jour par le serveur
|
|
senderLastName: '',
|
|
senderProfileImageUrl: '',
|
|
content: event.content,
|
|
timestamp: DateTime.now(),
|
|
isRead: false,
|
|
isDelivered: false, // Pas encore délivré
|
|
attachmentUrl: event.mediaUrl ?? '',
|
|
attachmentType: event.messageType == 'image' ? AttachmentType.image :
|
|
event.messageType == 'video' ? AttachmentType.video :
|
|
event.messageType == 'audio' ? AttachmentType.audio :
|
|
event.messageType == 'file' ? AttachmentType.file :
|
|
AttachmentType.none,
|
|
);
|
|
|
|
AppLogger.d('Message optimiste créé avec ID temporaire: $tempId', tag: 'ChatBloc');
|
|
|
|
// Ajouter immédiatement à la liste (avant la requête HTTP)
|
|
final optimisticMessages = List<ChatMessage>.from(state.messages);
|
|
optimisticMessages.insert(0, tempMessage);
|
|
|
|
emit(state.copyWith(
|
|
sendMessageStatus: SendMessageStatus.sending,
|
|
messages: optimisticMessages,
|
|
));
|
|
|
|
// Maintenant envoyer au serveur
|
|
final result = await chatRepository.sendMessage(
|
|
senderId: event.senderId,
|
|
recipientId: event.recipientId,
|
|
content: event.content,
|
|
messageType: event.messageType,
|
|
mediaUrl: event.mediaUrl,
|
|
);
|
|
|
|
result.fold(
|
|
(failure) {
|
|
_log('Erreur: ${failure.message}');
|
|
AppLogger.e('Échec envoi - Retrait du message temporaire', tag: 'ChatBloc');
|
|
|
|
// Retirer le message temporaire en cas d'erreur
|
|
final revertedMessages = state.messages
|
|
.where((msg) => msg.id != tempId)
|
|
.toList();
|
|
|
|
emit(state.copyWith(
|
|
sendMessageStatus: SendMessageStatus.error,
|
|
errorMessage: failure.message,
|
|
messages: revertedMessages,
|
|
));
|
|
},
|
|
(message) {
|
|
_log('Message envoyé avec succès');
|
|
AppLogger.d('Message envoyé - ID réel: ${message.id}, isDelivered: ${message.isDelivered}, isRead: ${message.isRead}', tag: 'ChatBloc');
|
|
|
|
// Remplacer le message temporaire par le vrai message du serveur
|
|
final updatedMessages = state.messages.map((msg) {
|
|
if (msg.id == tempId) {
|
|
AppLogger.d('Remplacement du message temporaire par le message réel', tag: 'ChatBloc');
|
|
return message;
|
|
}
|
|
return msg;
|
|
}).toList();
|
|
|
|
emit(state.copyWith(
|
|
sendMessageStatus: SendMessageStatus.success,
|
|
messages: updatedMessages,
|
|
));
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Marque un message comme lu.
|
|
Future<void> _onMarkMessageAsRead(
|
|
MarkMessageAsRead event,
|
|
Emitter<ChatState> emit,
|
|
) async {
|
|
_log('Marquage du message ${event.messageId} comme lu');
|
|
|
|
final result = await chatRepository.markMessageAsRead(event.messageId);
|
|
result.fold(
|
|
(failure) => _log('Erreur: ${failure.message}'),
|
|
(_) => _log('Message marqué comme lu'),
|
|
);
|
|
}
|
|
|
|
/// Marque un message comme délivré (reçu par le destinataire).
|
|
void _onMarkMessageAsDelivered(
|
|
MarkMessageAsDelivered event,
|
|
Emitter<ChatState> emit,
|
|
) {
|
|
_log('Marquage du message ${event.messageId} comme délivré');
|
|
AppLogger.d('_onMarkMessageAsDelivered appelé pour ${event.messageId}', tag: 'ChatBloc');
|
|
AppLogger.d('Nombre de messages actuels: ${state.messages.length}', tag: 'ChatBloc');
|
|
|
|
// Mettre à jour le message dans la liste des messages
|
|
final updatedMessages = state.messages.map((msg) {
|
|
if (msg.id == event.messageId) {
|
|
AppLogger.d('Message trouvé! Avant: isDelivered=${msg.isDelivered}, Après: isDelivered=true', tag: 'ChatBloc');
|
|
return msg.copyWith(isDelivered: true);
|
|
}
|
|
return msg;
|
|
}).toList();
|
|
|
|
AppLogger.d('Émission du nouveau state avec ${updatedMessages.length} messages', tag: 'ChatBloc');
|
|
emit(state.copyWith(messages: updatedMessages));
|
|
}
|
|
|
|
/// Marque tous les messages d'une conversation comme lus.
|
|
Future<void> _onMarkConversationAsRead(
|
|
MarkConversationAsRead event,
|
|
Emitter<ChatState> emit,
|
|
) async {
|
|
_log('Marquage de la conversation ${event.conversationId} comme lue');
|
|
|
|
final result = await chatRepository.markConversationAsRead(
|
|
event.conversationId,
|
|
event.userId,
|
|
);
|
|
result.fold(
|
|
(failure) => _log('Erreur: ${failure.message}'),
|
|
(_) {
|
|
_log('Conversation marquée comme lue');
|
|
// Mettre à jour le compteur de non lus dans la conversation
|
|
final updatedConversations = state.conversations.map((conv) {
|
|
if (conv.id == event.conversationId) {
|
|
return Conversation(
|
|
id: conv.id,
|
|
participantId: conv.participantId,
|
|
participantFirstName: conv.participantFirstName,
|
|
participantLastName: conv.participantLastName,
|
|
participantProfileImageUrl: conv.participantProfileImageUrl,
|
|
lastMessage: conv.lastMessage,
|
|
lastMessageTimestamp: conv.lastMessageTimestamp,
|
|
unreadCount: 0,
|
|
isTyping: conv.isTyping,
|
|
);
|
|
}
|
|
return conv;
|
|
}).toList();
|
|
|
|
emit(state.copyWith(conversations: updatedConversations));
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Supprime un message.
|
|
Future<void> _onDeleteMessage(
|
|
DeleteMessage event,
|
|
Emitter<ChatState> emit,
|
|
) async {
|
|
_log('Suppression du message ${event.messageId}');
|
|
|
|
final result = await chatRepository.deleteMessage(event.messageId);
|
|
result.fold(
|
|
(failure) {
|
|
_log('Erreur: ${failure.message}');
|
|
emit(state.copyWith(errorMessage: failure.message));
|
|
},
|
|
(_) {
|
|
_log('Message supprimé');
|
|
// Retirer le message de la liste
|
|
final updatedMessages = state.messages
|
|
.where((msg) => msg.id != event.messageId)
|
|
.toList();
|
|
|
|
emit(state.copyWith(messages: updatedMessages));
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Supprime une conversation.
|
|
Future<void> _onDeleteConversation(
|
|
DeleteConversation event,
|
|
Emitter<ChatState> emit,
|
|
) async {
|
|
_log('Suppression de la conversation ${event.conversationId}');
|
|
|
|
final result = await chatRepository.deleteConversation(event.conversationId);
|
|
result.fold(
|
|
(failure) {
|
|
_log('Erreur: ${failure.message}');
|
|
emit(state.copyWith(errorMessage: failure.message));
|
|
},
|
|
(_) {
|
|
_log('Conversation supprimée');
|
|
// Retirer la conversation de la liste
|
|
final updatedConversations = state.conversations
|
|
.where((conv) => conv.id != event.conversationId)
|
|
.toList();
|
|
|
|
emit(state.copyWith(conversations: updatedConversations));
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Charge le nombre de messages non lus.
|
|
Future<void> _onLoadUnreadCount(
|
|
LoadUnreadCount event,
|
|
Emitter<ChatState> emit,
|
|
) async {
|
|
_log('Chargement du nombre de messages non lus');
|
|
|
|
final result = await chatRepository.getUnreadMessagesCount(event.userId);
|
|
result.fold(
|
|
(failure) => _log('Erreur: ${failure.message}'),
|
|
(count) {
|
|
_log('$count messages non lus');
|
|
emit(state.copyWith(unreadCount: count));
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Ajoute un message reçu via WebSocket à la conversation.
|
|
void _onAddMessageToConversation(
|
|
AddMessageToConversation event,
|
|
Emitter<ChatState> emit,
|
|
) {
|
|
_log('Ajout d\'un message reçu via WebSocket');
|
|
|
|
final updatedMessages = List<ChatMessage>.from(state.messages);
|
|
updatedMessages.insert(0, event.message);
|
|
|
|
emit(state.copyWith(messages: updatedMessages));
|
|
}
|
|
|
|
/// Met à jour le statut de frappe d'un utilisateur.
|
|
void _onUpdateTypingStatus(
|
|
UpdateTypingStatus event,
|
|
Emitter<ChatState> emit,
|
|
) {
|
|
_log('Mise à jour du statut de frappe pour ${event.conversationId}');
|
|
|
|
final updatedConversations = state.conversations.map((conv) {
|
|
if (conv.id == event.conversationId) {
|
|
return Conversation(
|
|
id: conv.id,
|
|
participantId: conv.participantId,
|
|
participantFirstName: conv.participantFirstName,
|
|
participantLastName: conv.participantLastName,
|
|
participantProfileImageUrl: conv.participantProfileImageUrl,
|
|
lastMessage: conv.lastMessage,
|
|
lastMessageTimestamp: conv.lastMessageTimestamp,
|
|
unreadCount: conv.unreadCount,
|
|
isTyping: event.isTyping,
|
|
);
|
|
}
|
|
return conv;
|
|
}).toList();
|
|
|
|
emit(state.copyWith(conversations: updatedConversations));
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// ÉVÉNEMENTS
|
|
// ============================================================================
|
|
|
|
/// Classe de base pour tous les événements du chat.
|
|
abstract class ChatEvent extends Equatable {
|
|
const ChatEvent();
|
|
|
|
@override
|
|
List<Object?> get props => [];
|
|
}
|
|
|
|
/// Événement pour charger les conversations d'un utilisateur.
|
|
class LoadConversations extends ChatEvent {
|
|
const LoadConversations(this.userId);
|
|
|
|
final String userId;
|
|
|
|
@override
|
|
List<Object?> get props => [userId];
|
|
}
|
|
|
|
/// Événement pour charger les messages d'une conversation.
|
|
class LoadMessages extends ChatEvent {
|
|
const LoadMessages({
|
|
required this.conversationId,
|
|
this.page = 0,
|
|
this.size = 50,
|
|
});
|
|
|
|
final String conversationId;
|
|
final int page;
|
|
final int size;
|
|
|
|
@override
|
|
List<Object?> get props => [conversationId, page, size];
|
|
}
|
|
|
|
/// Événement pour envoyer un message.
|
|
class SendMessage extends ChatEvent {
|
|
const SendMessage({
|
|
required this.senderId,
|
|
required this.recipientId,
|
|
required this.content,
|
|
this.messageType,
|
|
this.mediaUrl,
|
|
});
|
|
|
|
final String senderId;
|
|
final String recipientId;
|
|
final String content;
|
|
final String? messageType;
|
|
final String? mediaUrl;
|
|
|
|
@override
|
|
List<Object?> get props => [senderId, recipientId, content, messageType, mediaUrl];
|
|
}
|
|
|
|
/// Événement pour marquer un message comme lu.
|
|
class MarkMessageAsRead extends ChatEvent {
|
|
const MarkMessageAsRead(this.messageId);
|
|
|
|
final String messageId;
|
|
|
|
@override
|
|
List<Object?> get props => [messageId];
|
|
}
|
|
|
|
/// Événement pour marquer tous les messages d'une conversation comme lus.
|
|
class MarkConversationAsRead extends ChatEvent {
|
|
const MarkConversationAsRead({
|
|
required this.conversationId,
|
|
required this.userId,
|
|
});
|
|
|
|
final String conversationId;
|
|
final String userId;
|
|
|
|
@override
|
|
List<Object?> get props => [conversationId, userId];
|
|
}
|
|
|
|
/// Événement pour supprimer un message.
|
|
class DeleteMessage extends ChatEvent {
|
|
const DeleteMessage(this.messageId);
|
|
|
|
final String messageId;
|
|
|
|
@override
|
|
List<Object?> get props => [messageId];
|
|
}
|
|
|
|
/// Événement pour supprimer une conversation.
|
|
class DeleteConversation extends ChatEvent {
|
|
const DeleteConversation(this.conversationId);
|
|
|
|
final String conversationId;
|
|
|
|
@override
|
|
List<Object?> get props => [conversationId];
|
|
}
|
|
|
|
/// Événement pour charger le nombre de messages non lus.
|
|
class LoadUnreadCount extends ChatEvent {
|
|
const LoadUnreadCount(this.userId);
|
|
|
|
final String userId;
|
|
|
|
@override
|
|
List<Object?> get props => [userId];
|
|
}
|
|
|
|
/// Événement pour ajouter un message reçu via WebSocket.
|
|
class AddMessageToConversation extends ChatEvent {
|
|
const AddMessageToConversation(this.message);
|
|
|
|
final ChatMessage message;
|
|
|
|
@override
|
|
List<Object?> get props => [message];
|
|
}
|
|
|
|
/// Événement pour marquer un message comme délivré.
|
|
class MarkMessageAsDelivered extends ChatEvent {
|
|
const MarkMessageAsDelivered(this.messageId);
|
|
|
|
final String messageId;
|
|
|
|
@override
|
|
List<Object?> get props => [messageId];
|
|
}
|
|
|
|
/// Événement pour mettre à jour le statut de frappe.
|
|
class UpdateTypingStatus extends ChatEvent {
|
|
const UpdateTypingStatus({
|
|
required this.conversationId,
|
|
required this.isTyping,
|
|
});
|
|
|
|
final String conversationId;
|
|
final bool isTyping;
|
|
|
|
@override
|
|
List<Object?> get props => [conversationId, isTyping];
|
|
}
|
|
|
|
// ============================================================================
|
|
// ÉTATS
|
|
// ============================================================================
|
|
|
|
/// Énumération des statuts de chargement des conversations.
|
|
enum ConversationsStatus { initial, loading, success, error }
|
|
|
|
/// Énumération des statuts de chargement des messages.
|
|
enum MessagesStatus { initial, loading, success, error }
|
|
|
|
/// Énumération des statuts d'envoi de message.
|
|
enum SendMessageStatus { initial, sending, success, error }
|
|
|
|
/// État du ChatBloc.
|
|
class ChatState extends Equatable {
|
|
const ChatState({
|
|
this.conversationsStatus = ConversationsStatus.initial,
|
|
this.messagesStatus = MessagesStatus.initial,
|
|
this.sendMessageStatus = SendMessageStatus.initial,
|
|
this.conversations = const [],
|
|
this.messages = const [],
|
|
this.currentConversationId,
|
|
this.unreadCount = 0,
|
|
this.errorMessage,
|
|
});
|
|
|
|
final ConversationsStatus conversationsStatus;
|
|
final MessagesStatus messagesStatus;
|
|
final SendMessageStatus sendMessageStatus;
|
|
final List<Conversation> conversations;
|
|
final List<ChatMessage> messages;
|
|
final String? currentConversationId;
|
|
final int unreadCount;
|
|
final String? errorMessage;
|
|
|
|
ChatState copyWith({
|
|
ConversationsStatus? conversationsStatus,
|
|
MessagesStatus? messagesStatus,
|
|
SendMessageStatus? sendMessageStatus,
|
|
List<Conversation>? conversations,
|
|
List<ChatMessage>? messages,
|
|
String? currentConversationId,
|
|
int? unreadCount,
|
|
String? errorMessage,
|
|
}) {
|
|
return ChatState(
|
|
conversationsStatus: conversationsStatus ?? this.conversationsStatus,
|
|
messagesStatus: messagesStatus ?? this.messagesStatus,
|
|
sendMessageStatus: sendMessageStatus ?? this.sendMessageStatus,
|
|
conversations: conversations ?? this.conversations,
|
|
messages: messages ?? this.messages,
|
|
currentConversationId: currentConversationId ?? this.currentConversationId,
|
|
unreadCount: unreadCount ?? this.unreadCount,
|
|
errorMessage: errorMessage,
|
|
);
|
|
}
|
|
|
|
@override
|
|
List<Object?> get props => [
|
|
conversationsStatus,
|
|
messagesStatus,
|
|
sendMessageStatus,
|
|
conversations,
|
|
messages,
|
|
currentConversationId,
|
|
unreadCount,
|
|
errorMessage,
|
|
];
|
|
}
|