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:
@@ -0,0 +1,58 @@
|
||||
import { SoundboardClip, SoundboardClipInfo } from './types';
|
||||
|
||||
/** Parallels custom-emoji/PackImageReader, for a soundboard clip. */
|
||||
export class SoundboardClipReader {
|
||||
public readonly shortcode: string;
|
||||
|
||||
public readonly url: string;
|
||||
|
||||
private readonly clip: Omit<SoundboardClip, 'url'>;
|
||||
|
||||
constructor(shortcode: string, url: string, clip: Omit<SoundboardClip, 'url'>) {
|
||||
this.shortcode = shortcode;
|
||||
this.url = url;
|
||||
this.clip = clip;
|
||||
}
|
||||
|
||||
static fromClip(shortcode: string, clip: SoundboardClip): SoundboardClipReader | undefined {
|
||||
const { url } = clip;
|
||||
if (typeof url !== 'string' || !url.startsWith('mxc://')) return undefined;
|
||||
return new SoundboardClipReader(shortcode, url, clip);
|
||||
}
|
||||
|
||||
get body(): string | undefined {
|
||||
const { body } = this.clip;
|
||||
return typeof body === 'string' ? body : undefined;
|
||||
}
|
||||
|
||||
/** Display name — the clip body, falling back to the shortcode. */
|
||||
get name(): string {
|
||||
return this.body ?? this.shortcode;
|
||||
}
|
||||
|
||||
get emoji(): string | undefined {
|
||||
const { emoji } = this.clip;
|
||||
return typeof emoji === 'string' && emoji.length > 0 ? emoji : undefined;
|
||||
}
|
||||
|
||||
/** Per-clip volume 0–100; defaults to 100 when unset/invalid. */
|
||||
get volume(): number {
|
||||
const v = this.clip.volume;
|
||||
if (typeof v !== 'number' || Number.isNaN(v)) return 100;
|
||||
return Math.min(100, Math.max(0, v));
|
||||
}
|
||||
|
||||
get info(): SoundboardClipInfo | undefined {
|
||||
return this.clip.info;
|
||||
}
|
||||
|
||||
get content(): SoundboardClip {
|
||||
return {
|
||||
url: this.url,
|
||||
body: this.clip.body,
|
||||
emoji: this.clip.emoji,
|
||||
volume: this.clip.volume,
|
||||
info: this.clip.info,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user