fix(soundboard): read MANAGE_SPACE from the space bitfield
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s

The add-sound control was gated on channelPermissions, which carries the
per-channel bitfield; MANAGE_SPACE lives in the space-level one. The check
silently evaluated false for everybody, including owners, so the button never
rendered and there was no way to add a sound at all.

Also raise the clip cap to 2 MB — a 30-second clip at a high bitrate cleared
1 MB — and stop reporting every upload failure as 'too large', which sent
people to shrink a file that was not the problem.
This commit is contained in:
2026-08-31 13:54:00 -03:00
parent f5451e1b14
commit e89966435a
4 changed files with 13 additions and 5 deletions
@@ -12,7 +12,7 @@ interface SoundboardPopoverProps {
} }
/** Clips are short gags; anything larger is a music file in disguise. */ /** Clips are short gags; anything larger is a music file in disguise. */
const MAX_SOUND_BYTES = 1024 * 1024; const MAX_SOUND_BYTES = 2 * 1024 * 1024;
export function SoundboardPopover({ spaceId, canManage, onClose }: SoundboardPopoverProps) { export function SoundboardPopover({ spaceId, canManage, onClose }: SoundboardPopoverProps) {
const t = useT(); const t = useT();
@@ -66,7 +66,9 @@ export function SoundboardPopover({ spaceId, canManage, onClose }: SoundboardPop
const created = await api.soundboard.add(spaceId, name, filename); const created = await api.soundboard.add(spaceId, name, filename);
setSounds((prev) => [...prev, created]); setSounds((prev) => [...prev, created]);
} catch { } catch {
setError(t('soundboard.tooLarge')); // Distinct from the size check above: reporting every failure as "too
// large" sends people to shrink a file that was never the problem.
setError(t('soundboard.uploadFailed'));
} finally { } finally {
setUploading(false); setUploading(false);
if (fileRef.current) fileRef.current.value = ''; if (fileRef.current) fileRef.current.value = '';
@@ -39,6 +39,10 @@ export function VoiceControls() {
const activeDmCall = useVoiceStore((s) => s.activeDmCall); const activeDmCall = useVoiceStore((s) => s.activeDmCall);
const channelPerms = useSpaceStore((s) => currentVoiceChannelId ? s.channelPermissions.get(currentVoiceChannelId) : undefined); const channelPerms = useSpaceStore((s) => currentVoiceChannelId ? s.channelPermissions.get(currentVoiceChannelId) : undefined);
// MANAGE_SPACE lives in the space-level bitfield, not the per-channel one —
// reading it from channelPermissions silently yielded false for everyone.
const spacePerms = useSpaceStore((s) => currentVoiceSpaceId ? s.spacePermissions.get(currentVoiceSpaceId) : undefined);
const canManageSpace = hasPermissionBit(spacePerms, PermissionBits.MANAGE_SPACE);
// In DM calls, all permissions are granted; in space channels, check SPEAK and STREAM // In DM calls, all permissions are granted; in space channels, check SPEAK and STREAM
const isDmCall = !!activeDmCall; const isDmCall = !!activeDmCall;
@@ -128,7 +132,7 @@ export function VoiceControls() {
{showSoundboard && currentVoiceSpaceId && ( {showSoundboard && currentVoiceSpaceId && (
<SoundboardPopover <SoundboardPopover
spaceId={currentVoiceSpaceId} spaceId={currentVoiceSpaceId}
canManage={hasPermissionBit(channelPerms, PermissionBits.MANAGE_SPACE)} canManage={canManageSpace}
onClose={() => setShowSoundboard(false)} onClose={() => setShowSoundboard(false)}
/> />
)} )}
+2 -1
View File
@@ -41,7 +41,8 @@ export const en = {
'soundboard.remove': 'Remove', 'soundboard.remove': 'Remove',
'soundboard.namePrompt': 'Name for this sound', 'soundboard.namePrompt': 'Name for this sound',
'soundboard.joinFirst': 'Join a voice channel to use the soundboard.', 'soundboard.joinFirst': 'Join a voice channel to use the soundboard.',
'soundboard.tooLarge': 'Sound must be under 1 MB and a few seconds long.', 'soundboard.tooLarge': 'Sound must be under 2 MB and a few seconds long.',
'soundboard.uploadFailed': 'Could not upload that file. Try a different one.',
// Account menu (own name in the user bar) // Account menu (own name in the user bar)
'accountMenu.editProfile': 'Edit Profile', 'accountMenu.editProfile': 'Edit Profile',
+2 -1
View File
@@ -40,7 +40,8 @@ export const ptBR: Partial<Dictionary> = {
'soundboard.remove': 'Remover', 'soundboard.remove': 'Remover',
'soundboard.namePrompt': 'Nome deste som', 'soundboard.namePrompt': 'Nome deste som',
'soundboard.joinFirst': 'Entre num canal de voz para usar o soundboard.', 'soundboard.joinFirst': 'Entre num canal de voz para usar o soundboard.',
'soundboard.tooLarge': 'O som precisa ter menos de 1 MB e poucos segundos.', 'soundboard.tooLarge': 'O som precisa ter menos de 2 MB e poucos segundos.',
'soundboard.uploadFailed': 'Não foi possível enviar esse arquivo. Tente outro.',
// Menu da conta (próprio nome na barra de usuário) // Menu da conta (próprio nome na barra de usuário)
'accountMenu.editProfile': 'Editar perfil', 'accountMenu.editProfile': 'Editar perfil',