// /messages/messages.js (function () { const cfg = window.MESSAGING_CFG || {}; const app = document.querySelector('.msg-app'); const convList = document.getElementById('conv-list'); const threadPanel = document.getElementById('msg-thread-panel'); const titleEl = document.getElementById('thread-title'); const msgsEl = document.getElementById('messages'); const btnBack = document.getElementById('btn-back'); const form = document.getElementById('send-form'); const input = document.getElementById('msg-input'); const fileInput = document.getElementById('msg-file'); const hint = document.getElementById('send-hint'); const btnArchive = document.getElementById('btn-archive'); const btnClose = document.getElementById('btn-close'); // État let currentConv = Number(threadPanel?.dataset?.convId || cfg.initialConv || 0) || 0; let lastMsgId = 0; let pollTimer = null; let isArchived = false; let isClosed = false; // Utils function qs(obj) { return new URLSearchParams(obj).toString(); } function h(s) { const d = document.createElement('div'); d.textContent = (s ?? ''); return d.innerHTML; } function atBottom(el) { return Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < 6; } function scrollToBottom() { msgsEl.scrollTop = msgsEl.scrollHeight; } function setShowThread(on) { if (on) app.classList.add('show-thread'); else app.classList.remove('show-thread'); } // ===== API helpers ===== async function apiListConversations() { const r = await fetch('/messages/api/list_conversations.php', { credentials: 'same-origin' }); const j = await r.json(); if (!r.ok) throw new Error('list_failed'); return j.items || []; } async function apiFetchMessages(onlyNew) { if (!currentConv) return []; const url = new URL('/messages/api/fetch_messages.php', location.origin); url.searchParams.set('conversation_id', String(currentConv)); if (onlyNew && lastMsgId) url.searchParams.set('since_id', String(lastMsgId)); const r = await fetch(url.toString(), { credentials: 'same-origin' }); const j = await r.json(); if (!r.ok) throw new Error('fetch_failed'); return j.items || []; } async function apiSendMessage(fd) { const r = await fetch('/messages/api/send_message.php', { method: 'POST', body: fd, credentials: 'same-origin' }); const j = await r.json().catch(() => ({})); if (!r.ok || !j.ok) throw new Error(j.error || 'send_failed'); return j; } async function apiMarkRead() { if (!currentConv) return; try { const res = await fetch('/messages/api/mark_read.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, credentials: 'same-origin', body: qs({ conversation_id: String(currentConv) }) }); const data = await res.json(); if (data.ok && window.applyBadges) { window.applyBadges(data); } } catch (e) { // silencieux volontairement } } async function apiDeleteMessage(id) { const fd = new FormData(); fd.set('_csrf', cfg.csrf); fd.set('message_id', String(id)); const r = await fetch('/messages/api/delete_message.php', { method: 'POST', body: fd, credentials: 'same-origin' }); const j = await r.json(); if (!r.ok || !j.ok) throw new Error(j.error || 'delete_failed'); return j; } async function apiArchiveConversation(convId, archive) { const fd = new FormData(); fd.set('_csrf', cfg.csrf); fd.set('conversation_id', String(convId)); fd.set('archive', String(archive ? 1 : 0)); const r = await fetch('/messages/api/archive_conversation.php', { method: 'POST', body: fd, credentials: 'same-origin' }); const j = await r.json(); if (!r.ok || !j.ok) throw new Error(j.error || 'archive_failed'); return j; } async function apiCloseConversation(convId, close) { const fd = new FormData(); fd.set('_csrf', cfg.csrf); fd.set('conversation_id', String(convId)); fd.set('close', String(close ? 1 : 0)); const r = await fetch('/messages/api/close_conversation.php', { method: 'POST', body: fd, credentials: 'same-origin' }); const j = await r.json(); if (!r.ok || !j.ok) throw new Error(j.error || 'close_failed'); return j; } async function apiGetConversation(convId) { const url = new URL('/messages/api/get_conversation.php', location.origin); url.searchParams.set('conversation_id', String(convId)); const r = await fetch(url.toString(), { credentials: 'same-origin' }); const j = await r.json(); if (!r.ok || !j.ok) throw new Error(j.error || 'meta_failed'); return j; // {status, archived_for_me, ...} } // ===== Rendu UI ===== async function loadConversations() { try { const items = await apiListConversations(); renderConversations(items); } catch (e) { convList.innerHTML = `
API indisponible
`; } } function renderConversations(items){ convList.innerHTML = ''; if (!items.length){ const el = document.createElement('div'); el.className = 'conv-item'; el.innerHTML = `
${h(cfg.i18n?.noConversations || 'Sélectionner une conversation')}
`; convList.appendChild(el); return; } items.forEach(it => { const btn = document.createElement('div'); // div pour pouvoir avoir 2 zones cliquables btn.className = 'conv-item'; btn.innerHTML = `
${h(it.peer_name || ('#'+it.id))}
${h(it.last_body || '')}
${Number(it.unread||0) ? `${it.unread}` : ''} `; // ouvrir la conversation si on clique sur le bloc texte btn.querySelector('.conv-main').onclick = () => openConversation(Number(it.id)); // suppression via la corbeille btn.querySelector('.conv-delete').onclick = async (e) => { e.stopPropagation(); if (!confirm(cfg.i18n?.deleteConvConfirm || 'Supprimer cette conversation ?')) return; try{ await apiDeleteConversation(it.id); // Si on est en train de regarder cette conv, on ferme le thread if (Number(threadPanel.dataset.convId||0) === Number(it.id)) { threadPanel.dataset.convId = '0'; titleEl.textContent = cfg.i18n?.noConversations || 'Sélectionner une conversation'; msgsEl.innerHTML = ''; setShowThread(false); const url = new URL(window.location.href); url.searchParams.delete('conv'); window.history.replaceState(null, '', url.toString()); } await loadConversations(); }catch(err){ alert(cfg.i18n?.deleteConvFail || 'Suppression impossible.'); } }; convList.appendChild(btn); }); } async function apiDeleteConversation(convId){ const fd = new FormData(); fd.set('_csrf', cfg.csrf); fd.set('conversation_id', String(convId)); const r = await fetch('/messages/api/delete_conversation.php', { method:'POST', body: fd, credentials: 'same-origin' }); const j = await r.json(); if (!r.ok || !j.ok) throw new Error(j.error || 'delete_conv_failed'); return j; } async function fetchAndRender(onlyNew) { if (!currentConv) return; try { const items = await apiFetchMessages(onlyNew); renderMessages(items, !!onlyNew); } catch (e) { // silencieux } } function renderMessages(items, append) { if (!append) msgsEl.innerHTML = ''; const stick = atBottom(msgsEl); items.forEach(m => { const mine = (m.sender_role === cfg.role && Number(m.sender_id) === Number(cfg.meId)); const wrap = document.createElement('div'); wrap.className = 'msg' + (mine ? ' me' : ''); wrap.dataset.id = String(m.id); const parts = []; if (m.body) parts.push(`
${h(m.body)}
`); if (m.attachment_path) { const name = m.attachment_name || 'fichier'; parts.push(`
${h(name)}
`); } const actions = `
`; wrap.innerHTML = `
${parts.join('')}
${h(m.created_at || '')}
${actions} `; msgsEl.appendChild(wrap); lastMsgId = Math.max(lastMsgId, Number(m.id || 0)); }); if (stick) scrollToBottom(); } function applyClosedUI(on) { isClosed = !!on; if (btnClose) btnClose.textContent = on ? (cfg.i18n?.reopen || 'Rouvrir') : (cfg.i18n?.close || 'Clôturer'); if (input) input.disabled = on; if (fileInput) fileInput.disabled = on; const sendBtn = form?.querySelector('.send-btn'); if (sendBtn) sendBtn.disabled = on; // bannière info let banner = document.getElementById('thread-closed-banner'); if (on) { if (!banner) { banner = document.createElement('div'); banner.id = 'thread-closed-banner'; banner.className = 'thread-banner'; banner.textContent = cfg.i18n?.closedBanner || 'Cette conversation est clôturée.'; const body = threadPanel.querySelector('.thread-body'); threadPanel.insertBefore(banner, body || threadPanel.firstChild); } } else if (banner) { banner.remove(); } } async function openConversation(id) { currentConv = Number(id) || 0; threadPanel.dataset.convId = String(currentConv); titleEl.textContent = `…`; setShowThread(true); lastMsgId = 0; // labels par défaut if (btnArchive) btnArchive.textContent = cfg.i18n?.archive || 'Archiver'; if (btnClose) btnClose.textContent = cfg.i18n?.close || 'Clôturer'; applyClosedUI(false); isArchived = false; // synchro état réel try { const meta = await apiGetConversation(currentConv); titleEl.textContent = meta.peer_name || `${(cfg.i18n?.conversation || 'Conversation')} #${currentConv}`; applyClosedUI(meta.status === 'closed'); isArchived = !!meta.archived_for_me; if (btnArchive) btnArchive.textContent = isArchived ? (cfg.i18n?.unarchive || cfg.i18n?.desarchiver || 'Désarchiver') : (cfg.i18n?.archive || 'Archiver'); } catch (e) { /* ok si indispo */ } await fetchAndRender(false); await fetchAndRender(false); // force descendre tout en bas à l’ouverture msgsEl.scrollTop = msgsEl.scrollHeight; await apiMarkRead(); startPolling(); const url = new URL(window.location.href); url.searchParams.set('conv', String(currentConv)); window.history.replaceState(null, '', url.toString()); } function startPolling() { if (pollTimer) clearInterval(pollTimer); pollTimer = setInterval(async () => { if (!currentConv) return; await fetchAndRender(true); // Optionnel: rafraîchir la liste pour les badges loadConversations(); }, 3000); } // ===== Handlers ===== form?.addEventListener('submit', async (e) => { e.preventDefault(); if (!currentConv) return; const fd = new FormData(form); fd.set('conversation_id', String(currentConv)); fd.set('_csrf', cfg.csrf); const btn = form.querySelector('.send-btn'); if (btn) btn.disabled = true; hint.textContent = ''; try { await apiSendMessage(fd); form.reset(); await fetchAndRender(true); await apiMarkRead(); loadConversations(); input?.focus(); } catch (e2) { hint.textContent = cfg.i18n?.sendError || 'Erreur lors de l’envoi.'; } finally { if (btn) btn.disabled = false; } }); // Délégation: suppression “pour moi” msgsEl.addEventListener('click', async (e) => { const delBtn = e.target.closest?.('[data-action="delete-for-me"]'); if (!delBtn) return; const wrap = delBtn.closest('.msg'); const id = Number(wrap?.dataset.id || 0); if (!id) return; if (!confirm(cfg.i18n?.hideConfirm || 'Masquer ce message pour vous ?')) return; try { await apiDeleteMessage(id); wrap.remove(); hint.textContent = cfg.i18n?.hideOk || 'Message masqué.'; setTimeout(() => { hint.textContent = ''; }, 2000); } catch (err) { alert(cfg.i18n?.hideFail || 'Suppression impossible pour le moment.'); } }); // Archiver / Désarchiver (toggle) btnArchive?.addEventListener('click', async () => { if (!currentConv) return; try { btnArchive.disabled = true; const target = isArchived ? 0 : 1; await apiArchiveConversation(currentConv, target); isArchived = !!target; // Label btnArchive.textContent = isArchived ? (cfg.i18n?.unarchive || cfg.i18n?.desarchiver || 'Désarchiv­er') : (cfg.i18n?.archive || 'Archiver'); await loadConversations(); if (isArchived) { // fermer le fil, nettoyer l’URL currentConv = 0; threadPanel.dataset.convId = '0'; titleEl.textContent = cfg.i18n?.noConversations || 'Sélectionner une conversation'; msgsEl.innerHTML = ''; const url = new URL(window.location.href); url.searchParams.delete('conv'); window.history.replaceState(null, '', url.toString()); setShowThread(false); } } catch (e) { alert('Archive: erreur.'); } finally { btnArchive.disabled = false; } }); // Clôturer / Rouvrir (toggle) btnClose?.addEventListener('click', async () => { if (!currentConv) return; try { btnClose.disabled = true; if (!isClosed) { await apiCloseConversation(currentConv, 1); applyClosedUI(true); } else { await apiCloseConversation(currentConv, 0); applyClosedUI(false); } } catch (e) { alert('Clôture: ' + (e.message || 'erreur.')); } finally { btnClose.disabled = false; } }); // Retour mobile btnBack?.addEventListener('click', () => { setShowThread(false); const url = new URL(window.location.href); url.searchParams.delete('conv'); window.history.replaceState(null, '', url.toString()); }); // ===== INIT ===== loadConversations(); if (currentConv) { setShowThread(true); openConversation(currentConv); } else { setShowThread(false); } })();