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 { useUIStore } from '../../stores/uiStore';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { useInstanceStore } from '../../stores/instanceStore';
|
||||
import { VoiceChannel } from '../voice/VoiceChannel';
|
||||
import { AccountMenu } from './AccountMenu';
|
||||
import { VoiceControls } from '../voice/VoiceControls';
|
||||
import { useVoiceStore } from '../../stores/voiceStore';
|
||||
import { ProfileAvatar } from '../ui/ProfileAvatar';
|
||||
@@ -844,6 +845,7 @@ function UserAreaPanel({
|
||||
onDeafenToggle: () => void;
|
||||
onSettingsClick: (tab?: string) => void;
|
||||
}) {
|
||||
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
|
||||
const [openPanel, setOpenPanel] = useState<'input' | 'output' | null>(null);
|
||||
const inputDeviceId = useVoiceStore((s) => s.inputDeviceId);
|
||||
const outputDeviceId = useVoiceStore((s) => s.outputDeviceId);
|
||||
@@ -1132,14 +1134,27 @@ function UserAreaPanel({
|
||||
)}
|
||||
|
||||
{/* User area bar */}
|
||||
<div className="h-[52px] px-2 flex items-center select-none">
|
||||
{/* Avatar + name */}
|
||||
<div className="p-1 hover:bg-interactive-hover rounded-[4px] flex items-center gap-2 flex-1 min-w-0 cursor-pointer transition-colors group">
|
||||
<div className="relative h-[52px] px-2 flex items-center select-none">
|
||||
{accountMenuOpen && (
|
||||
<AccountMenu
|
||||
onClose={() => setAccountMenuOpen(false)}
|
||||
onEditProfile={() => onSettingsClick('account')}
|
||||
/>
|
||||
)}
|
||||
{/* Avatar + name. The avatar opens the profile card (ProfileAvatar);
|
||||
the name opens the account menu — which is what the cursor here has
|
||||
been promising all along without anything happening. */}
|
||||
<div className="p-1 hover:bg-interactive-hover rounded-[4px] flex items-center gap-2 flex-1 min-w-0 transition-colors group">
|
||||
<ProfileAvatar src={user.avatar} name={user.displayName ?? user.username} size={34} status={user.status} user={user} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<button
|
||||
onClick={() => setAccountMenuOpen((v) => !v)}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={accountMenuOpen}
|
||||
className="flex-1 min-w-0 text-left cursor-pointer"
|
||||
>
|
||||
<div className="text-[13.5px] font-semibold text-txt-primary truncate leading-tight">{user.displayName ?? user.username}</div>
|
||||
<div className="text-[11px] text-txt-tertiary truncate leading-tight group-hover:text-txt-secondary">@{user.username}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
|
||||
Reference in New Issue
Block a user