import React, { MouseEventHandler, useCallback, useRef, useState } from 'react'; import { Box, Chip, Icon, IconButton, Icons, Menu, PopOut, RectCords, Spinner, Text, Tooltip, TooltipProvider, color, config, } from 'folds'; import FocusTrap from 'focus-trap-react'; import { CallEmbed } from '../../plugins/call'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useSoundboard } from '../../hooks/useSoundboard'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { stopPropagation } from '../../utils/keyboard'; import { SOUNDBOARD_ACCEPT, SOUNDBOARD_MAX_CLIPS, playClipLocally, resolveClipObjectUrl, } from '../../utils/soundboardClips'; type CallSoundboardProps = { callEmbed: CallEmbed; }; /** * [P5-15] In-call soundboard: trigger user-uploaded clips into the call. Each * clip is published to peers as a separate track by the EC fork * (`io.lotus.inject_audio`) and also played locally for the presser's feedback. * Clips are uploadable/managed here and synced across devices via the * `io.lotus.soundboard` account data (like custom emoji/sticker packs). */ export function CallSoundboard({ callEmbed }: CallSoundboardProps) { const mx = useMatrixClient(); const { clips, addClip, removeClip } = useSoundboard(); const [soundboardVolume] = useSetting(settingsAtom, 'soundboardVolume'); const [cords, setCords] = useState(); const [busyId, setBusyId] = useState(); const [uploading, setUploading] = useState(false); const [error, setError] = useState(); const fileInputRef = useRef(null); const volume = Math.max(0, Math.min(1, soundboardVolume / 100)); const handleOpen: MouseEventHandler = (evt) => { setError(undefined); setCords(evt.currentTarget.getBoundingClientRect()); }; const handlePlay = useCallback( async (id: string, mxc: string) => { setBusyId(id); setError(undefined); try { const objectUrl = await resolveClipObjectUrl(mx, mxc); callEmbed.control.injectAudio(objectUrl, volume); playClipLocally(objectUrl, volume); } catch { setError('Could not play that clip.'); } finally { setBusyId(undefined); } }, [mx, callEmbed, volume], ); const handleFile = useCallback( async (file: File | undefined) => { if (!file) return; setUploading(true); setError(undefined); try { await addClip(file); } catch (e) { setError(e instanceof Error ? e.message : 'Upload failed.'); } finally { setUploading(false); } }, [addClip], ); return ( <> { handleFile(e.target.files?.[0]); e.target.value = ''; }} /> setCords(undefined), clickOutsideDeactivates: true, escapeDeactivates: stopPropagation, }} > Soundboard = SOUNDBOARD_MAX_CLIPS} onClick={() => fileInputRef.current?.click()} before={ uploading ? : } > Upload {clips.length === 0 ? ( No clips yet. Upload a short audio clip (max 1 MB) to play it into the call. Clips sync across your devices. ) : ( {clips.map((clip) => ( handlePlay(clip.id, clip.url)} before={ busyId === clip.id ? ( ) : ( ) } after={ { e.stopPropagation(); removeClip(clip.id); }} /> } > {clip.name} ))} )} {error && ( {error} )} } > Soundboard } > {(triggerRef) => ( )} ); }