/* Kundenansicht neu (P2): Bereich Chat. Verlauf und Eingabezeile des gemeinsamen Composers (window.ChatComposer, chat-composer.js), Poll alle 10 s mit ?since=, Gelesen-Markierung, Vorgabe aus "Postfach anfragen". Nachrichten gehen nur als Text in den Verlauf. */ import { api } from './api.js' import { h, leeren } from './dom.js' import { K, fmtDatumZeit, meldeZaehlerNeu, nimmChatVorgabe } from './ansicht-hilfen.js' export const CHAT_TEXTE = { platzhalter: 'Nachricht an das WEBklar-Team…', kurz: 'Nachricht…', gesperrt: 'Im Ansichtsmodus kann nicht als Kunde geschrieben werden.', leer: 'Noch keine Nachrichten.', } const POLL_MS = 10000 const dateiUrl = (att) => `/api/chat/attachments/${encodeURIComponent(att.id)}` /** 'Du · 14.10.26, 10:05' oder ' · …'. */ export function nachrichtMeta(msg) { const meine = msg?.sender === 'customer' return `${meine ? 'Du' : msg?.senderName || 'Webklar-Team'} · ${fmtDatumZeit(msg?.createdAt)}` } let aktiv = null // der laufende Chat: { timer, composer } function raeumeAuf() { if (!aktiv) return if (aktiv.timer) clearInterval(aktiv.timer) try { aktiv.composer?.destroy?.() } catch { /* schon abgebaut */ } aktiv = null } export async function zeigeChat(wurzel, { route, me, nav } = {}) { raeumeAuf() const eigener = { timer: null, composer: null } aktiv = eigener const gilt = () => aktiv === eigener leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …')) const Composer = globalThis.ChatComposer if (!Composer) { leeren(wurzel).append(h('p', { class: K.fehler }, 'Der Chat konnte nicht gestartet werden. Bitte lade die Seite neu.')) return } const viewAs = me?.viewAs === true const verlauf = h('div', { class: 'chat-messages kn-chat-verlauf', role: 'log', 'aria-label': 'Nachrichtenverlauf' }) const composerWurzel = h('div', { class: 'kn-chat-composer' }) const kasten = h('div', { class: 'chat-box kn-chat-kasten' }, verlauf, composerWurzel) leeren(wurzel).append(h('div', { class: 'kn-chat' }, kasten)) const gezeigt = new Set() let zuletzt = '' let leerHinweis = null let fehler = null const hinweisLeer = () => { if (gezeigt.size || leerHinweis) return leerHinweis = h('p', { class: K.leer }, CHAT_TEXTE.leer) verlauf.append(leerHinweis) } const raeumeHinweise = () => { leerHinweis?.remove(); leerHinweis = null; fehler?.remove(); fehler = null } /** Neue Nachrichten anhängen (nach id entdoppelt); Rückgabe: die wirklich neuen. */ function haengeAn(nachrichten) { const neu = [] for (const msg of nachrichten) { if (!msg || gezeigt.has(msg.id)) continue gezeigt.add(msg.id) neu.push(msg) } if (!neu.length) return neu raeumeHinweise() for (const msg of neu) { verlauf.append(Composer.renderMessage(msg, { mine: msg.sender === 'customer', meta: nachrichtMeta(msg), fileUrl: dateiUrl })) if (msg.createdAt > zuletzt) zuletzt = msg.createdAt } verlauf.scrollTop = verlauf.scrollHeight return neu } // Nachrichten als gelesen markieren; im Ansichtsmodus nie (der Kunde hat sie nicht gelesen) async function markiereGelesen() { if (viewAs) return await api('/api/chat/read', { method: 'POST' }).catch(() => {}) if (gilt()) meldeZaehlerNeu('chat') } async function hole() { const frage = zuletzt ? `?since=${encodeURIComponent(zuletzt)}` : '' const daten = await api(`/api/chat/messages${frage}`) if (!gilt()) return [] return haengeAn(Array.isArray(daten?.messages) ? daten.messages : []) } async function poll() { if (!gilt()) return if (!wurzel.isConnected) { clearInterval(eigener.timer); return } if (globalThis.document?.hidden) return if (wurzel.closest?.('[hidden]')) return // der Chat ist gerade nicht zu sehen try { const neu = await hole() if (neu.some((m) => m.sender !== 'customer')) await markiereGelesen() } catch { /* der nächste Poll versucht es erneut */ } } Composer.trackScroll(verlauf) eigener.composer = Composer.create({ root: composerWurzel, dropZone: kasten, uploadUrl: '/api/chat/attachments', placeholder: CHAT_TEXTE.platzhalter, placeholderShort: CHAT_TEXTE.kurz, disabled: viewAs, disabledText: CHAT_TEXTE.gesperrt, send: async ({ text, attachmentIds }) => { const { message } = await api('/api/chat/messages', { method: 'POST', body: { text, attachmentIds } }) return message }, onSent: (message) => { haengeAn([message]) }, }) const vorgabe = nimmChatVorgabe() if (vorgabe) { const feld = composerWurzel.querySelector('textarea') if (feld) { feld.value = vorgabe if (typeof Event === 'function') feld.dispatchEvent(new Event('input', { bubbles: true })) feld.focus() if (typeof feld.setSelectionRange === 'function') feld.setSelectionRange(vorgabe.length, vorgabe.length) } } try { await hole() if (!gilt()) return hinweisLeer() } catch (f) { if (!gilt()) return fehler = h('p', { class: K.fehler }, `Nachrichten konnten nicht geladen werden: ${f.message}`) verlauf.append(fehler) } await markiereGelesen() if (!gilt()) return eigener.timer = setInterval(poll, POLL_MS) eigener.timer?.unref?.() // im Browser eine Zahl; in Node hält der Timer den Prozess nicht fest }