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,160 @@
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import {
|
||||
getGlobalSoundboardPacks,
|
||||
getRoomSoundboardPack,
|
||||
getRoomSoundboardPacks,
|
||||
getUserSoundboardPack,
|
||||
SoundboardPack,
|
||||
} from '../plugins/soundboard';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useAccountDataCallback } from './useAccountDataCallback';
|
||||
import { useStateEventCallback } from './useStateEventCallback';
|
||||
|
||||
// Parallels hooks/useImagePacks.ts (custom emoji). Same aggregation shape.
|
||||
|
||||
export const useUserSoundboardPack = (): SoundboardPack | undefined => {
|
||||
const mx = useMatrixClient();
|
||||
const [userPack, setUserPack] = useState(() => getUserSoundboardPack(mx));
|
||||
|
||||
useAccountDataCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (mEvent.getType() === AccountDataEvent.LotusSoundboard) {
|
||||
setUserPack(getUserSoundboardPack(mx));
|
||||
}
|
||||
},
|
||||
[mx],
|
||||
),
|
||||
);
|
||||
|
||||
return userPack;
|
||||
};
|
||||
|
||||
export const useGlobalSoundboardPacks = (): SoundboardPack[] => {
|
||||
const mx = useMatrixClient();
|
||||
const [globalPacks, setGlobalPacks] = useState(() => getGlobalSoundboardPacks(mx));
|
||||
|
||||
useAccountDataCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (mEvent.getType() === AccountDataEvent.LotusSoundboardRooms) {
|
||||
setGlobalPacks(getGlobalSoundboardPacks(mx));
|
||||
}
|
||||
},
|
||||
[mx],
|
||||
),
|
||||
);
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
const roomId = mEvent.getRoomId();
|
||||
const stateKey = mEvent.getStateKey();
|
||||
if (
|
||||
mEvent.getType() === StateEvent.LotusSoundboardRoom &&
|
||||
roomId &&
|
||||
typeof stateKey === 'string'
|
||||
) {
|
||||
const isGlobal = !!globalPacks.find(
|
||||
(pack) =>
|
||||
pack.address && pack.address.roomId === roomId && pack.address.stateKey === stateKey,
|
||||
);
|
||||
if (isGlobal) setGlobalPacks(getGlobalSoundboardPacks(mx));
|
||||
}
|
||||
},
|
||||
[mx, globalPacks],
|
||||
),
|
||||
);
|
||||
|
||||
return globalPacks;
|
||||
};
|
||||
|
||||
export const useRoomSoundboardPack = (room: Room, stateKey: string): SoundboardPack | undefined => {
|
||||
const mx = useMatrixClient();
|
||||
const [roomPack, setRoomPack] = useState(() => getRoomSoundboardPack(room, stateKey));
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (
|
||||
mEvent.getRoomId() === room.roomId &&
|
||||
mEvent.getType() === StateEvent.LotusSoundboardRoom &&
|
||||
mEvent.getStateKey() === stateKey
|
||||
) {
|
||||
setRoomPack(getRoomSoundboardPack(room, stateKey));
|
||||
}
|
||||
},
|
||||
[room, stateKey],
|
||||
),
|
||||
);
|
||||
|
||||
return roomPack;
|
||||
};
|
||||
|
||||
export const useRoomSoundboardPacks = (room: Room): SoundboardPack[] => {
|
||||
const mx = useMatrixClient();
|
||||
const [roomPacks, setRoomPacks] = useState(() => getRoomSoundboardPacks(room));
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (
|
||||
mEvent.getRoomId() === room.roomId &&
|
||||
mEvent.getType() === StateEvent.LotusSoundboardRoom
|
||||
) {
|
||||
setRoomPacks(getRoomSoundboardPacks(room));
|
||||
}
|
||||
},
|
||||
[room],
|
||||
),
|
||||
);
|
||||
|
||||
return roomPacks;
|
||||
};
|
||||
|
||||
export const useRoomsSoundboardPacks = (rooms: Room[]): SoundboardPack[] => {
|
||||
const mx = useMatrixClient();
|
||||
const [roomPacks, setRoomPacks] = useState(() => rooms.flatMap(getRoomSoundboardPacks));
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (
|
||||
rooms.find((room) => room.roomId === mEvent.getRoomId()) &&
|
||||
mEvent.getType() === StateEvent.LotusSoundboardRoom
|
||||
) {
|
||||
setRoomPacks(rooms.flatMap(getRoomSoundboardPacks));
|
||||
}
|
||||
},
|
||||
[rooms],
|
||||
),
|
||||
);
|
||||
|
||||
return roomPacks;
|
||||
};
|
||||
|
||||
/** User ∪ global ∪ room packs, deduped by id, keeping only packs with clips. */
|
||||
export const useRelevantSoundboardPacks = (rooms: Room[]): SoundboardPack[] => {
|
||||
const userPack = useUserSoundboardPack();
|
||||
const globalPacks = useGlobalSoundboardPacks();
|
||||
const roomsPacks = useRoomsSoundboardPacks(rooms);
|
||||
|
||||
return useMemo(() => {
|
||||
const packs = userPack ? [userPack] : [];
|
||||
const globalPackIds = new Set(globalPacks.map((pack) => pack.id));
|
||||
const relPacks = packs.concat(
|
||||
globalPacks,
|
||||
roomsPacks.filter((pack) => !globalPackIds.has(pack.id)),
|
||||
);
|
||||
return relPacks.filter((pack) => pack.getClips().length > 0);
|
||||
}, [userPack, globalPacks, roomsPacks]);
|
||||
};
|
||||
Reference in New Issue
Block a user