// 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 (
{msg.isAlert === 2 && (
En attente de contrôle {isAdmin && onModerate && (
)}
)}
{textContent && ( isHtmlAllowed ? (
/g, '>').replace(/"/g, '"').replace(/'/g, ''') }} className="text-sm leading-relaxed whitespace-pre-wrap admin-html break-words" /> ) : (

{textContent}

) )} {suspendMatch && msg.authorType === 'Admin' && !isMe && (
)} {attachments.length > 0 && (
{attachments.map((att, i) => ( {att.type === 'image' && (
onViewAttachment(att)} className="cursor-pointer"> Pièce jointe
{canDeleteAttachment && onDeleteAttachment && (
)} {att.type === 'pdf' && PdfThumbnail && (
onViewAttachment(att)} className={`cursor-pointer rounded-xl border flex flex-col text-left transition-transform hover:scale-[1.02] h-full ${isMe ? 'border-white/20 bg-black/10' : 'border-slate-200 bg-white'}`}>
Aperçu PDF
{canDeleteAttachment && onDeleteAttachment && (
)}
))}
)} {deletedAttachments.length > 0 && (
0 ? 'pt-2 border-t border-white/10' : ''}`}> {deletedAttachments.map((att, i) => (
Fichier supprimé par l'utilisateur pour libérer de l'espace.
))}
)}
{displayAuthor} • {isTemp ? Envoi en cours... : new Date(String(msg.created_at || '').replace(' ', 'T')).toLocaleString('fr-FR')} {isTemp && }
); }, (prevProps, nextProps) => { return prevProps.msg.id === nextProps.msg.id && prevProps.msg.resolved === nextProps.msg.resolved && prevProps.msg.isAlert === nextProps.msg.isAlert && prevProps.isAdmin === nextProps.isAdmin && prevProps.themeColor === nextProps.themeColor; }); return (
{isDragging && !readOnly && (
Déposez vos fichiers ici Image ou PDF ({maxFiles} max)
)} {rejectTarget && Modal && ( window.ReactDOM ? window.ReactDOM.createPortal( setRejectTarget(null)} preventClose={sending} actions={(close) => ( <> )} >

Vous êtes sur le point de rejeter ce message. Il sera supprimé et non transmis au destinataire.