Files
afterwork/lib/presentation/widgets/message_bubble.dart
dahoud 92612abbd7 fix(chat): Correction race condition + Implémentation TODOs
## Corrections Critiques

### Race Condition - Statuts de Messages
- Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas
- Cause : WebSocket delivery confirmations arrivaient avant messages locaux
- Solution : Pattern Optimistic UI dans chat_bloc.dart
  - Création message temporaire immédiate
  - Ajout à la liste AVANT requête HTTP
  - Remplacement par message serveur à la réponse
- Fichier : lib/presentation/state_management/chat_bloc.dart

## Implémentation TODOs (13/21)

### Social (social_header_widget.dart)
-  Copier lien du post dans presse-papiers
-  Partage natif via Share.share()
-  Dialogue de signalement avec 5 raisons

### Partage (share_post_dialog.dart)
-  Interface sélection d'amis avec checkboxes
-  Partage externe via Share API

### Média (media_upload_service.dart)
-  Parsing JSON réponse backend
-  Méthode deleteMedia() pour suppression
-  Génération miniature vidéo

### Posts (create_post_dialog.dart, edit_post_dialog.dart)
-  Extraction URL depuis uploads
-  Documentation chargement médias

### Chat (conversations_screen.dart)
-  Navigation vers notifications
-  ConversationSearchDelegate pour recherche

## Nouveaux Fichiers

### Configuration
- build-prod.ps1 : Script build production avec dart-define
- lib/core/constants/env_config.dart : Gestion environnements

### Documentation
- TODOS_IMPLEMENTED.md : Documentation complète TODOs

## Améliorations

### Architecture
- Refactoring injection de dépendances
- Amélioration routing et navigation
- Optimisation providers (UserProvider, FriendsProvider)

### UI/UX
- Amélioration thème et couleurs
- Optimisation animations
- Meilleure gestion erreurs

### Services
- Configuration API avec env_config
- Amélioration datasources (events, users)
- Optimisation modèles de données
2026-01-10 10:43:17 +00:00

324 lines
9.9 KiB
Dart

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';
}
}
}