diff --git a/src/app/components/soundboard-pack-view/SoundboardPackEditor.tsx b/src/app/components/soundboard-pack-view/SoundboardPackEditor.tsx index 38916e2ac..14fd53e0d 100644 --- a/src/app/components/soundboard-pack-view/SoundboardPackEditor.tsx +++ b/src/app/components/soundboard-pack-view/SoundboardPackEditor.tsx @@ -73,6 +73,32 @@ const formatClipSeconds = (seconds: number): string => { return `${m}:${s.toString().padStart(2, '0')}`; }; +/** + * [Gitea #31] Pure running-count cap check for `handleFiles`: given how many + * clips already exist (staged uploads included) before this batch starts, + * decide which of the batch's files fit under `max`. Kept pure/exported so the + * "running count, not a stale double-counted closure value" logic can be unit + * tested without a DOM/MatrixClient. + */ +export function acceptClips( + currentCount: number, + files: readonly T[], + max: number, +): { accepted: T[]; rejected: T[] } { + const accepted: T[] = []; + const rejected: T[] = []; + let count = currentCount; + files.forEach((file) => { + if (count >= max) { + rejected.push(file); + } else { + accepted.push(file); + count += 1; + } + }); + return { accepted, rejected }; +} + type ClipDraft = { url: string; body: string; @@ -186,11 +212,19 @@ export function SoundboardPackEditor({ pack, canEdit, onUpdate }: SoundboardPack ...existing.map((c) => c.shortcode), ...uploads.map((u) => u.shortcode), ]); - for (let i = 0; i < files.length; i += 1) { - const file = files[i]; - if (clipCount + uploads.length >= SOUNDBOARD_MAX_CLIPS) { - throw new Error(`Soundboard is full (max ${SOUNDBOARD_MAX_CLIPS} clips).`); - } + // [Gitea #31] `clipCount` already includes staged `uploads`, so don't + // add `uploads.length` again here (double-counting). And since + // `setUploads` inside the loop doesn't update this closure's + // `clipCount`, track the running total in a local variable that starts + // from the real current total instead of re-reading a stale value for + // every file in the batch. + const { accepted, rejected } = acceptClips( + clipCount, + Array.from(files), + SOUNDBOARD_MAX_CLIPS, + ); + for (let i = 0; i < accepted.length; i += 1) { + const file = accepted[i]; if (file.size > SOUNDBOARD_MAX_CLIP_BYTES) { throw new Error(`"${file.name}" is too large (max 1 MB).`); } @@ -215,6 +249,9 @@ export function SoundboardPackEditor({ pack, canEdit, onUpdate }: SoundboardPack }, ]); } + if (rejected.length > 0) { + throw new Error(`Soundboard is full (max ${SOUNDBOARD_MAX_CLIPS} clips).`); + } } catch (e) { setError(e instanceof Error ? e.message : 'Upload failed.'); } finally {