feat: soundboard, account menu, and call timer
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
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
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
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Soundboard: the trigger travels over the WebSocket and every client in the call plays the clip locally, instead of mixing it into the presser's microphone or publishing a LiveKit track. No upstream bandwidth, no media stack changes, and the clip is not degraded by voice processing. Fan-out uses a new sendToRoomParticipants rather than sendToRoom: the latter broadcasts a space room to the whole space, which is right for the presence the sidebar shows and wrong for anything audible. The cooldown is enforced server-side — a client-side one only slows down people not trying to abuse it, and a soundboard is the easiest thing here to turn into a weapon. Playing is open to anyone in the call; deciding what the buttons are needs MANAGE_SPACE. Account menu: the name in the user bar had cursor-pointer and no handler, so the interface was already promising a click that did nothing. Offers profile, status and copy-id — not the Clips or account switching the reference design shows, which would be dead UI here. Call timer: startedAt comes from the server, so a late joiner sees the call's age rather than their own arrival. Empty space rooms are destroyed already, which is what makes the next call start from zero — no reset logic needed.
This commit is contained in:
@@ -75,6 +75,15 @@ import type {
|
||||
} from '@backspace/shared';
|
||||
import type { AuditEvent } from '@backspace/shared/src/audit.js';
|
||||
|
||||
export interface SoundboardSound {
|
||||
id: string;
|
||||
spaceId: string;
|
||||
name: string;
|
||||
filename: string;
|
||||
uploaderId: string | null;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface StatsLeader {
|
||||
userId: string;
|
||||
username: string;
|
||||
@@ -302,6 +311,12 @@ export class BackspaceApiClient {
|
||||
removeFavorite: (id: string) => Promise<void>;
|
||||
};
|
||||
|
||||
readonly soundboard: {
|
||||
list: (spaceId: string) => Promise<{ sounds: SoundboardSound[] }>;
|
||||
add: (spaceId: string, name: string, filename: string) => Promise<SoundboardSound>;
|
||||
remove: (soundId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
readonly stats: {
|
||||
space: (spaceId: string, days: number) => Promise<SpaceStats>;
|
||||
};
|
||||
@@ -724,6 +739,13 @@ export class BackspaceApiClient {
|
||||
},
|
||||
};
|
||||
|
||||
this.soundboard = {
|
||||
list: (spaceId: string) => request<{ sounds: SoundboardSound[] }>('GET', `/spaces/${spaceId}/sounds`),
|
||||
add: (spaceId: string, name: string, filename: string) =>
|
||||
request<SoundboardSound>('POST', `/spaces/${spaceId}/sounds`, { name, filename }),
|
||||
remove: (soundId: string) => request<void>('DELETE', `/sounds/${soundId}`),
|
||||
};
|
||||
|
||||
this.stats = {
|
||||
space: (spaceId: string, days: number) =>
|
||||
request<SpaceStats>('GET', `/spaces/${spaceId}/stats?days=${days}`),
|
||||
|
||||
Reference in New Issue
Block a user