Files
dumpsterChat/web/src/components/VoicePanel.tsx
T
hobokenchicken f694301ca7 Phase 2 frontend: voice store, VoiceChannel, VoicePanel, VoiceControls
Frontend:
- stores/voice.ts: Zustand store for voice state (join, leave, mute, deafen, video, screen share)
- VoiceChannel.tsx: sidebar item showing 🔊 channel name with participant count
- VoicePanel.tsx: box-drawn frame with participant list and controls
- VoiceControls.tsx: [m] mute, [d] deafen, [cam] video, [share] screen, [x] disconnect
- ChannelList.tsx: integrated VoiceChannel for voice-type channels
- Layout.tsx: added VoicePanel above chat area

Deps:
- Added livekit-client, @livekit/components-react
- NODE_ENV fix for dev deps installation
2026-06-28 16:26:19 -04:00

92 lines
2.8 KiB
TypeScript

import { useVoiceStore } from '../stores/voice.ts';
import { VoiceControls } from './VoiceControls.tsx';
export function VoicePanel() {
const isConnected = useVoiceStore((state) => state.isConnected);
const currentRoom = useVoiceStore((state) => state.currentRoom);
const currentRoomName = useVoiceStore((state) => state.currentRoomName);
const participants = useVoiceStore((state) => state.participants);
const error = useVoiceStore((state) => state.error);
if (!currentRoom) return null;
return (
<div className="bg-gb-bg-s border border-gb-bg-t rounded-sm font-mono text-sm mb-2">
{/* Box-drawn header */}
<div className="px-3 py-1.5 border-b border-gb-bg-t flex items-center gap-2">
<span className="text-gb-fg-f">
<span className="text-gb-orange font-bold">VOICE</span>
{'─'.repeat(Math.max(0, 20 - (currentRoomName?.length ?? 0)))}
</span>
<span className="text-gb-fg-s truncate flex-1">
{currentRoomName || currentRoom}
</span>
<span
className={`text-xxs ${
isConnected ? 'text-gb-green' : 'text-gb-fg-f'
}`}
>
{isConnected ? '● LIVE' : '◐ connecting...'}
</span>
</div>
{/* Error display */}
{error && (
<div className="px-3 py-1 text-gb-red text-xxs border-b border-gb-bg-t">
! {error}
</div>
)}
{/* Participants */}
<div className="px-3 py-1.5 border-b border-gb-bg-t">
<div className="text-xxs text-gb-fg-f mb-1">
participants
</div>
{participants.length === 0 && (
<div className="text-xxs text-gb-fg-f">[empty channel]</div>
)}
{participants.map((p) => (
<div
key={p.identity}
className="flex items-center gap-2 py-0.5 text-xs"
>
<span
className={
p.isSpeaking
? 'text-gb-green'
: p.isMuted
? 'text-gb-red'
: 'text-gb-fg-s'
}
>
{p.isMuted ? '○' : '●'}
</span>
<span
className={p.isSpeaking ? 'text-gb-green' : 'text-gb-fg-s'}
>
{p.username}
</span>
{p.isMuted && (
<span className="text-xxs text-gb-red">[muted]</span>
)}
{p.isSpeaking && (
<span className="text-xxs text-gb-green">[speaking]</span>
)}
</div>
))}
</div>
{/* Controls */}
<div className="px-3 py-1.5">
<VoiceControls />
</div>
{/* Box-drawn footer */}
<div className="px-3 pb-1 text-xxs text-gb-fg-f">
{'─'.repeat(40)}
</div>
</div>
);
}