"use client"; import { Headphones, Mic, MicOff, PhoneOff, Radio, Volume2, Waves, } from "lucide-react"; import { useEffect, useState } from "react"; import { useAmbient } from "@/components/ambient/ambient-context"; import { Equalizer } from "@/components/charts"; import { Button, GlassPanel, toast } from "@/components/primitives"; import { EmptyState, ErrorState, SectionHeader, SkeletonPanel, } from "@/components/shared"; import { GuildChannelPicker } from "@/components/shared/guild-picker"; import { VoiceStage } from "@/components/voice/voice-stage"; import { useMicTransmit, useSpeakers, useVoiceConnect, useVoiceDisconnect, useVoiceListen, useVoiceStatus, } from "@/hooks"; import type { Guild, VoiceStatus } from "@/lib/types"; import { useWebSocket } from "@/lib/ws/context"; export function VoiceView({ initialStatus, initialGuilds, }: { initialStatus?: VoiceStatus; initialGuilds?: Guild[]; }) { const ws = useWebSocket(); const { data: status, isLoading, error } = useVoiceStatus(initialStatus); const connect = useVoiceConnect(); const disconnect = useVoiceDisconnect(); const mic = useMicTransmit(ws); const listen = useVoiceListen(ws); const { speakers, subscribe } = useSpeakers(initialStatus?.activeSpeakers); const ambient = useAmbient(); const [guildId, setGuildId] = useState( initialStatus?.activeGuildId ?? initialGuilds?.[0]?.id ?? null, ); const [channelId, setChannelId] = useState( initialStatus?.activeChannelId ?? null, ); const [micOn, setMicOn] = useState(false); const [micVol, setMicVol] = useState(100); const [listenVol, setListenVol] = useState(75); useEffect(() => { const unsub = subscribe(ws); return unsub; }, [subscribe, ws]); useEffect(() => { if (status?.connected) ambient.set("signal", 0.55, "voice live"); else ambient.set("vermilion", 0.35, "voice idle"); }, [status?.connected, ambient]); if (error && !status) return ; if (!status && isLoading) return (
); const connected = status?.connected ?? false; const listenBars = Array.from(listen.levels.values()).slice(0, 32); const micBars = mic.micLevel ? Array.from({ length: 12 }, (_, i) => Math.max( 0.08, Math.min(1, mic.micLevel * (1 - i * 0.06) + (i % 3) * 0.05), ), ) : []; const onConnect = async () => { if (!guildId || !channelId) { toast({ title: "Pick a guild + channel", tone: "vermilion" }); return; } try { await connect.mutateAsync({ guildId, channelId }); toast({ title: "Connected to voice", tone: "signal" }); } catch (e) { toast({ title: "Connect failed", description: String(e), tone: "vermilion", }); } }; const onMic = async (on: boolean) => { try { await mic.mutateAsync(on); setMicOn(on); } catch (e) { toast({ title: "Mic error", description: String(e), tone: "vermilion" }); } }; return (
{ setGuildId(g); setChannelId(c); }} />
{connected ? ( ) : ( )}
{micOn && (
)} {listen.active && (
)}
{speakers.length} present } /> {speakers.length === 0 ? ( } title={connected ? "Silent right now" : "Not connected"} description={ connected ? "Speakers appear as they talk." : "Connect to a voice channel to see presence." } /> ) : ( <>
{speakers.map((sp) => ( {sp.username} ))}
)}
{(status?.connections ?? []).map((c) => (
{c.channelName} {new Date(c.connectedAt).toLocaleTimeString()}
))} {(status?.connections ?? []).length === 0 && (
No active links
)}
channel{" "} {status?.activeChannelName ?? "—"}
); }