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 (