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:
@@ -7,6 +7,7 @@ import { getActiveRoom } from '../../hooks/useLiveKit';
|
||||
import { wsSend } from '../../hooks/useWebSocket';
|
||||
import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover';
|
||||
import { ConnectionInfoPopover } from './ConnectionInfoPopover';
|
||||
import { SoundboardPopover } from './SoundboardPopover';
|
||||
import { startScreenShare, stopScreenShare } from '../../utils/screenShare';
|
||||
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
||||
import { broadcastVoiceStatus } from '../../utils/voice';
|
||||
@@ -22,6 +23,7 @@ export function VoiceControls() {
|
||||
const currentVoiceChannelName = useVoiceStore((s) => s.currentVoiceChannelName);
|
||||
const setCurrentChannel = useChatStore((s) => s.setCurrentChannel);
|
||||
const navigate = useNavigate();
|
||||
const [showSoundboard, setShowSoundboard] = useState(false);
|
||||
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
||||
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
||||
const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled);
|
||||
@@ -119,6 +121,19 @@ export function VoiceControls() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Zero-height anchor: the component returns a fragment, so without a
|
||||
positioned ancestor the popover would resolve against whatever
|
||||
happened to be relative further up the sidebar. */}
|
||||
<div className="relative">
|
||||
{showSoundboard && currentVoiceSpaceId && (
|
||||
<SoundboardPopover
|
||||
spaceId={currentVoiceSpaceId}
|
||||
canManage={hasPermissionBit(channelPerms, PermissionBits.MANAGE_SPACE)}
|
||||
onClose={() => setShowSoundboard(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 1: Signal icon + status text + disconnect */}
|
||||
<div className="relative flex items-center gap-2 px-3 pt-3 pb-1">
|
||||
<button
|
||||
@@ -216,6 +231,23 @@ export function VoiceControls() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Soundboard — space calls only: clips belong to a space. */}
|
||||
{currentVoiceSpaceId && (
|
||||
<button
|
||||
onClick={() => setShowSoundboard((v) => !v)}
|
||||
className={`${btnBase} ${
|
||||
showSoundboard
|
||||
? 'bg-surface-base text-accent-primary hover:bg-surface-channel'
|
||||
: btnDefaultStyle
|
||||
}`}
|
||||
title="Soundboard"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6Z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Video Quality */}
|
||||
<button
|
||||
ref={qualityBtnRef}
|
||||
|
||||
Reference in New Issue
Block a user