import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import '../models/message.dart'; import '../services/chat_service.dart'; /// Page simple de chat class ChatPage extends StatefulWidget { final String roomId; final String roomTitle; final String? roomType; // Type de room (private, group, broadcast) final int? roomCreatorId; // ID du créateur pour les rooms broadcast final bool isEmbedded; // Pour indiquer si on est en mode split-view web const ChatPage({ super.key, required this.roomId, required this.roomTitle, this.roomType, this.roomCreatorId, this.isEmbedded = false, }); @override State createState() => ChatPageState(); } class ChatPageState extends State { final _service = ChatService.instance; final _messageController = TextEditingController(); final _scrollController = ScrollController(); bool _isLoading = true; bool _isLoadingMore = false; bool _hasMore = true; int _lastMessageCount = 0; // Vérifier si c'est une room broadcast et si l'utilisateur peut poster bool get _isBroadcast => widget.roomType == 'broadcast'; bool get _canSendMessage => !_isBroadcast || (widget.roomCreatorId != null && widget.roomCreatorId == _service.currentUserId); @override void initState() { super.initState(); _loadInitialMessages(); // L'API marque automatiquement comme lu lors du chargement des messages } Future _loadInitialMessages() async { setState(() => _isLoading = true); print('🚀 ChatPage: Chargement initial des messages pour room ${widget.roomId}'); final result = await _service.getMessages(widget.roomId, isInitialLoad: true); setState(() { _hasMore = result['has_more'] as bool; _isLoading = false; }); // Attendre un peu avant de scroller pour laisser le temps au ListView de se construire Future.delayed(const Duration(milliseconds: 100), _scrollToBottom); } Future _loadMoreMessages() async { if (_isLoadingMore || !_hasMore) return; // Récupérer le message le plus ancien dans Hive pour cette room final currentMessages = _service.messagesBox.values .where((m) => m.roomId == widget.roomId) .toList() ..sort((a, b) => a.sentAt.compareTo(b.sentAt)); if (currentMessages.isEmpty) return; setState(() => _isLoadingMore = true); final oldestMessageId = currentMessages.first.id; final result = await _service.getMessages(widget.roomId, beforeMessageId: oldestMessageId); setState(() { _hasMore = result['has_more'] as bool; _isLoadingMore = false; }); } void _scrollToBottom() { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 200), curve: Curves.easeOut, ); } } Future _sendMessage() async { final text = _messageController.text.trim(); if (text.isEmpty) return; _messageController.clear(); await _service.sendMessage(widget.roomId, text); _scrollToBottom(); } @override Widget build(BuildContext context) { // Si on est en mode embedded (web split-view), pas de Scaffold/AppBar if (widget.isEmbedded) { return Container( color: const Color(0xFFFAFAFA), child: Column( children: [ // En-tête simple pour le mode embedded Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.white, border: Border( bottom: BorderSide(color: Colors.grey[200]!), ), ), child: Row( children: [ if (_isBroadcast) ...[ Icon(Icons.campaign, size: 20, color: Colors.amber.shade600), const SizedBox(width: 8), ], Expanded( child: Text( widget.roomTitle, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ), if (_isBroadcast) Container( margin: const EdgeInsets.only(left: 8), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.amber.shade100, borderRadius: BorderRadius.circular(12), ), child: Text( 'ANNONCE', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.amber.shade800, ), ), ), ], ), ), // Contenu du chat Expanded( child: _buildChatContent(context), ), ], ), ); } // Mode normal avec Scaffold pour mobile return Scaffold( backgroundColor: const Color(0xFFFAFAFA), appBar: AppBar( title: Row( children: [ if (_isBroadcast) ...[ Icon(Icons.campaign, size: 20, color: Colors.amber.shade600), const SizedBox(width: 8), ], Expanded( child: Text( widget.roomTitle, overflow: TextOverflow.ellipsis, ), ), if (_isBroadcast) Container( margin: const EdgeInsets.only(left: 8), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.amber.shade100, borderRadius: BorderRadius.circular(12), ), child: Text( 'ANNONCE', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.amber.shade800, ), ), ), ], ), backgroundColor: Colors.white, foregroundColor: const Color(0xFF1E293B), elevation: 0, ), body: _buildChatContent(context), ); } Widget _buildChatContent(BuildContext context) { return Stack( children: [ Column( children: [ // Messages Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : ValueListenableBuilder>( valueListenable: _service.messagesBox.listenable(), builder: (context, box, _) { // Récupérer tous les messages de cette room depuis Hive final allMessages = box.values .where((m) => m.roomId == widget.roomId) .toList() ..sort((a, b) => a.sentAt.compareTo(b.sentAt)); print('🔍 ChatPage: ${allMessages.length} messages trouvés pour room ${widget.roomId}'); if (allMessages.isEmpty) { print('📭 Aucun message dans Hive pour cette room'); print('📦 Total messages dans Hive: ${box.length}'); final roomIds = box.values.map((m) => m.roomId).toSet(); print('🏠 Rooms dans Hive: $roomIds'); } else { // Détecter les doublons potentiels final messageIds = {}; final duplicates = []; for (final msg in allMessages) { if (messageIds.contains(msg.id)) { duplicates.add(msg.id); } messageIds.add(msg.id); } if (duplicates.isNotEmpty) { print('⚠️ DOUBLONS DÉTECTÉS: $duplicates'); } // Afficher les IDs des messages pour débugger print('📝 Liste des messages:'); for (final msg in allMessages) { print(' - ${msg.id}: "${msg.content.substring(0, msg.content.length > 20 ? 20 : msg.content.length)}..." (isMe: ${msg.isMe})'); } } // Détecter les nouveaux messages et scroller vers le bas if (allMessages.length > _lastMessageCount) { _lastMessageCount = allMessages.length; WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollToBottom(); } }); } if (allMessages.isEmpty) { return Center( child: Text( 'Aucun message', style: TextStyle( color: Colors.grey[600], fontSize: 16, ), ), ); } return Column( children: [ // Bouton "Charger plus" en haut if (_hasMore) Container( padding: const EdgeInsets.symmetric(vertical: 8), child: _isLoadingMore ? const SizedBox( height: 40, child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ), ) : TextButton.icon( onPressed: _loadMoreMessages, icon: const Icon(Icons.refresh, size: 18), label: const Text('Charger plus de messages'), style: TextButton.styleFrom( foregroundColor: const Color(0xFF2563EB), ), ), ), // Liste des messages avec pull-to-refresh Expanded( child: RefreshIndicator( onRefresh: _loadInitialMessages, child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: allMessages.length, itemBuilder: (context, index) { final message = allMessages[index]; return _MessageBubble( message: message, isBroadcast: _isBroadcast, ); }, ), ), ), ], ); }, ), ), // Input ou message broadcast if (_canSendMessage) Container( decoration: BoxDecoration( color: Colors.white, border: Border( top: BorderSide(color: Colors.grey[200]!), ), ), padding: const EdgeInsets.all(8), child: Row( children: [ Expanded( child: TextField( controller: _messageController, decoration: InputDecoration( hintText: _isBroadcast ? 'Nouvelle annonce...' : 'Message...', hintStyle: TextStyle(color: Colors.grey[400]), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), maxLines: null, textInputAction: TextInputAction.send, onSubmitted: (_) => _sendMessage(), ), ), IconButton( icon: Icon(_isBroadcast ? Icons.campaign : Icons.send), color: _isBroadcast ? Colors.amber.shade600 : const Color(0xFF2563EB), onPressed: _sendMessage, ), ], ), ) else Container( decoration: BoxDecoration( color: Colors.amber.shade50, border: Border( top: BorderSide(color: Colors.amber.shade200), ), ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Icon(Icons.campaign, size: 20, color: Colors.amber.shade700), const SizedBox(width: 12), Expanded( child: Text( 'Ceci est une annonce officielle. Seul l\'administrateur peut poster des messages.', style: TextStyle( fontSize: 13, color: Colors.amber.shade800, fontStyle: FontStyle.italic, ), ), ), ], ), ), ], ), ], ); } @override void dispose() { _messageController.dispose(); _scrollController.dispose(); super.dispose(); } } /// Widget simple pour une bulle de message class _MessageBubble extends StatelessWidget { final Message message; final bool isBroadcast; const _MessageBubble({ required this.message, this.isBroadcast = false, }); @override Widget build(BuildContext context) { final isMe = message.isMe; // Style spécial pour les messages broadcast if (isBroadcast) { return Container( margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 0), decoration: BoxDecoration( color: Colors.amber.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.amber.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // En-tête broadcast Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.amber.shade100, borderRadius: const BorderRadius.only( topLeft: Radius.circular(11), topRight: Radius.circular(11), ), ), child: Row( children: [ Icon(Icons.campaign, size: 18, color: Colors.amber.shade700), const SizedBox(width: 8), Text( 'ANNONCE OFFICIELLE', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: Colors.amber.shade800, letterSpacing: 0.5, ), ), const Spacer(), Text( _formatTime(message.sentAt), style: TextStyle( fontSize: 11, color: Colors.amber.shade700, ), ), ], ), ), // Contenu du message Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isMe) Padding( padding: const EdgeInsets.only(bottom: 4), child: Text( 'De: ${_getFullName(message)}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.amber.shade900, ), ), ), Text( message.content, style: const TextStyle( fontSize: 15, height: 1.4, ), ), ], ), ), ], ), ); } // Style normal pour les messages privés/groupe // Vérifier si le message est synchronisé final isSynced = message.isSynced; return Align( alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.75, ), decoration: BoxDecoration( color: isMe ? const Color(0xFFEFF6FF) : Colors.white, borderRadius: BorderRadius.circular(8), border: !isMe ? Border.all(color: Colors.grey[300]!) : null, ), child: Column( crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ if (!isMe) Text( _getFullName(message), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF2563EB), ), ), const SizedBox(height: 2), Text( message.content, style: TextStyle( fontSize: 14, fontStyle: isSynced ? FontStyle.normal : FontStyle.italic, color: isSynced ? null : Colors.grey[600], ), ), const SizedBox(height: 2), Row( mainAxisSize: MainAxisSize.min, children: [ Text( _formatTime(message.sentAt), style: TextStyle( fontSize: 11, color: Colors.grey[500], ), ), if (!isSynced) ...[ const SizedBox(width: 4), Icon( Icons.schedule, size: 12, color: Colors.orange[400], ), ], ], ), ], ), ), ); } String _formatTime(DateTime date) { return '${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; } String _getFullName(Message message) { // Construire le nom complet avec prénom + nom // sender_first_name contient le prénom (ex: "Pierre") // sender_name contient le nom de famille (ex: "VALERY ADM") final firstName = message.senderFirstName ?? ''; final lastName = message.senderName; if (firstName.isNotEmpty) { return '$firstName $lastName'; } return lastName; } }