import React, { useMemo, useCallback, useEffect, useRef } from 'react'; import { VirtualizedList, useInfiniteScroll, } 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'; import { logger } from '@/utils/logger'; 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.sender?.username?.charAt(0).toUpperCase() || '?'}
{/* Contenu du message */}
{message.sender?.username || 'Utilisateur inconnu'} {formatDistanceToNow(new Date(message.created_at), { addSuffix: true, locale: fr, })}
{/* Contenu du message avec sanitisation */}
{/* Attachments */} {message.attachment_url && (
📎 Pièce jointe
)}
), [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 // ... imports import { apiClient } from '@/services/api/client'; // ... (props interface same) // ... (VirtualizedChatMessages component) // Replace message.user with message.sender // Remove metadata/is_edited/is_deleted if not in type OR cast/guard if expected // 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 { // Use apiClient.get for messages const response = await apiClient.get<{ data: Message[] }>('/messages', { params: { conversation_id: conversationId, page: pageNum, limit: 50, }, }); // apiClient unwrap déjà le format { success, data } const data = response.data; const newMessages = (data.data as unknown as Message[]) || []; // Note: has_next peut être dans data si c'est une PaginatedResponse const paginatedData = data as any; if (pageNum === 1) { setMessages(newMessages); } else { setMessages((prev) => [...newMessages, ...prev]); } setHasNextPage(paginatedData.has_next || false); setPage(pageNum); } catch (error) { logger.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, }; }