fix(soundboard): upload cap counts staged clips once and per batch
The guard double-counted staged uploads and read a stale count for every file in a batch, so a 60-file drop bypassed the cap while a nearly-full pack refused early. Partition the batch with a running count. Fixes #31 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -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<T>(
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user