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:
dahoud
2026-04-15 20:26:35 +00:00
parent 07b8488714
commit 45dcd2171e
23 changed files with 2096 additions and 1588 deletions

View File

@@ -1,127 +1,120 @@
/// Entité métier Conversation
/// Entités métier Conversation v4
///
/// Représente une conversation (fil de messages) dans UnionFlow
/// Correspond aux DTOs backend : ConversationSummaryResponse et ConversationResponse
library conversation;
import 'package:equatable/equatable.dart';
import 'message.dart';
/// Type de conversation
enum ConversationType {
/// Conversation individuelle (1-1)
individual,
// ── Résumé de conversation (liste) ───────────────────────────────────────────
/// Conversation de groupe
group,
/// Canal broadcast (lecture seule pour la plupart)
broadcast,
/// Canal d'annonces organisation
announcement,
}
/// Entité Conversation
class Conversation extends Equatable {
/// Résumé d'une conversation pour l'affichage en liste
class ConversationSummary extends Equatable {
final String id;
final String name;
final String? description;
final ConversationType type;
final List<String> participantIds;
final String? organizationId;
final Message? lastMessage;
final int unreadCount;
final bool isMuted;
final bool isPinned;
final bool isArchived;
final DateTime createdAt;
final DateTime? updatedAt;
final String? avatarUrl;
final Map<String, dynamic>? metadata;
final String typeConversation; // DIRECTE | ROLE_CANAL | GROUPE
final String titre;
final String statut; // ACTIVE | ARCHIVEE
final String? dernierMessageApercu;
final String? dernierMessageType;
final DateTime? dernierMessageAt;
final int nonLus;
final String? organisationId;
const Conversation({
const ConversationSummary({
required this.id,
required this.name,
this.description,
required this.type,
required this.participantIds,
this.organizationId,
this.lastMessage,
this.unreadCount = 0,
this.isMuted = false,
this.isPinned = false,
this.isArchived = false,
required this.createdAt,
this.updatedAt,
this.avatarUrl,
this.metadata,
required this.typeConversation,
required this.titre,
required this.statut,
this.dernierMessageApercu,
this.dernierMessageType,
this.dernierMessageAt,
this.nonLus = 0,
this.organisationId,
});
/// Vérifie si la conversation a des messages non lus
bool get hasUnread => unreadCount > 0;
/// Vérifie si c'est une conversation individuelle
bool get isIndividual => type == ConversationType.individual;
/// Vérifie si c'est un broadcast
bool get isBroadcast => type == ConversationType.broadcast;
/// Nombre de participants
int get participantCount => participantIds.length;
/// Copie avec modifications
Conversation copyWith({
String? id,
String? name,
String? description,
ConversationType? type,
List<String>? participantIds,
String? organizationId,
Message? lastMessage,
int? unreadCount,
bool? isMuted,
bool? isPinned,
bool? isArchived,
DateTime? createdAt,
DateTime? updatedAt,
String? avatarUrl,
Map<String, dynamic>? metadata,
}) {
return Conversation(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
type: type ?? this.type,
participantIds: participantIds ?? this.participantIds,
organizationId: organizationId ?? this.organizationId,
lastMessage: lastMessage ?? this.lastMessage,
unreadCount: unreadCount ?? this.unreadCount,
isMuted: isMuted ?? this.isMuted,
isPinned: isPinned ?? this.isPinned,
isArchived: isArchived ?? this.isArchived,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
avatarUrl: avatarUrl ?? this.avatarUrl,
metadata: metadata ?? this.metadata,
);
}
bool get hasUnread => nonLus > 0;
bool get isDirecte => typeConversation == 'DIRECTE';
bool get isRoleCanal => typeConversation == 'ROLE_CANAL';
bool get isGroupe => typeConversation == 'GROUPE';
bool get isActive => statut == 'ACTIVE';
@override
List<Object?> get props => [
id,
name,
description,
type,
participantIds,
organizationId,
lastMessage,
unreadCount,
isMuted,
isPinned,
isArchived,
createdAt,
updatedAt,
avatarUrl,
metadata,
id, typeConversation, titre, statut,
dernierMessageApercu, dernierMessageType, dernierMessageAt,
nonLus, organisationId,
];
}
// ── Participant ───────────────────────────────────────────────────────────────
/// Participant dans une conversation
class ConversationParticipant extends Equatable {
final String membreId;
final String? prenom;
final String? nom;
final String? roleDansConversation;
final DateTime? luJusqua;
const ConversationParticipant({
required this.membreId,
this.prenom,
this.nom,
this.roleDansConversation,
this.luJusqua,
});
String get nomComplet {
if (prenom != null && nom != null) return '$prenom $nom';
if (prenom != null) return prenom!;
if (nom != null) return nom!;
return membreId;
}
@override
List<Object?> get props => [membreId, prenom, nom, roleDansConversation, luJusqua];
}
// ── Conversation complète (détail) ───────────────────────────────────────────
/// Conversation complète avec participants et messages
class Conversation extends Equatable {
final String id;
final String typeConversation;
final String titre;
final String statut;
final String? organisationId;
final String? organisationNom;
final DateTime? dateCreation;
final int nombreMessages;
final List<ConversationParticipant> participants;
final List<Message> messages;
final int nonLus;
final String? roleCible;
const Conversation({
required this.id,
required this.typeConversation,
required this.titre,
required this.statut,
this.organisationId,
this.organisationNom,
this.dateCreation,
this.nombreMessages = 0,
this.participants = const [],
this.messages = const [],
this.nonLus = 0,
this.roleCible,
});
bool get isDirecte => typeConversation == 'DIRECTE';
bool get isRoleCanal => typeConversation == 'ROLE_CANAL';
bool get isGroupe => typeConversation == 'GROUPE';
bool get isActive => statut == 'ACTIVE';
@override
List<Object?> get props => [
id, typeConversation, titre, statut, organisationId, organisationNom,
dateCreation, nombreMessages, participants, messages, nonLus, roleCible,
];
}