import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_state.dart' as user_state; import '../../blocs/message/message_bloc.dart'; import '../../blocs/message/message_event.dart'; import '../../blocs/message/message_state.dart'; import '../../models/group.dart'; import '../../models/message.dart'; /// Chat group content widget for group messaging functionality. /// /// This widget provides a complete chat interface for group members to /// communicate within a travel group. Features include: /// - Real-time message loading and sending /// - Message editing and deletion /// - Message reactions (like/unlike) /// - Scroll-to-bottom functionality /// - Message status indicators /// /// The widget integrates with MessageBloc for state management and /// handles various message operations through the bloc pattern. class ChatGroupContent extends StatefulWidget { /// The group for which to display the chat interface final Group group; /// Creates a chat group content widget. /// /// Args: /// [group]: The group object containing group details and ID const ChatGroupContent({ super.key, required this.group, }); @override State createState() => _ChatGroupContentState(); } class _ChatGroupContentState extends State { /// Controller for the message input field final _messageController = TextEditingController(); /// Controller for managing scroll position in the message list final _scrollController = ScrollController(); /// Currently selected message for editing (null if not editing) Message? _editingMessage; @override void initState() { super.initState(); // Load messages when the widget initializes context.read().add(LoadMessages(widget.group.id)); } @override void dispose() { _messageController.dispose(); _scrollController.dispose(); super.dispose(); } /// Sends a new message or updates an existing message. /// /// Handles both sending new messages and editing existing ones based /// on the current editing state. Validates input and clears the input /// field after successful submission. /// /// Args: /// [currentUser]: The user sending or editing the message void _sendMessage(user_state.UserModel currentUser) { final messageText = _messageController.text.trim(); if (messageText.isEmpty) return; if (_editingMessage != null) { // Edit mode - update existing message context.read().add( UpdateMessage( groupId: widget.group.id, messageId: _editingMessage!.id, newText: messageText, ), ); _cancelEdit(); } else { // Send mode - create new message context.read().add( SendMessage( groupId: widget.group.id, text: messageText, senderId: currentUser.id, senderName: currentUser.prenom, ), ); } _messageController.clear(); } /// Initiates editing mode for a selected message. /// /// Sets the message as the currently editing message and populates /// the input field with the message text for modification. /// /// Args: /// [message]: The message to edit void _editMessage(Message message) { setState(() { _editingMessage = message; _messageController.text = message.text; }); } /// Cancels the current editing operation. /// /// Resets the editing state and clears the input field, /// returning to normal message sending mode. void _cancelEdit() { setState(() { _editingMessage = null; _messageController.clear(); }); } /// Deletes a message from the group chat. /// /// Sends a delete event to the MessageBloc to remove the specified /// message from the group's message history. /// /// Args: /// [messageId]: The ID of the message to delete void _deleteMessage(String messageId) { context.read().add( DeleteMessage( groupId: widget.group.id, messageId: messageId, ), ); } void _reactToMessage(String messageId, String userId, String reaction) { context.read().add( ReactToMessage( groupId: widget.group.id, messageId: messageId, userId: userId, reaction: reaction, ), ); } void _removeReaction(String messageId, String userId) { context.read().add( RemoveReaction( groupId: widget.group.id, messageId: messageId, userId: userId, ), ); } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return BlocBuilder( builder: (context, userState) { if (userState is! user_state.UserLoaded) { return Scaffold( appBar: AppBar(title: Text(widget.group.name)), body: const Center(child: Text('Utilisateur non connecté')), ); } final currentUser = userState.user; return Scaffold( appBar: AppBar( title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(widget.group.name, style: const TextStyle(fontSize: 18)), Text( '${widget.group.members.length} membre${widget.group.members.length > 1 ? 's' : ''}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.normal), ), ], ), actions: [ IconButton( icon: const Icon(Icons.people), onPressed: () => _showMembersDialog(context), ), ], ), body: Column( children: [ // Liste des messages Expanded( child: BlocConsumer( listener: (context, state) { if (state is MessageError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: Colors.red, ), ); } }, builder: (context, state) { if (state is MessageLoading) { return const Center(child: CircularProgressIndicator()); } if (state is MessagesLoaded) { if (state.messages.isEmpty) { return _buildEmptyState(); } // Scroller automatiquement vers le bas au chargement WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.jumpTo( _scrollController.position.maxScrollExtent, ); } }); return ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(16), itemCount: state.messages.length, itemBuilder: (context, index) { final message = state.messages[index]; final isMe = message.senderId == currentUser.id; final showDate = index == 0 || !_isSameDay( state.messages[index - 1].timestamp, message.timestamp, ); return Column( children: [ if (showDate) _buildDateSeparator(message.timestamp), _buildMessageBubble(message, isMe, isDark, currentUser.id), ], ); }, ); } return _buildEmptyState(); }, ), ), // Barre d'édition si un message est en cours de modification if (_editingMessage != null) Container( color: isDark ? Colors.blue[900] : Colors.blue[100], padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ const Icon(Icons.edit, size: 20), const SizedBox(width: 8), const Expanded( child: Text('Modification du message'), ), IconButton( icon: const Icon(Icons.close), onPressed: _cancelEdit, ), ], ), ), // Zone de saisie Container( decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, border: Border( top: BorderSide( color: isDark ? Colors.grey[800]! : Colors.grey[300]!, width: 1, ), ), ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: SafeArea( child: Row( children: [ Expanded( child: TextField( controller: _messageController, decoration: InputDecoration( hintText: _editingMessage != null ? 'Modifier le message...' : 'Écrire un message...', filled: true, fillColor: isDark ? Colors.grey[850] : Colors.grey[100], border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), maxLines: null, textCapitalization: TextCapitalization.sentences, ), ), const SizedBox(width: 8), IconButton( onPressed: () => _sendMessage(currentUser), icon: Icon(_editingMessage != null ? Icons.check : Icons.send), style: IconButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, padding: const EdgeInsets.all(12), ), ), ], ), ), ), ], ), ); }, ); } Widget _buildEmptyState() { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.chat_bubble_outline, size: 80, color: Colors.grey[400], ), const SizedBox(height: 16), const Text( 'Aucun message', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text( 'Commencez la conversation !', textAlign: TextAlign.center, style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), ], ), ), ); } Widget _buildMessageBubble(Message message, bool isMe, bool isDark, String currentUserId) { final Color bubbleColor; final Color textColor; if (isMe) { bubbleColor = isDark ? const Color(0xFF1E3A5F) : const Color(0xFF90CAF9); textColor = isDark ? Colors.white : Colors.black87; } else { bubbleColor = isDark ? Colors.grey[800]! : Colors.grey[200]!; textColor = isDark ? Colors.white : Colors.black87; } return Align( alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, child: GestureDetector( onLongPress: () => _showMessageOptions(context, message, isMe, currentUserId), child: Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.7, ), decoration: BoxDecoration( color: bubbleColor, borderRadius: BorderRadius.only( topLeft: const Radius.circular(16), topRight: const Radius.circular(16), bottomLeft: Radius.circular(isMe ? 16 : 4), bottomRight: Radius.circular(isMe ? 4 : 16), ), ), child: Column( crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ if (!isMe) ...[ Text( message.senderName, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isDark ? Colors.grey[400] : Colors.grey[700], ), ), const SizedBox(height: 4), ], Text( message.text, style: TextStyle(fontSize: 15, color: textColor), ), const SizedBox(height: 4), Row( mainAxisSize: MainAxisSize.min, children: [ Text( _formatTime(message.timestamp), style: TextStyle( fontSize: 11, color: textColor.withValues(alpha: 0.7), ), ), if (message.isEdited) ...[ const SizedBox(width: 4), Text( '(modifié)', style: TextStyle( fontSize: 10, fontStyle: FontStyle.italic, color: textColor.withValues(alpha: 0.6), ), ), ], ], ), // Afficher les réactions if (message.reactions.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 4), child: Wrap( spacing: 4, children: _buildReactionChips(message, currentUserId), ), ), ], ), ), ), ); } List _buildReactionChips(Message message, String currentUserId) { final reactionCounts = >{}; // Grouper les réactions par emoji message.reactions.forEach((userId, emoji) { reactionCounts.putIfAbsent(emoji, () => []).add(userId); }); return reactionCounts.entries.map((entry) { final emoji = entry.key; final userIds = entry.value; final hasReacted = userIds.contains(currentUserId); return GestureDetector( onTap: () { if (hasReacted) { _removeReaction(message.id, currentUserId); } else { _reactToMessage(message.id, currentUserId, emoji); } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: hasReacted ? Colors.blue.withValues(alpha: 0.3) : Colors.grey.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(12), border: hasReacted ? Border.all(color: Colors.blue, width: 1) : null, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(emoji, style: const TextStyle(fontSize: 12)), const SizedBox(width: 2), Text( '${userIds.length}', style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold), ), ], ), ), ); }).toList(); } void _showMessageOptions(BuildContext context, Message message, bool isMe, String currentUserId) { showModalBottomSheet( context: context, builder: (context) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ // Réactions rapides Padding( padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: ['👍', '❤️', '😂', '😮', '😢', '🔥'].map((emoji) { return GestureDetector( onTap: () { Navigator.pop(context); _reactToMessage(message.id, currentUserId, emoji); }, child: Text(emoji, style: const TextStyle(fontSize: 32)), ); }).toList(), ), ), const Divider(height: 1), if (isMe) ...[ ListTile( leading: const Icon(Icons.edit), title: const Text('Modifier'), onTap: () { Navigator.pop(context); _editMessage(message); }, ), ListTile( leading: const Icon(Icons.delete, color: Colors.red), title: const Text('Supprimer', style: TextStyle(color: Colors.red)), onTap: () { Navigator.pop(context); _showDeleteConfirmation(context, message.id); }, ), ], ListTile( leading: const Icon(Icons.close), title: const Text('Annuler'), onTap: () => Navigator.pop(context), ), ], ), ), ); } void _showDeleteConfirmation(BuildContext context, String messageId) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Supprimer le message'), content: const Text('Êtes-vous sûr de vouloir supprimer ce message ?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler'), ), TextButton( onPressed: () { Navigator.pop(context); _deleteMessage(messageId); }, style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Supprimer'), ), ], ), ); } // Helpers pour les dates bool _isSameDay(DateTime date1, DateTime date2) { return date1.year == date2.year && date1.month == date2.month && date1.day == date2.day; } String _formatTime(DateTime date) { final hour = date.hour.toString().padLeft(2, '0'); final minute = date.minute.toString().padLeft(2, '0'); return '$hour:$minute'; } String _formatDate(DateTime date) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final yesterday = today.subtract(const Duration(days: 1)); final messageDate = DateTime(date.year, date.month, date.day); if (messageDate == today) { return 'Aujourd\'hui'; } else if (messageDate == yesterday) { return 'Hier'; } else { return '${date.day}/${date.month}/${date.year}'; } } Widget _buildDateSeparator(DateTime date) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.grey.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(12), ), child: Text( _formatDate(date), style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ), ), ), ); } void _showMembersDialog(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Membres (${widget.group.members.length})'), content: SizedBox( width: double.maxFinite, child: ListView.builder( shrinkWrap: true, itemCount: widget.group.members.length, itemBuilder: (context, index) { final member = widget.group.members[index]; return ListTile( leading: CircleAvatar( child: Text(member.pseudo.substring(0, 1).toUpperCase()), ), title: Text(member.pseudo), ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Fermer'), ), ], ), ); } }