import 'package:flutter/material.dart'; import '../../core/constants/design_system.dart'; import '../../core/utils/app_logger.dart'; import '../../core/utils/date_formatter.dart'; import '../../domain/entities/chat_message.dart'; /// Widget pour afficher une bulle de message dans le chat. /// /// **Fonctionnalités :** /// - Design moderne inspiré de WhatsApp/Telegram 2025 /// - Groupement de messages (via `showAvatar`) /// - Statuts de message : ✓ envoyé, ✓✓ délivré, ✓✓ (bleu) lu /// - Support des attachments (image, vidéo, audio, fichier) /// - Timestamp intelligent avec formatage contextuel class MessageBubble extends StatelessWidget { const MessageBubble({ required this.message, required this.isCurrentUser, this.showAvatar = true, this.isFirstInGroup = false, this.isLastInGroup = false, super.key, }); final ChatMessage message; final bool isCurrentUser; /// Afficher l'avatar (seulement sur le dernier message d'un groupe) final bool showAvatar; /// Premier message d'un groupe (même expéditeur) final bool isFirstInGroup; /// Dernier message d'un groupe (même expéditeur) final bool isLastInGroup; @override Widget build(BuildContext context) { final theme = Theme.of(context); // Padding selon le groupement final bottomPadding = isLastInGroup ? DesignSystem.spacingMd : 2.0; final leftPadding = isCurrentUser ? 64.0 : (showAvatar ? 8.0 : 48.0); final rightPadding = isCurrentUser ? 8.0 : 64.0; return Padding( padding: EdgeInsets.only( left: leftPadding, right: rightPadding, bottom: bottomPadding, ), child: Row( mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [ // Avatar pour les messages reçus (seulement sur le dernier message du groupe) if (!isCurrentUser && showAvatar) ...[ _buildAvatar(), const SizedBox(width: DesignSystem.spacingSm), ], // Espace invisible pour alignement quand pas d'avatar if (!isCurrentUser && !showAvatar) const SizedBox(width: 40), // Bulle de message Flexible( child: _buildMessageContainer(theme), ), ], ), ); } /// Construit l'avatar de l'expéditeur. Widget _buildAvatar() { final initial = message.senderFirstName.isNotEmpty ? message.senderFirstName[0].toUpperCase() : '?'; return CircleAvatar( radius: 18, backgroundImage: message.senderProfileImageUrl != null && message.senderProfileImageUrl!.isNotEmpty && message.senderProfileImageUrl!.startsWith('http') ? NetworkImage(message.senderProfileImageUrl!) : null, child: message.senderProfileImageUrl == null || message.senderProfileImageUrl!.isEmpty || !message.senderProfileImageUrl!.startsWith('http') ? Text(initial, style: const TextStyle(fontSize: 16)) : null, ); } /// Construit le conteneur de la bulle de message. Widget _buildMessageContainer(ThemeData theme) { final bgColor = isCurrentUser ? theme.colorScheme.primary : theme.brightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[200]!; final textColor = isCurrentUser ? Colors.white : theme.brightness == Brightness.dark ? Colors.white : Colors.black87; return Container( decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.only( topLeft: const Radius.circular(18), topRight: const Radius.circular(18), bottomLeft: isCurrentUser ? const Radius.circular(18) : const Radius.circular(4), bottomRight: isCurrentUser ? const Radius.circular(4) : const Radius.circular(18), ), ), padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Nom de l'expéditeur (seulement pour messages reçus ET premier du groupe) if (!isCurrentUser && isFirstInGroup && message.senderFullName.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 4), child: Text( message.senderFullName, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: theme.colorScheme.primary, ), ), ), // Pièce jointe ou contenu if (message.hasAttachment) _buildAttachment(textColor, theme) else Text( message.content, style: TextStyle(fontSize: 15, color: textColor), ), const SizedBox(height: 4), // Timestamp et statuts Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, children: [ Text( ChatDateFormatter.formatMessageTimestamp(message.timestamp), style: TextStyle( fontSize: 11, color: isCurrentUser ? Colors.white.withOpacity(0.7) : textColor.withOpacity(0.6), ), ), if (isCurrentUser) ...[ const SizedBox(width: 4), _buildMessageStatus(), ], ], ), ], ), ); } /// Construit l'indicateur de statut du message (envoyé/délivré/lu). Widget _buildMessageStatus() { // DEBUG: Afficher les statuts dans la console if (isCurrentUser) { AppLogger.d('Message ${message.id.substring(0, 8)}: isRead=${message.isRead}, isDelivered=${message.isDelivered}', tag: 'MessageBubble'); } if (message.isRead) { // Lu : Double check bleu (très visible) return const Icon( Icons.done_all, size: 18, color: Color(0xFF0096FF), // Bleu vif pour "lu" ); } else if (message.isDelivered) { // Délivré : Double check blanc/gris clair (bien visible) return Icon( Icons.done_all, size: 18, color: Colors.white.withOpacity(0.95), ); } else { // Envoyé : Simple check blanc/gris clair return Icon( Icons.done, size: 18, color: Colors.white.withOpacity(0.95), ); } } /// Construit l'affichage d'une pièce jointe. Widget _buildAttachment(Color textColor, ThemeData theme) { if (message.attachmentType == AttachmentType.image) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( message.attachmentUrl!, width: 250, height: 250, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( width: 250, height: 250, color: Colors.grey[300], child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.broken_image, size: 48, color: Colors.grey[600], ), const SizedBox(height: 8), Text( 'Image non disponible', style: TextStyle(color: Colors.grey[600], fontSize: 12), ), ], ), ); }, ), ), if (message.content.isNotEmpty) ...[ const SizedBox(height: 8), Text( message.content, style: TextStyle(fontSize: 15, color: textColor), ), ], ], ); } // Autres types d'attachments (vidéo, audio, fichier) return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(_getAttachmentIcon(), color: textColor, size: 28), const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _getAttachmentTypeName(), style: TextStyle( color: textColor, fontWeight: FontWeight.w600, fontSize: 14, ), ), if (message.content.isNotEmpty) Text( message.content, style: TextStyle( color: textColor.withOpacity(0.8), fontSize: 13, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ], ); } /// Retourne l'icône correspondant au type d'attachment. IconData _getAttachmentIcon() { switch (message.attachmentType) { case AttachmentType.video: return Icons.videocam_rounded; case AttachmentType.audio: return Icons.audiotrack_rounded; case AttachmentType.file: return Icons.insert_drive_file_rounded; default: return Icons.attach_file_rounded; } } /// Retourne le nom d'affichage du type d'attachment. String _getAttachmentTypeName() { switch (message.attachmentType) { case AttachmentType.video: return 'Vidéo'; case AttachmentType.audio: return 'Audio'; case AttachmentType.file: return 'Fichier'; default: return 'Pièce jointe'; } } }