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
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user