Files
unionflow-mobile-apps/lib/features/communication/presentation/widgets/message_bubble.dart
dahoud 45dcd2171e 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.)
2026-04-15 20:26:35 +00:00

242 lines
8.0 KiB
Dart

/// 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,
),
),
),
);
}
}