Files
Webklar-Kundenbereich/public/neu/ansicht-chat.js
T

147 lines
5.3 KiB
JavaScript

/* 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 '<Name oder Webklar-Team> · …'. */
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
}