import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '../../../config/injection/injection.dart'; import '../../../core/constants/design_system.dart'; import '../../../core/utils/page_transitions.dart'; import '../../../data/services/secure_storage.dart'; import '../../../domain/entities/conversation.dart'; import '../../state_management/chat_bloc.dart'; import '../../widgets/animated_widgets.dart'; import '../../widgets/custom_snackbar.dart'; import '../../widgets/modern_empty_state.dart'; import '../../widgets/shimmer_loading.dart'; import 'chat_screen.dart'; /// Écran de la liste des conversations. class ConversationsScreen extends StatelessWidget { const ConversationsScreen({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => sl(), child: const _ConversationsScreenContent(), ); } } class _ConversationsScreenContent extends StatefulWidget { const _ConversationsScreenContent(); @override State<_ConversationsScreenContent> createState() => _ConversationsScreenContentState(); } class _ConversationsScreenContentState extends State<_ConversationsScreenContent> { final SecureStorage _storage = SecureStorage(); String? _currentUserId; @override void initState() { super.initState(); _loadConversations(); } Future _loadConversations() async { final userId = await _storage.getUserId(); if (userId == null) return; setState(() { _currentUserId = userId; }); if (mounted) { context.read().add(LoadConversations(userId)); context.read().add(LoadUnreadCount(userId)); } } void _openConversation(Conversation conversation) { context.pushSlideUp(ChatScreen(conversation: conversation)); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text('Messages'), actions: [ BlocBuilder( builder: (context, state) { if (state.unreadCount > 0) { return Stack( children: [ IconButton( icon: const Icon(Icons.notifications_outlined), onPressed: () { Navigator.of(context).pushNamed('/notifications'); }, ), Positioned( right: 8, top: 8, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: theme.colorScheme.error, shape: BoxShape.circle, ), constraints: const BoxConstraints( minWidth: 16, minHeight: 16, ), child: Text( state.unreadCount > 9 ? '9+' : state.unreadCount.toString(), style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ), ], ); } return const SizedBox.shrink(); }, ), IconButton( icon: const Icon(Icons.search), onPressed: () { showSearch( context: context, delegate: ConversationSearchDelegate( conversations: context.read().state.conversations, currentUserId: _currentUserId, ), ); }, ), ], ), body: BlocConsumer( listener: (context, state) { if (state.conversationsStatus == ConversationsStatus.error) { context.showError(state.errorMessage ?? 'Erreur de chargement'); } }, builder: (context, state) { if (state.conversationsStatus == ConversationsStatus.loading) { return const SkeletonList( itemCount: 8, skeletonWidget: ListItemSkeleton(), ); } if (state.conversationsStatus == ConversationsStatus.error) { return _buildErrorState(theme, state.errorMessage); } if (state.conversations.isEmpty) { return _buildEmptyState(); } // Trier les conversations par dernière activité final sortedConversations = List.from(state.conversations); sortedConversations.sort((a, b) { if (a.lastMessageTimestamp == null && b.lastMessageTimestamp == null) return 0; if (a.lastMessageTimestamp == null) return 1; if (b.lastMessageTimestamp == null) return -1; return b.lastMessageTimestamp!.compareTo(a.lastMessageTimestamp!); }); return RefreshIndicator( onRefresh: _loadConversations, child: ListView.separated( padding: const EdgeInsets.all(DesignSystem.spacingLg), itemCount: sortedConversations.length, separatorBuilder: (context, index) => Divider( height: 1, color: theme.dividerColor, ), itemBuilder: (context, index) { return _buildConversationCard(sortedConversations[index]); }, ), ); }, ), ); } Widget _buildConversationCard(Conversation conversation) { final theme = Theme.of(context); final timeFormat = conversation.lastMessageTimestamp != null ? _getTimeFormat(conversation.lastMessageTimestamp!) : ''; return FadeInWidget( child: AnimatedCard( margin: const EdgeInsets.only(bottom: DesignSystem.spacingSm), padding: const EdgeInsets.all(DesignSystem.spacingLg), onTap: () => _openConversation(conversation), child: Row( children: [ // Avatar avec badge pour les non lus Stack( children: [ Hero( tag: 'conversation_avatar_${conversation.participantId}', child: CircleAvatar( radius: 28, backgroundImage: conversation.participantProfileImageUrl != null ? NetworkImage(conversation.participantProfileImageUrl!) : null, child: conversation.participantProfileImageUrl == null ? Text( conversation.participantFirstName[0].toUpperCase(), style: const TextStyle(fontSize: 20), ) : null, ), ), if (conversation.hasUnreadMessages) Positioned( right: 0, top: 0, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: theme.colorScheme.error, shape: BoxShape.circle, border: Border.all( color: theme.scaffoldBackgroundColor, width: 2, ), ), constraints: const BoxConstraints( minWidth: 20, minHeight: 20, ), child: Text( conversation.unreadCount > 9 ? '9+' : conversation.unreadCount.toString(), style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ), ], ), const SizedBox(width: DesignSystem.spacingLg), // Informations de la conversation Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( conversation.participantFullName, style: theme.textTheme.titleMedium?.copyWith( fontWeight: conversation.hasUnreadMessages ? FontWeight.bold : FontWeight.normal, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (conversation.lastMessageTimestamp != null) ...[ const SizedBox(width: DesignSystem.spacingSm), Text( timeFormat, style: theme.textTheme.bodySmall?.copyWith( color: Colors.grey[600], fontWeight: conversation.hasUnreadMessages ? FontWeight.bold : FontWeight.normal, ), ), ], ], ), const SizedBox(height: 4), Row( children: [ if (conversation.isTyping) ...[ Text( 'En train d\'écrire...', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary, fontStyle: FontStyle.italic, ), ), ] else if (conversation.hasLastMessage) ...[ Expanded( child: Text( conversation.lastMessage!, style: theme.textTheme.bodySmall?.copyWith( color: conversation.hasUnreadMessages ? theme.textTheme.bodyMedium?.color : Colors.grey[600], fontWeight: conversation.hasUnreadMessages ? FontWeight.w500 : FontWeight.normal, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ], ), ], ), ), ], ), ), ); } String _getTimeFormat(DateTime timestamp) { final now = DateTime.now(); final difference = now.difference(timestamp); if (difference.inDays == 0) { // Aujourd'hui - afficher l'heure return DateFormat('HH:mm').format(timestamp); } else if (difference.inDays == 1) { // Hier return 'Hier'; } else if (difference.inDays < 7) { // Cette semaine - afficher le jour return DateFormat('EEEE', 'fr_FR').format(timestamp); } else { // Plus ancien - afficher la date return DateFormat('dd/MM/yyyy').format(timestamp); } } Widget _buildErrorState(ThemeData theme, String? errorMessage) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.grey[400]), const SizedBox(height: DesignSystem.spacingLg), Text('Erreur de chargement', style: theme.textTheme.titleLarge), const SizedBox(height: DesignSystem.spacingSm), Text( errorMessage ?? 'Une erreur est survenue', style: theme.textTheme.bodyMedium, textAlign: TextAlign.center, ), const SizedBox(height: DesignSystem.spacingXl), ElevatedButton.icon( onPressed: _loadConversations, icon: const Icon(Icons.refresh), label: const Text('Réessayer'), ), ], ), ); } Widget _buildEmptyState() { return ModernEmptyState( illustration: EmptyStateIllustration.social, title: 'Aucune conversation', description: 'Commencez à discuter avec vos amis !', actionLabel: 'Voir mes amis', onAction: () { Navigator.pop(context); }, ); } } /// Delegate de recherche pour les conversations class ConversationSearchDelegate extends SearchDelegate { ConversationSearchDelegate({ required this.conversations, required this.currentUserId, }); final List conversations; final String? currentUserId; @override List buildActions(BuildContext context) { return [ IconButton( icon: const Icon(Icons.clear), onPressed: () { query = ''; }, ), ]; } @override Widget buildLeading(BuildContext context) { return IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { close(context, null); }, ); } @override Widget buildResults(BuildContext context) { return _buildSearchResults(context); } @override Widget buildSuggestions(BuildContext context) { return _buildSearchResults(context); } Widget _buildSearchResults(BuildContext context) { if (query.isEmpty) { return const Center( child: Text('Rechercher une conversation...'), ); } final results = conversations.where((conv) { final fullName = '${conv.participantFirstName} ${conv.participantLastName}'.toLowerCase(); final lastMessage = conv.lastMessage.toLowerCase(); final searchQuery = query.toLowerCase(); return fullName.contains(searchQuery) || lastMessage.contains(searchQuery); }).toList(); if (results.isEmpty) { return const Center( child: Text('Aucun résultat trouvé'), ); } return ListView.builder( itemCount: results.length, itemBuilder: (context, index) { final conversation = results[index]; return ListTile( leading: CircleAvatar( backgroundImage: conversation.participantProfileImageUrl != null ? NetworkImage(conversation.participantProfileImageUrl!) : null, child: conversation.participantProfileImageUrl == null ? Text(conversation.participantFirstName[0].toUpperCase()) : null, ), title: Text(conversation.participantFullName), subtitle: Text( conversation.lastMessage, maxLines: 1, overflow: TextOverflow.ellipsis, ), onTap: () { close(context, conversation); Navigator.of(context).pushNamed( '/chat', arguments: conversation, ); }, ); }, ); } }