diff --git a/frontend/src/components/UploadQueue.tsx b/frontend/src/components/UploadQueue.tsx index b69e071..8950919 100644 --- a/frontend/src/components/UploadQueue.tsx +++ b/frontend/src/components/UploadQueue.tsx @@ -1,3 +1,5 @@ +import { useEffect, useRef } from 'react' + interface UploadItem { id: string name: string @@ -13,12 +15,68 @@ interface Props { export type { UploadItem } +/** + * Wie lange eine fertige Meldung stehen bleibt. + * + * Lang genug, um sie im Vorbeigehen zu lesen, kurz genug, dass beim Hochladen + * vieler Dateien nicht der halbe Bildschirm zuwächst. + */ +const dismissAfter = 4000 + +/** + * So viele Meldungen zeigen wir gleichzeitig, der Rest wird gezählt. + * + * Bei fünfzig Dateien auf einmal wäre die Liste sonst länger als das Fenster – + * und das gilt schon während des Hochladens, nicht erst danach. + */ +const maxVisible = 4 + export default function UploadQueue({ items, onDismiss }: Props) { + // Der Aufrufer gibt bei jedem Rendern eine neue Funktion herein. Über eine + // Referenz gelesen, muss der Effekt darauf nicht erneut laufen – sonst + // begännen die Fristen bei jedem Fortschrittsschritt von vorn. + const dismiss = useRef(onDismiss) + dismiss.current = onDismiss + + const timers = useRef(new Map>()) + + useEffect(() => { + for (const item of items) { + // Fehler bleiben stehen. Sie sind das Einzige, was hier eine Auskunft + // trägt, die man sonst nirgends wiederfindet – die verschwindet nicht + // von selbst. + if (item.status !== 'done') continue + if (timers.current.has(item.id)) continue + timers.current.set(item.id, setTimeout(() => { + timers.current.delete(item.id) + dismiss.current(item.id) + }, dismissAfter)) + } + + // Aufräumen, wenn eine Meldung von Hand weggeklickt wurde: Ihre Frist läuft + // sonst weiter und rührte später an eine Kennung, die es nicht mehr gibt. + const live = new Set(items.map(i => i.id)) + for (const [id, timer] of timers.current) { + if (!live.has(id)) { clearTimeout(timer); timers.current.delete(id) } + } + }, [items]) + + // Beim Verlassen der Seite alle Fristen abräumen. + const all = timers.current + useEffect(() => () => { for (const t of all.values()) clearTimeout(t) }, [all]) + if (items.length === 0) return null + // Fehler zuerst: Wenn mehr Meldungen anstehen als Platz ist, darf gerade die + // eine gescheiterte Datei nicht diejenige sein, die unter den Tisch fällt. + const failed = items.filter(i => i.status === 'error') + const rest = items.filter(i => i.status !== 'error') + const visible = [...failed, ...rest].slice(0, maxVisible) + const hidden = items.length - visible.length + return (
- {items.map(item => ( + {visible.map(item => (
))} + + {hidden > 0 && ( +
+ +{hidden} more +
+ )}
) }