Files
tickte-system/src/components/chat/ChatThread.jsx
T
ClaudeandClaude Opus 5.5 813c6a7e18 Chat: Korrekturen aus dem Abschlussreview
- Gespraech startet nicht neu, wenn der Kundenname nachlaedt (Entwurf bleibt, kein verwaister Timer)
- Neue Nachricht an Kunde: erst die eindeutige Beschriftung Name · E-Mail · Kd.-Nr. oeffnet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:46:51 +00:00

132 lines
4.9 KiB
React

import { useEffect, useRef, useState } from 'react'
import { FaRotateRight } from 'react-icons/fa6'
import { Button } from '../ui'
import { PORTAL_URL, chatUploadUrl, getChatMessages, markChatRead, sendChatMessage, teamAuthHeaders } from '../../lib/teamApi'
import { loadChatComposer } from '../../lib/loadChatComposer'
import { messageMeta, needsFullRefresh, noticeText } from '../../lib/chatFormat'
import '../../styles/chat.css'
const POLL_MS = 15 * 1000
/**
* Gespraech mit einem Kunden (Messenger und Reiter "Chat"). Sprechblasen und Eingabezeile kommen aus dem
* Chat-Baustein des Portals (window.ChatComposer); React verwaltet nur die leeren Container.
* Antworten fuer einen inzwischen gewechselten Kunden werden verworfen (alive je Effekt).
*/
export default function ChatThread({ customerId, customerName }) {
const zoneRef = useRef(null)
const boxRef = useRef(null)
const composerRef = useRef(null)
const [error, setError] = useState(null)
const [notice, setNotice] = useState('')
const [reloadKey, setReloadKey] = useState(0)
// Name nur ueber eine Referenz lesen: kommt er spaeter (Liste/Kunden laden noch), startet das Gespraech nicht neu
const nameRef = useRef(customerName)
nameRef.current = customerName
useEffect(() => {
let alive = true
let composer = null
let timer = null
let chat = null
const state = { shown: new Set(), last: '', fullAt: 0 }
setError(null)
setNotice('')
const append = (messages) => {
const box = boxRef.current
if (!alive || !box || !chat) return
let added = false
for (const msg of messages) {
if (state.shown.has(msg.id)) continue
state.shown.add(msg.id)
box.appendChild(chat.renderMessage(msg, {
mine: msg.sender === 'admin',
meta: messageMeta(msg, nameRef.current),
fileUrl: (att) => att.url,
}))
if (msg.createdAt > state.last) state.last = msg.createdAt
added = true
}
if (added) box.scrollTop = box.scrollHeight
}
// Ganzer Verlauf mit frischen signierten Links (beim Oeffnen und nach 10 Minuten)
const loadAll = async () => {
const { messages } = await getChatMessages(customerId)
if (!alive) return
boxRef.current?.replaceChildren()
state.shown = new Set()
state.last = ''
state.fullAt = Date.now()
append(messages || [])
markChatRead(customerId).catch(() => {})
}
const poll = async () => {
if (!alive || document.visibilityState === 'hidden' || !chat) return
try {
if (needsFullRefresh(state.fullAt, Date.now())) return await loadAll()
const { messages } = await getChatMessages(customerId, state.last)
if (!alive) return
append(messages || [])
if ((messages || []).some((m) => m.sender === 'customer')) markChatRead(customerId).catch(() => {})
} catch {
/* naechste Abfrage versucht es erneut */
}
}
;(async () => {
try {
chat = await loadChatComposer({ baseUrl: PORTAL_URL })
if (!alive) return
chat.trackScroll(boxRef.current)
composer = chat.create({
root: composerRef.current,
dropZone: zoneRef.current,
uploadUrl: chatUploadUrl(customerId),
requestHeaders: teamAuthHeaders,
placeholder: `Antwort an ${nameRef.current || 'Kunde'}…`,
placeholderShort: 'Antwort…',
send: async ({ text, attachmentIds }) => {
const { message, notice: result } = await sendChatMessage(customerId, { text, attachmentIds })
const time = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
if (alive) setNotice(noticeText(result, time))
return message
},
onSent: (message) => append([message]),
})
await loadAll()
// waehrend des Ladens verlassen: keinen Timer mehr anlegen, den das Aufraeumen nicht mehr sieht
if (!alive) return
timer = setInterval(poll, POLL_MS)
} catch (err) {
if (alive) setError(err.message || 'Die Unterhaltung konnte nicht geladen werden.')
}
})()
const onVisible = () => { if (document.visibilityState === 'visible') poll() }
document.addEventListener('visibilitychange', onVisible)
return () => {
alive = false
clearInterval(timer)
document.removeEventListener('visibilitychange', onVisible)
composer?.destroy()
}
}, [customerId, reloadKey])
return (
<div className="woms-chat" ref={zoneRef}>
{error && (
<div className="flex items-center gap-2 wrap">
<span className="muted">{error}</span>
<Button variant="ghost" onClick={() => setReloadKey((k) => k + 1)}><FaRotateRight /> Erneut versuchen</Button>
</div>
)}
<div className="chat-messages" ref={boxRef} aria-live="polite" />
{notice && <div className="chat-notice muted">{notice}</div>}
<div ref={composerRef} />
</div>
)
}