Chat: Bilder, Kamerafoto, Dateien und Sprachnachrichten
- Kundenbereich und Admin-Chat: Bueroklammer-Menue (Bild, Kamera, Datei), Mikrofon fuer Sprachnachrichten, Vorschau vor dem Senden mit Upload-Fortschritt, Einfuegen (Strg+V) und Hineinziehen von Dateien - Kamera: am Handy/Tablet die Kamera-App, am Rechner die Kamera im Browser mit Vorschau, Kamerawechsel und "Nochmal" - Sprachnachricht: Aufnahme mit Zeit und Pegel, vorher anhoerbar oder direkt senden, hoechstens 5 Minuten - Verlauf: Bilder (einzeln bzw. Raster, Grossansicht), Player fuer Sprachnachrichten, Datei-Kacheln mit Download - Server: jede Datei einzeln roh hochgeladen (max. 25 MB, 10 je Nachricht, max. 3 gleichzeitig), Typ aus dem Inhalt, nur erlaubte Endungen, keine Programme; Ablage unter upload-data/chat/<Kunde>/ ausserhalb von public/, Auslieferung nur mit Login; Bilder, Audio und PDF inline, alles andere als Download (nosniff, CSP sandbox) - Nicht gesendete Anhaenge werden nach 24 Stunden geloescht - Benachrichtigungs-Mail nennt nur Art und Namen der Anhaenge - Appwrite: neues Attribut portalMessages.attachments (scripts/setup-chat-collection.mjs, vor dem Deploy ausfuehren) - Datenschutzerklaerung Abschnitt 6 ergaenzt (Stand 26.09.2026), PDF neu Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e18a70a5d
commit
35d98da977
15 files changed
+2086
-107
No files matched your search
+32
-41
@@ -517,16 +517,25 @@ const chatState = {
|
||||
badgeTimer: null,
|
||||
initialized: false,
|
||||
viewAs: false,
|
||||
shownIds: new Set(),
|
||||
composer: null,
|
||||
}
|
||||
|
||||
const chatFileUrl = (att) => `/api/chat/attachments/${encodeURIComponent(att.id)}`
|
||||
|
||||
function appendChatMessages(messages) {
|
||||
const box = document.getElementById('chat-messages')
|
||||
if (!box || !messages.length) return
|
||||
for (const msg of messages) {
|
||||
const div = document.createElement('div')
|
||||
div.className = `chat-bubble ${msg.sender === 'customer' ? 'mine' : 'theirs'}`
|
||||
div.innerHTML = `${escapeHtml(msg.text)}<span class="chat-meta">${msg.sender === 'customer' ? 'Du' : escapeHtml(msg.senderName || 'Webklar-Team')} · ${fmtDateTime(msg.createdAt)}</span>`
|
||||
box.appendChild(div)
|
||||
// Poll und Antwort auf das Senden koennen dieselbe Nachricht liefern
|
||||
if (chatState.shownIds.has(msg.id)) continue
|
||||
chatState.shownIds.add(msg.id)
|
||||
const mine = msg.sender === 'customer'
|
||||
box.appendChild(ChatComposer.renderMessage(msg, {
|
||||
mine,
|
||||
meta: `${mine ? 'Du' : msg.senderName || 'Webklar-Team'} · ${fmtDateTime(msg.createdAt)}`,
|
||||
fileUrl: chatFileUrl,
|
||||
}))
|
||||
if (msg.createdAt > chatState.lastCreatedAt) chatState.lastCreatedAt = msg.createdAt
|
||||
}
|
||||
box.scrollTop = box.scrollHeight
|
||||
@@ -562,47 +571,29 @@ function isChatTabActive() {
|
||||
return Boolean(document.getElementById('view-chat')?.classList.contains('active'))
|
||||
}
|
||||
|
||||
async function sendChatMessage() {
|
||||
const input = document.getElementById('chat-input')
|
||||
const btn = document.getElementById('chat-send')
|
||||
const text = input.value.trim()
|
||||
if (!text) return
|
||||
btn.disabled = true
|
||||
try {
|
||||
const { message } = await api('/api/chat/messages', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ text }),
|
||||
})
|
||||
input.value = ''
|
||||
appendChatMessages([message])
|
||||
} catch (err) {
|
||||
alert(err.status === 403 ? err.message : 'Nachricht konnte nicht gesendet werden: ' + err.message)
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
input.focus()
|
||||
}
|
||||
}
|
||||
|
||||
async function initChat() {
|
||||
if (chatState.initialized) return
|
||||
chatState.initialized = true
|
||||
|
||||
const input = document.getElementById('chat-input')
|
||||
const btn = document.getElementById('chat-send')
|
||||
|
||||
if (chatState.viewAs) {
|
||||
input.disabled = true
|
||||
btn.disabled = true
|
||||
input.placeholder = 'Im Ansichtsmodus kann nicht als Kunde geschrieben werden.'
|
||||
} else {
|
||||
btn.addEventListener('click', sendChatMessage)
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
sendChatMessage()
|
||||
}
|
||||
})
|
||||
}
|
||||
ChatComposer.trackScroll(document.getElementById('chat-messages'))
|
||||
// Text, Bilder, Kamerafoto, Dateien und Sprachnachrichten (public/chat-composer.js)
|
||||
chatState.composer = ChatComposer.create({
|
||||
root: document.getElementById('chat-composer'),
|
||||
dropZone: document.querySelector('#view-chat .chat-box'),
|
||||
uploadUrl: '/api/chat/attachments',
|
||||
placeholder: 'Nachricht an das WEBklar-Team…',
|
||||
placeholderShort: 'Nachricht…',
|
||||
disabled: chatState.viewAs,
|
||||
disabledText: 'Im Ansichtsmodus kann nicht als Kunde geschrieben werden.',
|
||||
send: async ({ text, attachmentIds }) => {
|
||||
const { message } = await api('/api/chat/messages', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ text, attachmentIds }),
|
||||
})
|
||||
return message
|
||||
},
|
||||
onSent: (message) => appendChatMessages([message]),
|
||||
})
|
||||
|
||||
await pollChatMessages()
|
||||
await api('/api/chat/read', { method: 'POST' }).catch(() => {})
|
||||
|
||||
Reference in new issue
Block a user