'use client'; import { useState, useEffect, useRef } from 'react'; import useSWR from 'swr'; import AlertModal from './AlertModal'; interface User { id: string; email: string; name: string | null; } interface Participant { id: string; email: string; name: string | null; } interface MessageFile { id: string; filename: string; filepath: string; fileType: string; fileSize: number; } interface Message { id: string; content: string | null; senderId: string; sender: User; createdAt: string; files: MessageFile[]; } interface Conversation { id: string; name: string | null; type: string; displayName: string; participants: Participant[]; } interface ChatWindowProps { conversationId: string; conversation: Conversation; onNewMessage: () => void; onShowGroupSettings: () => void; } const fetcher = (url: string) => fetch(url).then((res) => res.json()); export default function ChatWindow({ conversationId, conversation, onNewMessage, onShowGroupSettings, }: ChatWindowProps) { const [message, setMessage] = useState(''); const [files, setFiles] = useState([]); const [isSending, setIsSending] = useState(false); const [currentUser, setCurrentUser] = useState(null); const [typingUsers, setTypingUsers] = useState([]); const [readReceipts, setReadReceipts] = useState>({}); const [alertModal, setAlertModal] = useState<{ show: boolean; type: 'success' | 'error' | 'info' | 'warning'; title: string; message: string; } | null>(null); const messagesEndRef = useRef(null); const fileInputRef = useRef(null); const typingTimeoutRef = useRef(null); const lastTypingSignalRef = useRef(0); // Récupérer l'utilisateur actuel const { data: userData } = useSWR('/api/auth/me', fetcher); useEffect(() => { if (userData) { setCurrentUser(userData); } }, [userData]); const { data: messagesData, error, mutate } = useSWR<{ messages: Message[]; hasMore: boolean; cursor: string | null; }>(`/api/conversations/${conversationId}/messages?limit=50`, fetcher, { refreshInterval: 1000, // Rafraîchir toutes les secondes pour le temps réel }); const messages = messagesData?.messages || []; // Scroll vers le bas quand de nouveaux messages arrivent useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // Marquer les messages comme lus quand on les voit useEffect(() => { if (messages.length > 0) { fetch(`/api/conversations/${conversationId}/read`, { method: 'POST', }).catch(console.error); } }, [conversationId, messages.length]); // Récupérer les informations de lecture useEffect(() => { const fetchReadReceipts = async () => { try { const response = await fetch(`/api/conversations/${conversationId}/read`); if (response.ok) { const data = await response.json(); const receipts: Record = {}; data.participants.forEach((p: { userId: string; lastReadAt: string | null }) => { receipts[p.userId] = p.lastReadAt ? new Date(p.lastReadAt) : null; }); setReadReceipts(receipts); } } catch (error) { console.error('Erreur lors de la récupération des lectures:', error); } }; fetchReadReceipts(); const interval = setInterval(fetchReadReceipts, 2000); return () => clearInterval(interval); }, [conversationId]); // Récupérer les utilisateurs en train d'écrire useEffect(() => { const fetchTyping = async () => { try { const response = await fetch(`/api/conversations/${conversationId}/typing`); if (response.ok) { const data = await response.json(); setTypingUsers(data.typing || []); } } catch (error) { console.error('Erreur lors de la récupération du typing:', error); } }; fetchTyping(); const interval = setInterval(fetchTyping, 1000); return () => clearInterval(interval); }, [conversationId]); // Écouter les événements SSE pour les nouveaux messages useEffect(() => { const eventSource = new EventSource(`/api/conversations/${conversationId}/events`); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'new_message') { mutate(); onNewMessage(); // Marquer comme lu immédiatement fetch(`/api/conversations/${conversationId}/read`, { method: 'POST', }).catch(console.error); } }; eventSource.onerror = () => { eventSource.close(); }; return () => { eventSource.close(); }; }, [conversationId, mutate, onNewMessage]); const handleSendMessage = async () => { if ((!message.trim() && files.length === 0) || isSending) return; setIsSending(true); const formData = new FormData(); if (message.trim()) { formData.append('content', message); } files.forEach((file) => { formData.append('files', file); }); try { const response = await fetch(`/api/conversations/${conversationId}/messages`, { method: 'POST', body: formData, }); if (response.ok) { setMessage(''); setFiles([]); if (fileInputRef.current) { fileInputRef.current.value = ''; } mutate(); onNewMessage(); // Marquer comme lu immédiatement après l'envoi fetch(`/api/conversations/${conversationId}/read`, { method: 'POST', }).catch(console.error); } else { const error = await response.json(); setAlertModal({ show: true, type: 'error', title: 'Erreur', message: error.error || 'Erreur lors de l\'envoi du message', }); } } catch (error) { console.error('Erreur:', error); setAlertModal({ show: true, type: 'error', title: 'Erreur', message: 'Erreur lors de l\'envoi du message', }); } finally { setIsSending(false); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } else { // Signaler que l'utilisateur est en train d'écrire const now = Date.now(); if (now - lastTypingSignalRef.current > 1000) { // Envoyer le signal toutes les secondes maximum lastTypingSignalRef.current = now; fetch(`/api/conversations/${conversationId}/typing`, { method: 'POST', }).catch(console.error); } // Réinitialiser le timeout if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } } }; const handleMessageChange = (e: React.ChangeEvent) => { setMessage(e.target.value); // Signaler que l'utilisateur est en train d'écrire const now = Date.now(); if (now - lastTypingSignalRef.current > 1000) { lastTypingSignalRef.current = now; fetch(`/api/conversations/${conversationId}/typing`, { method: 'POST', }).catch(console.error); } // Réinitialiser le timeout if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } }; const handleFileSelect = (e: React.ChangeEvent) => { if (e.target.files) { setFiles(Array.from(e.target.files)); } }; const removeFile = (index: number) => { setFiles(files.filter((_, i) => i !== index)); }; const formatFileSize = (bytes: number) => { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; }; const isImageFile = (fileType: string) => { return fileType.startsWith('image/'); }; return (
{/* En-tête */}
{conversation.type === 'group' ? conversation.displayName.charAt(0).toUpperCase() : conversation.displayName .split(' ') .map((n) => n.charAt(0)) .join('') .toUpperCase() .slice(0, 2)}

{conversation.displayName}

{conversation.type === 'group' && (

{conversation.participants.length} participant{conversation.participants.length > 1 ? 's' : ''}

)}
{conversation.type === 'group' && ( )}
{/* Messages */}
{error && (
Erreur lors du chargement des messages
)} {!messagesData && !error && (
Chargement des messages...
)} {messages.length === 0 && messagesData && (
Aucun message. Commencez la conversation !
)}
{messages.map((msg) => { const isOwnMessage = currentUser && msg.senderId === currentUser.id; return (
{!isOwnMessage && conversation.type === 'group' && (
{msg.sender.name || msg.sender.email}
)} {msg.content && (

{msg.content}

)} {msg.files && msg.files.length > 0 && (
{msg.files.map((file) => (
{isImageFile(file.fileType) ? ( {file.filename} ) : (
{file.filename}
{formatFileSize(file.fileSize)}
)}
))}
)}
{new Date(msg.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', })} {isOwnMessage && (
{/* Checkmarks pour les messages envoyés/lus */} {(() => { // Filtrer les autres participants (pas l'utilisateur actuel) const otherParticipants = conversation.participants.filter( (p) => p.id !== currentUser?.id ); // Vérifier si tous les autres participants ont lu le message const allRead = otherParticipants.length > 0 && otherParticipants.every((p) => { const lastRead = readReceipts[p.id]; if (!lastRead) return false; return new Date(lastRead) >= new Date(msg.createdAt); }); // Premier checkmark (toujours visible) const firstCheck = ( ); // Deuxième checkmark (seulement si lu) const secondCheck = allRead ? ( ) : null; return ( <> {firstCheck} {secondCheck} ); })()}
)}
); })}
{/* Indicateur de frappe */} {typingUsers.length > 0 && (
{typingUsers.length === 1 ? `${typingUsers[0].name || typingUsers[0].email} est en train d'écrire...` : `${typingUsers.length} personnes sont en train d'écrire...`}
)}
{/* Zone de saisie */}
{/* Fichiers sélectionnés */} {files.length > 0 && (
{files.map((file, index) => (
{file.name}
))}
)}