import React, { useState, useRef } from 'react'; import { useVoiceStore } from '../../stores/voiceStore'; import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore'; import { getActiveRoom } from '../../hooks/useLiveKit'; import { wsSend } from '../../hooks/useWebSocket'; import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover'; import { ConnectionInfoPopover } from './ConnectionInfoPopover'; import { startScreenShare, stopScreenShare } from '../../utils/screenShare'; import { hasPermissionBit, PermissionBits } from '../../utils/permissions'; import { broadcastVoiceStatus } from '../../utils/voice'; /** * VoiceControls renders the voice status + button rows. * It has NO wrapper/card styling — the parent provides the container. */ export function VoiceControls() { const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId); const isCameraOn = useVoiceStore((s) => s.isCameraOn); const isScreenSharing = useVoiceStore((s) => s.isScreenSharing); const toggleCamera = useVoiceStore((s) => s.toggleCamera); const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled); const setRnnoiseEnabled = useVoiceStore((s) => s.setRnnoiseEnabled); const connectionError = useVoiceStore((s) => s.connectionError); const isLiveKitConnected = useVoiceStore((s) => s.isLiveKitConnected); const connectionQuality = useVoiceStore((s) => s.connectionQuality); const channels = useSpaceStore((s) => s.channels); const [showScreenShareSettings, setShowScreenShareSettings] = useState(false); const [showConnectionInfo, setShowConnectionInfo] = useState(false); const connectionBtnRef = useRef(null); const qualityBtnRef = useRef(null); const activeDmCall = useVoiceStore((s) => s.activeDmCall); const channelPerms = useSpaceStore((s) => currentVoiceChannelId ? s.channelPermissions.get(currentVoiceChannelId) : undefined); // In DM calls, all permissions are granted; in space channels, check SPEAK and STREAM const isDmCall = !!activeDmCall; const canSpeak = isDmCall || hasPermissionBit(channelPerms, PermissionBits.SPEAK); const canStream = isDmCall || hasPermissionBit(channelPerms, PermissionBits.STREAM); const voiceOrigin = currentVoiceChannelId ? getChannelOrigin(currentVoiceChannelId) : ''; if (!currentVoiceChannelId && !activeDmCall) return null; const channel = channels.find(c => c.id === currentVoiceChannelId); const channelName = channel?.name ?? (activeDmCall ? 'DM Call' : 'Voice Channel'); const handleCamera = async () => { const room = getActiveRoom(); if (!room) return; try { const willEnable = !isCameraOn; await room.localParticipant.setCameraEnabled(willEnable); toggleCamera(); broadcastVoiceStatus(); } catch (err) { console.error('[VoiceControls] Failed to toggle camera:', err); } }; const handleScreenShare = async () => { const room = getActiveRoom(); if (!room) return; try { if (!isScreenSharing) { const started = await startScreenShare(room); if (started) broadcastVoiceStatus(); } else { await stopScreenShare(room); broadcastVoiceStatus(); } } catch (err) { console.error('[VoiceControls] Failed to toggle screen share:', err); } }; const handleDisconnect = () => { const { activeDmCall } = useVoiceStore.getState(); if (activeDmCall) { wsSend({ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId }); // DM calls are home-only useVoiceStore.getState().setActiveDmCall(null); } else { wsSend({ type: 'voice_leave' }, voiceOrigin); useVoiceStore.getState().leaveVoice(); } }; const statusColor = connectionError ? 'text-txt-danger' : isLiveKitConnected ? 'text-status-online' : 'text-status-idle'; const statusBgColor = connectionError ? 'bg-accent-rose/20' : isLiveKitConnected ? 'bg-status-online/20' : 'bg-status-idle/20'; const qualityColor = connectionQuality === 'excellent' || connectionQuality === 'good' ? 'text-status-online' : connectionQuality === 'poor' ? 'text-status-idle' : connectionQuality === 'lost' ? 'text-txt-danger' : statusColor; // 'unknown' falls back to connection-state color const btnBase = 'flex-1 h-[34px] flex items-center justify-center rounded-[4px] transition-colors'; const btnDefaultStyle = 'bg-surface-base text-txt-tertiary hover:bg-surface-channel hover:text-txt-secondary'; return ( <> {/* Row 1: Signal icon + status text + disconnect */}
{connectionError ? 'Connection Failed' : isLiveKitConnected ? 'Voice Connected' : 'Connecting...'}
{connectionError ? connectionError : channelName}
{/* Connection Info Popover */} setShowConnectionInfo(false)} anchorRef={connectionBtnRef} />
{/* Row 2: Camera, Screen Share, Video Quality, Noise Suppression */}
{canSpeak && ( )} {canStream && ( )} {/* Video Quality */} {/* AI Noise Suppression (RNNoise) */} {/* Screen Share Settings Popover */} setShowScreenShareSettings(false)} anchorRef={qualityBtnRef} />
); }