feat(soundboard): shared room/space packs (like emoji/stickers), grid picker, management
Soundboard v2 — a near-parallel of the custom-emoji image-pack system for in-call audio clips. - Data model: 3-tier packs mirroring MSC2545 — room/space pack (state event io.lotus.soundboard, inherited by child rooms via parent-space aggregation), global refs (io.lotus.soundboard_rooms), and the personal pack (io.lotus.soundboard account data; the v1 flat-list content is migrated to the pack shape on read). New plugins/soundboard/ (readers, SoundboardPack, utils) + hooks/useSoundboardPacks (useRelevantSoundboardPacks = user U global U room, deduped). Unit-tested (migration + slug). - Management: reusable SoundboardPackEditor (name + emoji + per-clip volume + delete + upload + batched save), power-level-gated for room packs like emoji packs; a Soundboard page wired into Room + Space settings. - In-call: CallSoundboard rewritten as a Discord-style grid grouped by pack (emoji + name tiles), sourcing room+parent-space U personal clips; a Manage toggle embeds the editors; per-clip volume x master volume on playback. - Spam guard: host gates on a playing key (fork enforces one clip at a time). - Control bar: Mute-Screenshare moved next to the Screenshare button. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,9 @@
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { downloadMedia, mxcUrlToHttp } from './matrix';
|
||||
|
||||
/**
|
||||
* [P5-15] A user-uploaded soundboard clip. Stored (as a list) in the
|
||||
* `io.lotus.soundboard` account data event, so clips sync across a user's
|
||||
* devices exactly like custom emoji / sticker packs.
|
||||
*/
|
||||
export type SoundboardClip = {
|
||||
/** Stable local id (not shared with peers). */
|
||||
id: string;
|
||||
/** Display name / shortcode shown on the tile. */
|
||||
name: string;
|
||||
/** mxc:// URI of the uploaded audio. */
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export type SoundboardContent = {
|
||||
clips?: SoundboardClip[];
|
||||
};
|
||||
// [P5-15 v2] Shared media helpers for the soundboard. Clip storage/metadata now
|
||||
// lives in the soundboard pack plugin (plugins/soundboard); this module only
|
||||
// handles resolving an mxc clip for playback + local preview.
|
||||
|
||||
export const SOUNDBOARD_NAME_MAX = 24;
|
||||
/** Keep clips short: they publish to every peer and hold a track open. */
|
||||
@@ -53,20 +37,19 @@ export const resolveClipObjectUrl = async (mx: MatrixClient, mxcUrl: string): Pr
|
||||
* Play a resolved clip locally so the person who pressed it gets immediate
|
||||
* feedback — LiveKit doesn't loop a participant's own published track back to
|
||||
* them, so without this the presser would hear nothing. `volume` is 0–1.
|
||||
* Returns the audio element so callers can track when it ends (or undefined if
|
||||
* playback couldn't start).
|
||||
*/
|
||||
export const playClipLocally = (objectUrl: string, volume: number): void => {
|
||||
export const playClipLocally = (
|
||||
objectUrl: string,
|
||||
volume: number,
|
||||
): HTMLAudioElement | undefined => {
|
||||
try {
|
||||
const audio = new Audio(objectUrl);
|
||||
audio.volume = Math.max(0, Math.min(1, volume));
|
||||
audio.play().catch(() => undefined);
|
||||
return audio;
|
||||
} catch {
|
||||
/* best effort */
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const readSoundboardClips = (mx: MatrixClient): SoundboardClip[] => {
|
||||
const content = mx.getAccountData('io.lotus.soundboard' as never)?.getContent() as
|
||||
| SoundboardContent
|
||||
| undefined;
|
||||
return Array.isArray(content?.clips) ? content.clips : [];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user