// ECO-PANNEAU.FR - __react/socle/_socle_messagerie_chatbox.jsx const { useState, useEffect, useRef, useCallback, memo } = React; // 1. - Moteur d'échange (ChatBox et Upload) window.pano_ChatBox = ({ messages, currentUserRole, panneauId, refreshData, onSend, clientName, settings = {}, themeColor = '#059669', readOnly = false, onAction }) => { // Conformité de l'API History (Google 2026) const { activeDialog: routerActiveDialog, openDialog: routerOpenDialog, closeCurrentLayer: routerCloseLayer } = window.pano_useUrlModal(); // 1.1 - SÉCURITÉ ANTI-FUITE DE MÉMOIRE : Utilisation du Hook Global Zéro-Dette const { isMounted, safeFetch } = window.pano_useSafeFetch(); const [forceRender, setForceRender] = useState(0); useEffect(() => { const onPop = () => { if (isMounted.current) setForceRender(prev => prev + 1); }; window.addEventListener('popstate', onPop); return () => window.removeEventListener('popstate', onPop); }, [isMounted]); const { ArrowUpIcon, ArrowDownIcon, LoaderIcon, ImageIcon, FileTextIcon, PaperclipIcon, XIcon } = window.pano_getIcons(); const { Button, Modal, ModerationModal, UniversalViewer } = window.pano_getComponents(); const containerRef = useRef(null); const textareaRef = useRef(null); const [draft, setDraft] = useState(''); const [sending, setSending] = useState(false); const [uploadProgress, setUploadProgress] = useState(''); const [selectedFiles, setSelectedFiles] = useState([]); const [fileError, setFileError] = useState(''); const [isAtBottom, setIsAtBottom] = useState(true); const [isDragging, setIsDragging] = useState(false); const [dragCounter, setDragCounter] = useState(0); const [moderationWarning, setModerationWarning] = useState(null); const [viewingAttachment, setViewingAttachment] = useState(null); const [rejectTarget, setRejectTarget] = useState(null); const [rejectReason, setRejectReason] = useState(""); const isAtBottomRef = useRef(true); // RÈGLE MÉTIER : Débridage pour les rôles de confiance const maxFiles = (currentUserRole === 'public' || currentUserRole === 'riverain') ? 1 : 10; useEffect(() => { isAtBottomRef.current = isAtBottom; }, [isAtBottom]); useEffect(() => { if (routerActiveDialog !== 'view_attachment') setViewingAttachment(null); if (routerActiveDialog !== 'moderation_warning') setModerationWarning(null); }, [routerActiveDialog]); useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; } }, [draft]); useEffect(() => { const authorRole = currentUserRole === 'admin' ? 'Admin' : 'Client'; const unread = messages.filter(msg => { if (msg.resolved) return false; const isMe = (currentUserRole === 'admin' && msg.authorType === 'Admin') || (currentUserRole === 'client' && msg.authorType === 'Client' && (msg.author === 'Client' || msg.author === clientName)) || (currentUserRole === 'public' && msg.authorType !== 'Admin' && msg.authorType !== 'Client' && msg.authorType !== 'Système' && msg.authorType !== 'Systeme' && msg.authorType !== 'System'); return !isMe; }); if (unread.length > 0 && currentUserRole !== 'public' && currentUserRole !== 'riverain' && !readOnly) { safeFetch('interactions/read', { body: { panneauId, author: authorRole }, silent: true }).then((d) => { if (!isMounted.current) return; if(d && refreshData) { refreshData(true); } }); if (window.Notification && Notification.permission === 'granted' && localStorage.getItem('eco_push_enabled') === 'true') { const latestMsgId = unread[0].id || unread[0].created_at; const lastNotified = sessionStorage.getItem('last_notified_msg_' + panneauId); if (lastNotified !== latestMsgId) { new Notification("eco-panneau.fr", { body: "Nouveau message reçu.", icon: "/favicon.svg" }); sessionStorage.setItem('last_notified_msg_' + panneauId, latestMsgId); } } } }, [messages, panneauId, currentUserRole, refreshData, readOnly, clientName, safeFetch, isMounted]); const handleScroll = useCallback(() => { if (!containerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; const isBottom = scrollHeight - scrollTop - clientHeight < 50; setIsAtBottom(isBottom); }, []); const scrollToBottom = useCallback((smooth = true) => { if (containerRef.current) { containerRef.current.scrollTo({ top: containerRef.current.scrollHeight, behavior: smooth ? 'smooth' : 'auto' }); } }, []); const handleImageLoad = useCallback(() => { if (isAtBottomRef.current) { scrollToBottom(true); } }, [scrollToBottom]); useEffect(() => { const timer = setTimeout(() => scrollToBottom(false), 100); return () => clearTimeout(timer); }, [scrollToBottom]); useEffect(() => { if (isAtBottom) { scrollToBottom(true); } }, [messages.length, isAtBottom, scrollToBottom]); // GESTION DES FICHIERS MULTIPLES const addFiles = (fileList) => { setFileError(''); const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf']; const files = Array.from(fileList).filter(f => validTypes.includes(f.type)); if (files.length === 0) { setFileError("Format de fichier non supporté."); return; } if (selectedFiles.length + files.length > maxFiles) { setFileError(`Maximum ${maxFiles} fichier${maxFiles > 1 ? 's' : ''} autorisé${maxFiles > 1 ? 's' : ''}.`); return; } const oversized = files.filter(f => f.size > 5 * 1024 * 1024); if (oversized.length > 0) { setFileError("Certains fichiers dépassent la limite de 5 Mo."); return; } setSelectedFiles(prev => [...prev, ...files]); }; const handleFileChange = (e) => { addFiles(e.target.files); e.target.value = null; }; const handlePaste = (e) => { if (readOnly) return; if (e.clipboardData && e.clipboardData.files && e.clipboardData.files.length > 0) { addFiles(e.clipboardData.files); } }; const handleDragEnter = (e) => { if (readOnly) return; e.preventDefault(); e.stopPropagation(); setDragCounter(prev => prev + 1); setIsDragging(true); }; const handleDragLeave = (e) => { if (readOnly) return; e.preventDefault(); e.stopPropagation(); setDragCounter(prev => { const nc = prev - 1; if (nc === 0) setIsDragging(false); return nc; }); }; const handleDragOver = (e) => { if (readOnly) return; e.preventDefault(); e.stopPropagation(); }; const handleDrop = (e) => { if (readOnly) return; e.preventDefault(); e.stopPropagation(); setDragCounter(0); setIsDragging(false); if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) { addFiles(e.dataTransfer.files); } }; const removeFile = (index) => { const newFiles = [...selectedFiles]; newFiles.splice(index, 1); setSelectedFiles(newFiles); }; const executeSend = async (isPendingControl = false) => { setSending(true); setUploadProgress(''); let finalDraft = draft.trim(); for (let file of selectedFiles) { if (!isMounted.current) return; const fileType = file.type.includes('pdf') ? 'pdf' : 'image'; try { const isPublic = currentUserRole === 'public' || currentUserRole === 'riverain'; const fileId = await window.pano_uploadFile( file, fileType, (msg, percent) => { if (!isMounted.current) return; if (percent < 100) setUploadProgress(`${percent}%`); else setUploadProgress('Finalisation...'); }, isPublic, false, null, panneauId ); if (!isMounted.current) return; const actualId = fileId.id || fileId; const actualNumPages = fileId.numPages || 1; finalDraft += `\n[ATTACHMENT:${actualId}:${fileType}:${actualNumPages}]`; } catch(err) { if (!isMounted.current) return; setFileError("Erreur lors de l'upload : " + err.message); setSending(false); setUploadProgress(''); return; } } if (!isMounted.current) return; if (onSend) await onSend(finalDraft, isPendingControl); if (!isMounted.current) return; setDraft(''); setSelectedFiles([]); setUploadProgress(''); setSending(false); setTimeout(() => { if (isMounted.current) scrollToBottom(true); }, 50); }; const handleInitialSend = (e) => { if (e && e.preventDefault) e.preventDefault(); if (sending) return; if (!draft.trim() && selectedFiles.length === 0) return; if (currentUserRole === 'public' || currentUserRole === 'riverain') { if (window.pano_checkForBlacklist) { const word = window.pano_checkForBlacklist(draft, settings); if (word) { setModerationWarning(word); routerOpenDialog('moderation_warning'); return; } } else { if (window.pano_logFallback) window.pano_logFallback("Bouclier sémantique introuvable. Modération ignorée sur ChatBox."); } } executeSend(false); }; const triggerModerate = (msgId, action) => { if (action === 'reject') { setRejectTarget(msgId); setRejectReason(""); } else { executeModerate(msgId, action); } }; const executeModerate = async (msgId, action, reason = "") => { const d = await safeFetch('interactions/moderate', { body: { id: msgId, action: action, reason: reason }, setLoading: setSending, successMessage: action === 'approve' ? "Message validé et transmis." : "Message rejeté et supprimé." }); if (!isMounted.current) return; if (d && refreshData) refreshData(true); setRejectTarget(null); }; const triggerViewAttachment = (att) => { setViewingAttachment(att); routerOpenDialog('view_attachment'); }; // ACTION MÉTIER : Suppression d'une pièce jointe pour libérer du quota const handleDeleteAttachment = async (msgId, attId, attType) => { const d = await safeFetch('interactions/delete_attachment', { body: { message_id: msgId, file_id: attId, file_type: attType }, setLoading: setSending, successMessage: "Pièce jointe supprimée avec succès pour libérer de l'espace." }); if (!isMounted.current) return; if (d && refreshData) refreshData(true); }; // COMPOSANT BULLE DE MESSAGE INTÉGRÉ ICI const MessageBubble = window.pano_MessageBubble || memo(({ msg, isMe, displayAuthor, onImageLoad, onViewAttachment, isAdmin, onModerate, themeColor = '#059669', onAction, canDeleteAttachment, onDeleteAttachment }) => { const { AlertTriangleIcon, LoaderIcon, FileTextIcon, EditIcon, CheckCircleIcon, Trash2Icon } = window.pano_getIcons(); const { Button, PdfThumbnail } = window.pano_getComponents(); useEffect(() => { if (!window.DOMPurify && window.pano_logFallback) { window.pano_logFallback("DOMPurify introuvable dans la bulle locale de ChatBox. Utilisation du fallback regex pour la sécurisation HTML."); } }, []); const attachments = []; const deletedAttachments = []; const regex = /\[ATTACHMENT:([a-zA-Z0-9_.\/-]+):(image|pdf)(?::(\d+))?\]/g; let match; while ((match = regex.exec(msg.detail || '')) !== null) { attachments.push({ id: match[1], type: match[2], numPages: match[3] ? parseInt(match[3], 10) : 1 }); } const delRegex = /\[DELETED_ATTACHMENT(?::(image|pdf))?\]/g; let delMatch; while ((delMatch = delRegex.exec(msg.detail || '')) !== null) { deletedAttachments.push({ type: delMatch[1] || 'fichier' }); } const textContent = (msg.detail || '').replace(regex, '').replace(delRegex, '').trim(); const isHtmlAllowed = msg.authorType === 'Admin' || msg.authorType === 'Système' || msg.authorType === 'Systeme' || msg.authorType === 'System'; const suspendRegex = /Le panneau "([^"]+)" a été (?:suspendu|rejeté)/i; const suspendMatch = textContent.match(suspendRegex); const isTemp = msg.id.toString().startsWith('temp_'); const handleCorrigerClick = (e) => { e.preventDefault(); if (onAction && suspendMatch) { onAction('correct_panel', suspendMatch[1]); } }; const handleHtmlClick = (e) => { const grantBtn = e.target.closest('[data-action="grant_access"]'); if (grantBtn) { e.preventDefault(); e.stopPropagation(); window.dispatchEvent(new CustomEvent('chat_grant_access')); } }; return (
{textContent}
) )} {suspendMatch && msg.authorType === 'Admin' && !isMe && (Vous êtes sur le point de rejeter ce message. Il sera supprimé et non transmis au destinataire.
Mode lecture seule
Vous n'avez pas les droits requis pour envoyer des messages dans cette conversation.
{fileError}
} {selectedFiles.length > 0 && (