147 lines
5.3 KiB
JavaScript
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
|
|
}
|