feat(communication): module messagerie unifié + contact policies + blocages
Aligné avec le backend MessagingResource : - Nouveau module communication (conversations, messages, participants) - Respect des ContactPolicy (qui peut parler à qui par rôle) - Gestion MemberBlock (blocages individuels) - UI : conversations list, conversation detail, broadcast, tiles - BLoC : MessagingBloc avec events (envoyer, démarrer conversation rôle, etc.)
This commit is contained in:
@@ -1,105 +1,202 @@
|
||||
/// BLoC de gestion de la messagerie
|
||||
/// BLoC de gestion de la messagerie v4
|
||||
library messaging_bloc;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../domain/usecases/get_conversations.dart';
|
||||
import '../../domain/usecases/get_messages.dart';
|
||||
import '../../domain/usecases/send_message.dart';
|
||||
import '../../domain/usecases/send_broadcast.dart';
|
||||
|
||||
import '../../../../core/utils/logger.dart';
|
||||
import '../../../../core/websocket/websocket_service.dart';
|
||||
import '../../domain/repositories/messaging_repository.dart';
|
||||
import 'messaging_event.dart';
|
||||
import 'messaging_state.dart';
|
||||
|
||||
@injectable
|
||||
class MessagingBloc extends Bloc<MessagingEvent, MessagingState> {
|
||||
final GetConversations getConversations;
|
||||
final GetMessages getMessages;
|
||||
final SendMessage sendMessage;
|
||||
final SendBroadcast sendBroadcast;
|
||||
final MessagingRepository _repository;
|
||||
final WebSocketService _webSocketService;
|
||||
|
||||
StreamSubscription<WebSocketEvent>? _wsSubscription;
|
||||
String? _currentConversationId;
|
||||
|
||||
MessagingBloc({
|
||||
required this.getConversations,
|
||||
required this.getMessages,
|
||||
required this.sendMessage,
|
||||
required this.sendBroadcast,
|
||||
}) : super(MessagingInitial()) {
|
||||
on<LoadConversations>(_onLoadConversations);
|
||||
required MessagingRepository repository,
|
||||
required WebSocketService webSocketService,
|
||||
}) : _repository = repository,
|
||||
_webSocketService = webSocketService,
|
||||
super(MessagingInitial()) {
|
||||
on<LoadMesConversations>(_onLoadMesConversations);
|
||||
on<OpenConversation>(_onOpenConversation);
|
||||
on<DemarrerConversationDirecte>(_onDemarrerConversationDirecte);
|
||||
on<DemarrerConversationRole>(_onDemarrerConversationRole);
|
||||
on<ArchiverConversation>(_onArchiverConversation);
|
||||
on<EnvoyerMessageTexte>(_onEnvoyerMessageTexte);
|
||||
on<LoadMessages>(_onLoadMessages);
|
||||
on<SendMessageEvent>(_onSendMessage);
|
||||
on<SendBroadcastEvent>(_onSendBroadcast);
|
||||
on<MarquerLu>(_onMarquerLu);
|
||||
on<SupprimerMessage>(_onSupprimerMessage);
|
||||
on<NouveauMessageWebSocket>(_onNouveauMessageWebSocket);
|
||||
|
||||
_listenWebSocket();
|
||||
}
|
||||
|
||||
Future<void> _onLoadConversations(
|
||||
LoadConversations event,
|
||||
void _listenWebSocket() {
|
||||
_wsSubscription = _webSocketService.eventStream.listen((event) {
|
||||
if (event is ChatMessageEvent && event.conversationId != null) {
|
||||
AppLogger.info('MessagingBloc: NOUVEAU_MESSAGE WS pour conv ${event.conversationId}');
|
||||
add(NouveauMessageWebSocket(event.conversationId!));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onLoadMesConversations(
|
||||
LoadMesConversations event,
|
||||
Emitter<MessagingState> emit,
|
||||
) async {
|
||||
emit(MessagingLoading());
|
||||
try {
|
||||
final conversations = await _repository.getMesConversations();
|
||||
emit(MesConversationsLoaded(conversations));
|
||||
} catch (e) {
|
||||
emit(MessagingError(e.toString().replaceFirst('Exception: ', '')));
|
||||
}
|
||||
}
|
||||
|
||||
final result = await getConversations(
|
||||
organizationId: event.organizationId,
|
||||
includeArchived: event.includeArchived,
|
||||
);
|
||||
Future<void> _onOpenConversation(
|
||||
OpenConversation event,
|
||||
Emitter<MessagingState> emit,
|
||||
) async {
|
||||
emit(MessagingLoading());
|
||||
try {
|
||||
_currentConversationId = event.conversationId;
|
||||
final conversation = await _repository.getConversation(event.conversationId);
|
||||
emit(ConversationOuverte(conversation));
|
||||
// Marquer comme lu en arrière-plan (non-bloquant)
|
||||
_repository.marquerLu(event.conversationId);
|
||||
} catch (e) {
|
||||
emit(MessagingError(e.toString().replaceFirst('Exception: ', '')));
|
||||
}
|
||||
}
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(MessagingError(failure.message)),
|
||||
(conversations) => emit(ConversationsLoaded(conversations: conversations)),
|
||||
);
|
||||
Future<void> _onDemarrerConversationDirecte(
|
||||
DemarrerConversationDirecte event,
|
||||
Emitter<MessagingState> emit,
|
||||
) async {
|
||||
emit(MessagingLoading());
|
||||
try {
|
||||
final conversation = await _repository.demarrerConversationDirecte(
|
||||
destinataireId: event.destinataireId,
|
||||
organisationId: event.organisationId,
|
||||
premierMessage: event.premierMessage,
|
||||
);
|
||||
emit(ConversationCreee(conversation));
|
||||
} catch (e) {
|
||||
emit(MessagingError(e.toString().replaceFirst('Exception: ', '')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDemarrerConversationRole(
|
||||
DemarrerConversationRole event,
|
||||
Emitter<MessagingState> emit,
|
||||
) async {
|
||||
emit(MessagingLoading());
|
||||
try {
|
||||
final conversation = await _repository.demarrerConversationRole(
|
||||
roleCible: event.roleCible,
|
||||
organisationId: event.organisationId,
|
||||
premierMessage: event.premierMessage,
|
||||
);
|
||||
emit(ConversationCreee(conversation));
|
||||
} catch (e) {
|
||||
emit(MessagingError(e.toString().replaceFirst('Exception: ', '')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onArchiverConversation(
|
||||
ArchiverConversation event,
|
||||
Emitter<MessagingState> emit,
|
||||
) async {
|
||||
try {
|
||||
await _repository.archiverConversation(event.conversationId);
|
||||
emit(const MessagingActionOk('archiver'));
|
||||
add(const LoadMesConversations());
|
||||
} catch (e) {
|
||||
emit(MessagingError(e.toString().replaceFirst('Exception: ', '')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEnvoyerMessageTexte(
|
||||
EnvoyerMessageTexte event,
|
||||
Emitter<MessagingState> emit,
|
||||
) async {
|
||||
try {
|
||||
final message = await _repository.envoyerMessage(
|
||||
event.conversationId,
|
||||
typeMessage: 'TEXTE',
|
||||
contenu: event.contenu,
|
||||
messageParentId: event.messageParentId,
|
||||
);
|
||||
emit(MessageEnvoye(message: message, conversationId: event.conversationId));
|
||||
// Recharger les messages après envoi
|
||||
add(LoadMessages(conversationId: event.conversationId));
|
||||
} catch (e) {
|
||||
emit(MessagingError(e.toString().replaceFirst('Exception: ', '')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadMessages(
|
||||
LoadMessages event,
|
||||
Emitter<MessagingState> emit,
|
||||
) async {
|
||||
emit(MessagingLoading());
|
||||
|
||||
final result = await getMessages(
|
||||
conversationId: event.conversationId,
|
||||
limit: event.limit,
|
||||
beforeMessageId: event.beforeMessageId,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(MessagingError(failure.message)),
|
||||
(messages) => emit(MessagesLoaded(
|
||||
try {
|
||||
final messages = await _repository.getMessages(event.conversationId, page: event.page);
|
||||
emit(MessagesLoaded(
|
||||
conversationId: event.conversationId,
|
||||
messages: messages,
|
||||
hasMore: messages.length == (event.limit ?? 50),
|
||||
)),
|
||||
);
|
||||
hasMore: messages.length >= 20,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(MessagingError(e.toString().replaceFirst('Exception: ', '')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSendMessage(
|
||||
SendMessageEvent event,
|
||||
Future<void> _onMarquerLu(
|
||||
MarquerLu event,
|
||||
Emitter<MessagingState> emit,
|
||||
) async {
|
||||
final result = await sendMessage(
|
||||
conversationId: event.conversationId,
|
||||
content: event.content,
|
||||
attachments: event.attachments,
|
||||
priority: event.priority,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(MessagingError(failure.message)),
|
||||
(message) => emit(MessageSent(message)),
|
||||
);
|
||||
await _repository.marquerLu(event.conversationId);
|
||||
}
|
||||
|
||||
Future<void> _onSendBroadcast(
|
||||
SendBroadcastEvent event,
|
||||
Future<void> _onSupprimerMessage(
|
||||
SupprimerMessage event,
|
||||
Emitter<MessagingState> emit,
|
||||
) async {
|
||||
final result = await sendBroadcast(
|
||||
organizationId: event.organizationId,
|
||||
subject: event.subject,
|
||||
content: event.content,
|
||||
priority: event.priority,
|
||||
attachments: event.attachments,
|
||||
);
|
||||
try {
|
||||
await _repository.supprimerMessage(event.conversationId, event.messageId);
|
||||
emit(const MessagingActionOk('supprimer-message'));
|
||||
add(LoadMessages(conversationId: event.conversationId));
|
||||
} catch (e) {
|
||||
emit(MessagingError(e.toString().replaceFirst('Exception: ', '')));
|
||||
}
|
||||
}
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(MessagingError(failure.message)),
|
||||
(message) => emit(BroadcastSent(message)),
|
||||
);
|
||||
Future<void> _onNouveauMessageWebSocket(
|
||||
NouveauMessageWebSocket event,
|
||||
Emitter<MessagingState> emit,
|
||||
) async {
|
||||
// Si la conversation est actuellement ouverte, recharger les messages
|
||||
if (_currentConversationId == event.conversationId) {
|
||||
add(LoadMessages(conversationId: event.conversationId));
|
||||
} else {
|
||||
// Sinon, rafraîchir la liste pour mettre à jour le badge non-lus
|
||||
add(const LoadMesConversations());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_wsSubscription?.cancel();
|
||||
_currentConversationId = null;
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/// Événements du BLoC Messaging
|
||||
/// Événements du BLoC Messagerie v4
|
||||
library messaging_event;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../domain/entities/message.dart';
|
||||
|
||||
abstract class MessagingEvent extends Equatable {
|
||||
const MessagingEvent();
|
||||
@@ -11,108 +10,126 @@ abstract class MessagingEvent extends Equatable {
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Charger les conversations
|
||||
class LoadConversations extends MessagingEvent {
|
||||
final String? organizationId;
|
||||
final bool includeArchived;
|
||||
// ── Conversations ─────────────────────────────────────────────────────────────
|
||||
|
||||
const LoadConversations({
|
||||
this.organizationId,
|
||||
this.includeArchived = false,
|
||||
/// Charger la liste des conversations
|
||||
class LoadMesConversations extends MessagingEvent {
|
||||
const LoadMesConversations();
|
||||
}
|
||||
|
||||
/// Ouvrir une conversation (charge le détail)
|
||||
class OpenConversation extends MessagingEvent {
|
||||
final String conversationId;
|
||||
|
||||
const OpenConversation(this.conversationId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [conversationId];
|
||||
}
|
||||
|
||||
/// Démarrer une conversation directe avec un membre
|
||||
class DemarrerConversationDirecte extends MessagingEvent {
|
||||
final String destinataireId;
|
||||
final String organisationId;
|
||||
final String? premierMessage;
|
||||
|
||||
const DemarrerConversationDirecte({
|
||||
required this.destinataireId,
|
||||
required this.organisationId,
|
||||
this.premierMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [organizationId, includeArchived];
|
||||
List<Object?> get props => [destinataireId, organisationId, premierMessage];
|
||||
}
|
||||
|
||||
/// Charger les messages d'une conversation
|
||||
/// Démarrer un canal de communication avec un rôle
|
||||
class DemarrerConversationRole extends MessagingEvent {
|
||||
final String roleCible;
|
||||
final String organisationId;
|
||||
final String? premierMessage;
|
||||
|
||||
const DemarrerConversationRole({
|
||||
required this.roleCible,
|
||||
required this.organisationId,
|
||||
this.premierMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [roleCible, organisationId, premierMessage];
|
||||
}
|
||||
|
||||
/// Archiver une conversation
|
||||
class ArchiverConversation extends MessagingEvent {
|
||||
final String conversationId;
|
||||
|
||||
const ArchiverConversation(this.conversationId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [conversationId];
|
||||
}
|
||||
|
||||
// ── Messages ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Envoyer un message texte
|
||||
class EnvoyerMessageTexte extends MessagingEvent {
|
||||
final String conversationId;
|
||||
final String contenu;
|
||||
final String? messageParentId;
|
||||
|
||||
const EnvoyerMessageTexte({
|
||||
required this.conversationId,
|
||||
required this.contenu,
|
||||
this.messageParentId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [conversationId, contenu, messageParentId];
|
||||
}
|
||||
|
||||
/// Charger l'historique des messages
|
||||
class LoadMessages extends MessagingEvent {
|
||||
final String conversationId;
|
||||
final int? limit;
|
||||
final String? beforeMessageId;
|
||||
final int page;
|
||||
|
||||
const LoadMessages({
|
||||
required this.conversationId,
|
||||
this.limit,
|
||||
this.beforeMessageId,
|
||||
});
|
||||
const LoadMessages({required this.conversationId, this.page = 0});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [conversationId, limit, beforeMessageId];
|
||||
List<Object?> get props => [conversationId, page];
|
||||
}
|
||||
|
||||
/// Envoyer un message
|
||||
class SendMessageEvent extends MessagingEvent {
|
||||
/// Marquer une conversation comme lue
|
||||
class MarquerLu extends MessagingEvent {
|
||||
final String conversationId;
|
||||
final String content;
|
||||
final List<String>? attachments;
|
||||
final MessagePriority priority;
|
||||
|
||||
const SendMessageEvent({
|
||||
required this.conversationId,
|
||||
required this.content,
|
||||
this.attachments,
|
||||
this.priority = MessagePriority.normal,
|
||||
});
|
||||
const MarquerLu(this.conversationId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [conversationId, content, attachments, priority];
|
||||
List<Object?> get props => [conversationId];
|
||||
}
|
||||
|
||||
/// Envoyer un broadcast
|
||||
class SendBroadcastEvent extends MessagingEvent {
|
||||
final String organizationId;
|
||||
final String subject;
|
||||
final String content;
|
||||
final MessagePriority priority;
|
||||
final List<String>? attachments;
|
||||
|
||||
const SendBroadcastEvent({
|
||||
required this.organizationId,
|
||||
required this.subject,
|
||||
required this.content,
|
||||
this.priority = MessagePriority.normal,
|
||||
this.attachments,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [organizationId, subject, content, priority, attachments];
|
||||
}
|
||||
|
||||
/// Marquer un message comme lu
|
||||
class MarkMessageAsReadEvent extends MessagingEvent {
|
||||
/// Supprimer un message
|
||||
class SupprimerMessage extends MessagingEvent {
|
||||
final String conversationId;
|
||||
final String messageId;
|
||||
|
||||
const MarkMessageAsReadEvent(this.messageId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [messageId];
|
||||
}
|
||||
|
||||
/// Charger le nombre de messages non lus
|
||||
class LoadUnreadCount extends MessagingEvent {
|
||||
final String? organizationId;
|
||||
|
||||
const LoadUnreadCount({this.organizationId});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [organizationId];
|
||||
}
|
||||
|
||||
/// Créer une nouvelle conversation
|
||||
class CreateConversationEvent extends MessagingEvent {
|
||||
final String name;
|
||||
final List<String> participantIds;
|
||||
final String? organizationId;
|
||||
final String? description;
|
||||
|
||||
const CreateConversationEvent({
|
||||
required this.name,
|
||||
required this.participantIds,
|
||||
this.organizationId,
|
||||
this.description,
|
||||
const SupprimerMessage({
|
||||
required this.conversationId,
|
||||
required this.messageId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, participantIds, organizationId, description];
|
||||
List<Object?> get props => [conversationId, messageId];
|
||||
}
|
||||
|
||||
// ── Temps réel WebSocket ───────────────────────────────────────────────────────
|
||||
|
||||
/// Nouveau message reçu via WebSocket
|
||||
class NouveauMessageWebSocket extends MessagingEvent {
|
||||
final String conversationId;
|
||||
|
||||
const NouveauMessageWebSocket(this.conversationId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [conversationId];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/// États du BLoC Messaging
|
||||
/// États du BLoC Messagerie v4
|
||||
library messaging_state;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../../domain/entities/conversation.dart';
|
||||
import '../../domain/entities/message.dart';
|
||||
|
||||
@@ -18,18 +19,24 @@ class MessagingInitial extends MessagingState {}
|
||||
/// Chargement en cours
|
||||
class MessagingLoading extends MessagingState {}
|
||||
|
||||
/// Conversations chargées
|
||||
class ConversationsLoaded extends MessagingState {
|
||||
final List<Conversation> conversations;
|
||||
final int unreadCount;
|
||||
/// Liste des conversations chargée
|
||||
class MesConversationsLoaded extends MessagingState {
|
||||
final List<ConversationSummary> conversations;
|
||||
|
||||
const ConversationsLoaded({
|
||||
required this.conversations,
|
||||
this.unreadCount = 0,
|
||||
});
|
||||
const MesConversationsLoaded(this.conversations);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [conversations, unreadCount];
|
||||
List<Object?> get props => [conversations];
|
||||
}
|
||||
|
||||
/// Conversation ouverte (détail avec messages)
|
||||
class ConversationOuverte extends MessagingState {
|
||||
final Conversation conversation;
|
||||
|
||||
const ConversationOuverte(this.conversation);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [conversation];
|
||||
}
|
||||
|
||||
/// Messages d'une conversation chargés
|
||||
@@ -48,44 +55,35 @@ class MessagesLoaded extends MessagingState {
|
||||
List<Object?> get props => [conversationId, messages, hasMore];
|
||||
}
|
||||
|
||||
/// Message envoyé avec succès
|
||||
class MessageSent extends MessagingState {
|
||||
/// Message envoyé avec succès — la conversation s'actualise
|
||||
class MessageEnvoye extends MessagingState {
|
||||
final Message message;
|
||||
final String conversationId;
|
||||
|
||||
const MessageSent(this.message);
|
||||
const MessageEnvoye({required this.message, required this.conversationId});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => [message, conversationId];
|
||||
}
|
||||
|
||||
/// Broadcast envoyé avec succès
|
||||
class BroadcastSent extends MessagingState {
|
||||
final Message message;
|
||||
|
||||
const BroadcastSent(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
/// Conversation créée
|
||||
class ConversationCreated extends MessagingState {
|
||||
/// Conversation créée (après démarrage direct/rôle)
|
||||
class ConversationCreee extends MessagingState {
|
||||
final Conversation conversation;
|
||||
|
||||
const ConversationCreated(this.conversation);
|
||||
const ConversationCreee(this.conversation);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [conversation];
|
||||
}
|
||||
|
||||
/// Compteur de non lus chargé
|
||||
class UnreadCountLoaded extends MessagingState {
|
||||
final int count;
|
||||
/// Action silencieuse réussie (marquer lu, supprimer message, etc.)
|
||||
class MessagingActionOk extends MessagingState {
|
||||
final String action;
|
||||
|
||||
const UnreadCountLoaded(this.count);
|
||||
const MessagingActionOk(this.action);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [count];
|
||||
List<Object?> get props => [action];
|
||||
}
|
||||
|
||||
/// Erreur
|
||||
|
||||
Reference in New Issue
Block a user