Chat-Anhaenge: Korrekturen aus dem Code-Review

- Kamerafotos in anderen Formaten (z. B. HEIF von Android-Kameras) werden
  wie Dateien angenommen statt abgelehnt
- Dateinamen nach Zeichen kuerzen: ein halbiertes Emoji machte den Anhang
  unabrufbar (encodeURIComponent)
- Endet eine Aufnahme von selbst (Headset getrennt, Anruf), bleibt die
  Sprachnachricht erhalten statt "zu kurz"
- Kamera-App nur auf iOS/iPadOS/Android; Touch-Laptops bekommen die Kamera
  im Browser statt eines Dateidialogs
- Knapp neben dem Chat abgelegte Dateien oeffnet der Browser nicht mehr
  selbst (Portal bliebe sonst stehen); Datei-Felder bleiben Ablagen
- Fehler beim Ausliefern kommt als JSON statt mit dem Dateityp
- Server liest hoechstens 4 Uploads gleichzeitig ein (Speicher), weitere
  warten; abgebrochene Wartende geben ihren Platz frei

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-26 14:01:17 +00:00
1 parent 35d98da977
commit 06e9a0fe8d
4 files changed
+57 -8

No files matched your search

+18 -1
View File
@@ -75,6 +75,9 @@
const extOf = (name) => ((/\.([A-Za-z0-9]{1,8})$/.exec(String(name || '')) || [])[1] || '').toLowerCase()
const withQuery = (url, query) => `${url}${url.includes('?') ? '&' : '?'}${query}`
const isTouch = () => Boolean(window.matchMedia && window.matchMedia('(pointer: coarse)').matches)
// Handy/Tablet mit eigener Kamera-App (iPadOS meldet sich als Mac mit Touch)
const isMobileOs = () => /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
function stamp() {
const d = new Date()
@@ -953,7 +956,7 @@
// Kamera: am Handy/Tablet die eingebaute Kamera-App, sonst die Kamera im Browser
function takePhoto() {
if (isTouch() || !(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
if (isMobileOs() || !(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
captureInput.click()
return
}
@@ -989,11 +992,21 @@
zone.classList.remove('chat-drop-active')
addFiles(event.dataTransfer.files)
}
// Knapp daneben losgelassen wuerde der Browser die Datei selbst oeffnen und das Portal verlassen.
// Andere Ablagen (z. B. Datei-Felder) bleiben unberuehrt.
const onWindowDrag = (event) => {
if (event.defaultPrevented || !hasFiles(event) || zone.contains(event.target)) return
if (event.target && event.target.closest && event.target.closest('input[type=file]')) return
event.preventDefault()
if (event.type === 'dragover') event.dataTransfer.dropEffect = 'none'
}
zone.classList.add('chat-dropzone')
zone.addEventListener('dragenter', onDragEnter)
zone.addEventListener('dragover', onDragOver)
zone.addEventListener('dragleave', onDragLeave)
zone.addEventListener('drop', onDrop)
window.addEventListener('dragover', onWindowDrag)
window.addEventListener('drop', onWindowDrag)
// ------------------------------------------------------ Sprachnachricht --
function paintLevels(levels) {
@@ -1087,6 +1100,8 @@
if (recording === rec) recording = null
composer.classList.remove('is-recording')
if (destroyed) return
// Endet die Aufnahme von selbst (Headset getrennt, Anruf, Zugriff entzogen), wurde keine Dauer gesetzt
if (!rec.durationMs) rec.durationMs = Date.now() - rec.startedAt
if (rec.then === 'keep') {
if (rec.durationMs < MIN_VOICE_MS || !rec.chunks.length) {
showNote('Die Aufnahme war zu kurz.')
@@ -1121,6 +1136,8 @@
zone.removeEventListener('dragover', onDragOver)
zone.removeEventListener('dragleave', onDragLeave)
zone.removeEventListener('drop', onDrop)
window.removeEventListener('dragover', onWindowDrag)
window.removeEventListener('drop', onWindowDrag)
zone.classList.remove('chat-dropzone', 'chat-drop-active')
root.replaceChildren()
}