/** * ========================================================================= * PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0 * Socle Messagerie (2/3) - Moteur d'échange (ChatBox et Upload) * ========================================================================= */ const { useState, useEffect, useRef, useCallback } = React; // ------------------------------------------------------------------------- // COMPOSANT : CONTENEUR DE TCHAT (MOTEUR D'ÉCRITURE ET UPLOAD) // ------------------------------------------------------------------------- window.pano_ChatBox = ({ messages, currentUserRole, panneauId, refreshData, onSend, clientName, settings = {}, themeColor = '#059669', readOnly = false }) => { const routerActiveDialog = new URLSearchParams(window.location.search).get('dialog'); const routerCloseLayer = () => window.history.back(); const routerOpenDialog = (type) => { const u = new URL(window.location); u.searchParams.set('dialog', type); window.history.pushState({ dialog: type }, '', u); window.dispatchEvent(new Event('popstate')); }; const [forceRender, setForceRender] = useState(0); useEffect(() => { const onPop = () => setForceRender(prev => prev + 1); window.addEventListener('popstate', onPop); return () => window.removeEventListener('popstate', onPop); }, []); const { ArrowUpIcon, ArrowDownIcon, ArrowLeftIcon, LoaderIcon, ImageIcon, FileTextIcon, PaperclipIcon, DownloadIcon, XIcon } = window.pano_getIcons(); const Button = window.pano_Button; const ModerationModal = window.pano_ModerationModal; const PdfFullViewer = window.pano_PdfFullViewer; const MessageBubble = window.pano_MessageBubble; // Récupéré de _socle_messagerie_ui.jsx const containerRef = useRef(null); const textareaRef = useRef(null); const [draft, setDraft] = useState(''); const [sending, setSending] = useState(false); 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 isAtBottomRef = useRef(true); 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(m => !m.resolved && m.author !== authorRole); if (unread.length > 0 && currentUserRole !== 'public' && currentUserRole !== 'riverain' && !readOnly) { window.pano_apiFetch('interactions/read', { body: { panneauId, author: authorRole } }).then((d) => { 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]); 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]); 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) return; if (selectedFiles.length + files.length > 20) { setFileError("Maximum 20 fichiers autorisés par message."); return; } const validFiles = []; for (let f of files) { if (f.size > 5 * 1024 * 1024) { setFileError("Un ou plusieurs fichiers dépassent 5 Mo."); return; } validFiles.push(f); } setSelectedFiles(prev => [...prev, ...validFiles]); }; 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); let finalDraft = draft.trim(); for (let file of selectedFiles) { const fileType = file.type.includes('pdf') ? 'pdf' : 'image'; try { const isPublic = currentUserRole === 'public' || currentUserRole === 'riverain'; const fileId = await window.pano_uploadFile(file, fileType, null, isPublic, false, null, panneauId); finalDraft += `\n[ATTACHMENT:${fileId}:${fileType}]`; } catch(err) { setFileError("Erreur lors de l'upload : " + err.message); setSending(false); return; } } if (onSend) await onSend(finalDraft, isPendingControl); setDraft(''); setSelectedFiles([]); setSending(false); setTimeout(() => scrollToBottom(true), 50); }; const handleInitialSend = (e) => { if (e && e.preventDefault) e.preventDefault(); if (!draft.trim() && selectedFiles.length === 0) return; if (currentUserRole === 'public' || currentUserRole === 'riverain') { const word = window.pano_checkForBlacklist ? window.pano_checkForBlacklist(draft, settings) : null; if (word) { setModerationWarning(word); routerOpenDialog('moderation_warning'); return; } } executeSend(false); }; const triggerViewAttachment = (att) => { setViewingAttachment(att); routerOpenDialog('view_attachment'); }; return (
{isDragging && !readOnly && (
Déposez vos fichiers ici Images et PDF acceptés (max 20)
)} {routerActiveDialog === 'moderation_warning' && moderationWarning && ModerationModal && ( { routerCloseLayer(); setTimeout(() => executeSend(true), 100); }} /> )} {routerActiveDialog === 'view_attachment' && viewingAttachment && (
e.stopPropagation()}> Télécharger
e.stopPropagation()}> {viewingAttachment.type === 'image' && ( Aperçu )} {viewingAttachment.type === 'pdf' && PdfFullViewer && ( )}
)}
{messages.map((msg) => { const isMe = (currentUserRole === 'admin' && msg.authorType === 'Admin') || (currentUserRole === 'client' && msg.authorType === 'Client') || (currentUserRole === 'public' && msg.authorType !== 'Admin' && msg.authorType !== 'Client' && msg.authorType !== 'Système' && msg.authorType !== 'Systeme'); let displayAuthor = msg.author; if (panneauId === 'CONTACT_PUBLIC') { if (msg.authorType === 'Admin' || msg.author === 'Admin') displayAuthor = 'Service client'; else if (msg.authorType === 'Système' || msg.authorType === 'Systeme' || msg.author === 'Système' || msg.author === 'Systeme') displayAuthor = 'Système'; else displayAuthor = isMe ? 'Vous' : `Visiteur ${msg.author}`; } else { if (msg.authorType === 'Admin' || msg.author === 'Admin') displayAuthor = 'Support'; else if (msg.authorType === 'Client' || msg.author === 'Client') displayAuthor = isMe ? 'Vous' : msg.author; else if (msg.authorType === 'Système' || msg.authorType === 'Systeme' || msg.author === 'Système' || msg.author === 'Systeme') displayAuthor = 'Système'; else displayAuthor = isMe ? 'Vous' : 'Riverain'; } return ; })} {messages.length === 0 &&
Aucun message.
}
{!isAtBottom && messages.length > 0 && (
scrollToBottom(true)} className="bg-slate-800/90 backdrop-blur text-white px-4 py-2 rounded-full shadow-lg text-xs font-bold pointer-events-auto hover:bg-slate-900 transition flex items-center gap-2 animate-in fade-in slide-in-from-bottom-2 cursor-pointer"> Nouveaux messages
)} {readOnly ? (

Mode lecture seule

Vous n'avez pas les droits requis pour envoyer des messages dans cette conversation.

) : (
{fileError &&

{fileError}

} {selectedFiles.length > 0 && (
{selectedFiles.map((f, i) => (
{f.type.includes('pdf') ? : } {f.name}
))}
)}