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.)
242 lines
8.0 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|