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:
@@ -0,0 +1,241 @@
|
||||
/// Widget bulle de message v4 — Communication UnionFlow
|
||||
library message_bubble;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../domain/entities/message.dart';
|
||||
|
||||
/// Bulle de message différenciée envoyé/reçu
|
||||
class MessageBubble extends StatelessWidget {
|
||||
final Message message;
|
||||
final bool isMine;
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
const MessageBubble({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.isMine,
|
||||
this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
// Message supprimé
|
||||
if (message.supprime) {
|
||||
return _buildDeleted(scheme);
|
||||
}
|
||||
|
||||
return Align(
|
||||
alignment: isMine ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: GestureDetector(
|
||||
onLongPress: onLongPress,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.75,
|
||||
),
|
||||
margin: EdgeInsets.only(
|
||||
left: isMine ? 48 : 0,
|
||||
right: isMine ? 0 : 48,
|
||||
bottom: SpacingTokens.xs,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
isMine ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Nom expéditeur (messages reçus)
|
||||
if (!isMine && message.expediteurNomComplet.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: SpacingTokens.sm, bottom: 2),
|
||||
child: Text(
|
||||
message.expediteurNomComplet,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: ModuleColors.communication,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Réponse à un message parent
|
||||
if (message.hasParent && message.messageParentApercu != null)
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
left: isMine ? 0 : 0,
|
||||
bottom: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.sm,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerHighest.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: ModuleColors.communication,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
message.messageParentApercu!,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Bulle principale
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.md,
|
||||
vertical: SpacingTokens.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isMine
|
||||
? ModuleColors.communication
|
||||
: scheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(SpacingTokens.radiusMd),
|
||||
topRight: const Radius.circular(SpacingTokens.radiusMd),
|
||||
bottomLeft: Radius.circular(
|
||||
isMine ? SpacingTokens.radiusMd : SpacingTokens.radiusXs,
|
||||
),
|
||||
bottomRight: Radius.circular(
|
||||
isMine ? SpacingTokens.radiusXs : SpacingTokens.radiusMd,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Contenu selon type
|
||||
_buildContent(scheme),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Horodatage
|
||||
if (message.dateEnvoi != null)
|
||||
Text(
|
||||
DateFormat('HH:mm').format(message.dateEnvoi!),
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: isMine
|
||||
? Colors.white.withOpacity(0.7)
|
||||
: scheme.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(ColorScheme scheme) {
|
||||
final textColor = isMine ? ModuleColors.communicationOnColor : scheme.onSurface;
|
||||
|
||||
if (message.isVocal) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.mic, size: 18, color: textColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Note vocale${message.dureeAudio != null ? ' · ${message.dureeAudio}s' : ''}',
|
||||
style: AppTypography.bodyTextSmall.copyWith(color: textColor),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (message.isImage) {
|
||||
if (message.urlFichier != null) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
child: Image.network(
|
||||
message.urlFichier!,
|
||||
width: 200,
|
||||
height: 150,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.broken_image_outlined, size: 18, color: textColor),
|
||||
const SizedBox(width: 4),
|
||||
Text('Image', style: AppTypography.bodyTextSmall.copyWith(color: textColor)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.image_outlined, size: 18, color: textColor),
|
||||
const SizedBox(width: 4),
|
||||
Text('Image', style: AppTypography.bodyTextSmall.copyWith(color: textColor)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (message.isSysteme) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 16, color: textColor.withOpacity(0.7)),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
message.contenu ?? 'Notification système',
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: textColor.withOpacity(0.7),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// TEXTE (défaut)
|
||||
return Text(
|
||||
message.contenu ?? '',
|
||||
style: AppTypography.bodyTextSmall.copyWith(color: textColor),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeleted(ColorScheme scheme) {
|
||||
return Align(
|
||||
alignment: isMine ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: SpacingTokens.xs),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.md,
|
||||
vertical: SpacingTokens.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerHighest.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
border: Border.all(color: scheme.outlineVariant.withOpacity(0.3)),
|
||||
),
|
||||
child: Text(
|
||||
'🚫 Message supprimé',
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user