import React, { useMemo, useCallback, useEffect, useRef } from 'react'; import { VirtualizedList, useInfiniteScroll, useScrollPosition } from '@/components/ui/virtualized-list'; import { Message } from '@/types/api'; import { sanitizeChatMessage } from '@/utils/sanitize'; import { formatDistanceToNow } from 'date-fns'; import { fr } from 'date-fns/locale'; interface VirtualizedChatMessagesProps { messages: Message[]; hasNextPage: boolean; isFetching: boolean; fetchNextPage: () => void; className?: string; onMessageClick?: (message: Message) => void; } const MESSAGE_HEIGHT = 80; // Hauteur estimée d'un message const CONTAINER_HEIGHT = 400; // Hauteur du conteneur de messages export function VirtualizedChatMessages({ messages, hasNextPage, isFetching, fetchNextPage, className = '', onMessageClick, }: VirtualizedChatMessagesProps) { const containerRef = useRef(null); const { handleItemsRendered } = useInfiniteScroll( messages, hasNextPage, isFetching, fetchNextPage, 5 // Charger plus quand on est à 5 messages du bas ); // Mémoriser les messages pour éviter les re-renders inutiles const memoizedMessages = useMemo(() => messages, [messages]); // Rendu d'un message individuel const renderMessage = useCallback( (message: Message, index: number) => (
onMessageClick?.(message)} style={{ minHeight: MESSAGE_HEIGHT }} >
{/* Avatar */}
{message.user?.username?.charAt(0).toUpperCase() || '?'}
{/* Contenu du message */}
{message.user?.username || 'Utilisateur inconnu'} {formatDistanceToNow(new Date(message.created_at), { addSuffix: true, locale: fr, })}
{/* Contenu du message avec sanitisation */}
{/* Métadonnées du message */} {message.metadata && (
{message.metadata.message_type && ( {message.metadata.message_type} )} {message.metadata.attachments && message.metadata.attachments.length > 0 && ( 📎 {message.metadata.attachments.length} fichier(s) )}
)} {/* Indicateurs de statut */}
{message.is_edited && ( (modifié) )} {message.is_deleted && ( (supprimé) )}
), [onMessageClick] ); // Gestion du scroll vers le bas pour les nouveaux messages const scrollToBottom = useCallback(() => { if (containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, []); // Auto-scroll vers le bas quand de nouveaux messages arrivent useEffect(() => { if (!isFetching && messages.length > 0) { const lastMessage = messages[messages.length - 1]; const isRecentMessage = Date.now() - new Date(lastMessage.created_at).getTime() < 5000; // 5 secondes if (isRecentMessage) { scrollToBottom(); } } }, [messages.length, isFetching, scrollToBottom]); // Indicateur de chargement const loadingIndicator = useMemo(() => { if (!isFetching) return null; return (
Chargement des messages...
); }, [isFetching]); // Message vide if (messages.length === 0 && !isFetching) { return (
💬

Aucun message dans cette conversation

Soyez le premier à envoyer un message !

); } return (
{/* Indicateur de chargement en haut */} {isFetching && hasNextPage && (
{loadingIndicator}
)} {/* Liste virtualisée des messages */} {/* Bouton pour revenir en bas */}
); } // Hook pour gérer l'état des messages avec pagination export function useChatMessages(conversationId: string) { const [messages, setMessages] = React.useState([]); const [hasNextPage, setHasNextPage] = React.useState(true); const [isFetching, setIsFetching] = React.useState(false); const [page, setPage] = React.useState(1); const fetchMessages = useCallback(async (pageNum: number = 1) => { if (isFetching) return; setIsFetching(true); try { const response = await fetch(`/api/conversations/${conversationId}/messages?page=${pageNum}&limit=50`); const data = await response.json(); if (pageNum === 1) { setMessages(data.messages || []); } else { setMessages(prev => [...(data.messages || []), ...prev]); } setHasNextPage(data.has_next_page || false); setPage(pageNum); } catch (error) { console.error('Erreur lors du chargement des messages:', error); } finally { setIsFetching(false); } }, [conversationId, isFetching]); const fetchNextPage = useCallback(() => { if (hasNextPage && !isFetching) { fetchMessages(page + 1); } }, [hasNextPage, isFetching, fetchMessages, page]); const addMessage = useCallback((message: Message) => { setMessages(prev => [...prev, message]); }, []); const updateMessage = useCallback((messageId: string, updates: Partial) => { setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, ...updates } : msg ) ); }, []); const deleteMessage = useCallback((messageId: string) => { setMessages(prev => prev.filter(msg => msg.id !== messageId)); }, []); // Charger les messages au montage useEffect(() => { fetchMessages(1); }, [conversationId]); return { messages, hasNextPage, isFetching, fetchNextPage, addMessage, updateMessage, deleteMessage, }; }