f694301ca7
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
92 lines
2.8 KiB
TypeScript
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>
|
|
);
|
|
}
|