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 { /// Constructeur avec injection du repository de chat. ChatBloc({required this.chatRepository}) : super(const ChatState()) { on(_onLoadConversations); on(_onLoadMessages); on(_onSendMessage); on(_onMarkMessageAsRead); on(_onMarkMessageAsDelivered); on(_onMarkConversationAsRead); on(_onDeleteMessage); on(_onDeleteConversation); on(_onLoadUnreadCount); on(_onAddMessageToConversation); on(_onUpdateTypingStatus); } final ChatRepository chatRepository; void _log(String message) { AppLogger.d(message, tag: 'ChatBloc'); } /// Charge toutes les conversations d'un utilisateur. Future _onLoadConversations( LoadConversations event, Emitter 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 _onLoadMessages( LoadMessages event, Emitter 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 _onSendMessage( SendMessage event, Emitter 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.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 _onMarkMessageAsRead( MarkMessageAsRead event, Emitter 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 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 _onMarkConversationAsRead( MarkConversationAsRead event, Emitter 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 _onDeleteMessage( DeleteMessage event, Emitter 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 _onDeleteConversation( DeleteConversation event, Emitter 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 _onLoadUnreadCount( LoadUnreadCount event, Emitter 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 emit, ) { _log('Ajout d\'un message reçu via WebSocket'); final updatedMessages = List.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 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 get props => []; } /// Événement pour charger les conversations d'un utilisateur. class LoadConversations extends ChatEvent { const LoadConversations(this.userId); final String userId; @override List 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 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 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 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 get props => [conversationId, userId]; } /// Événement pour supprimer un message. class DeleteMessage extends ChatEvent { const DeleteMessage(this.messageId); final String messageId; @override List get props => [messageId]; } /// Événement pour supprimer une conversation. class DeleteConversation extends ChatEvent { const DeleteConversation(this.conversationId); final String conversationId; @override List 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 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 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 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 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 conversations; final List messages; final String? currentConversationId; final int unreadCount; final String? errorMessage; ChatState copyWith({ ConversationsStatus? conversationsStatus, MessagesStatus? messagesStatus, SendMessageStatus? sendMessageStatus, List? conversations, List? 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 get props => [ conversationsStatus, messagesStatus, sendMessageStatus, conversations, messages, currentConversationId, unreadCount, errorMessage, ]; }