From a66db8d70292aa4e746b069e35008d27c1d366ef Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 14 Aug 2026 11:43:07 +0700 Subject: [PATCH] fix(voice): live connection state instead of static SSR snapshot - VoiceView now reads connected/activeChannelName from useVoiceStatus (SWR live, invalidated by connect/disconnect) instead of initialStatus - Seed useSpeakers from live status.activeSpeakers - Add 4s refreshInterval to useVoiceStatus so state converges (tsc clean, next build green) --- .../src/app/(dashboard)/voice/view.tsx | 19 ++++++++++--------- services/frontend/src/hooks/use-voice.ts | 1 + 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/services/frontend/src/app/(dashboard)/voice/view.tsx b/services/frontend/src/app/(dashboard)/voice/view.tsx index f7ccdb1..abfd24b 100644 --- a/services/frontend/src/app/(dashboard)/voice/view.tsx +++ b/services/frontend/src/app/(dashboard)/voice/view.tsx @@ -1,7 +1,7 @@ "use client"; import { Headphones, Loader2, Radio, RadioOff } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { SignalField } from "@/components/three"; import { WebGLGuard } from "@/components/three/webgl-guard"; import { StaticFallback } from "@/components/three/static-fallback"; @@ -21,6 +21,7 @@ import { useVoiceConnect, useVoiceDisconnect, useVoiceListen, + useVoiceStatus, } from "@/hooks"; import type { VoiceStatus } from "@/lib/types"; import { useWebSocket } from "@/lib/ws/context"; @@ -30,11 +31,11 @@ export default function VoiceView({ initialStatus }: { initialStatus?: VoiceStat const [selectedGuild, setSelectedGuild] = useState(""); const [selectedChannel, setSelectedChannel] = useState(""); - const initialSpeakers = useMemo( - () => initialStatus?.activeSpeakers ?? [], - [initialStatus], - ); - const { speakers, subscribe } = useSpeakers(initialSpeakers); + // Live connection status — SWR revalidates on connect/disconnect (the + // useVoiceConnect/Disconnect actions invalidate the "voice-status" key), + // so this reflects real-time state instead of the static SSR snapshot. + const { data: status } = useVoiceStatus(initialStatus); + const { speakers, subscribe } = useSpeakers(status?.activeSpeakers ?? []); const { data: guilds = [] } = useGuilds(); const { data: voiceChannels = [] } = useVoiceChannels(selectedGuild); const connect = useVoiceConnect(); @@ -47,7 +48,7 @@ export default function VoiceView({ initialStatus }: { initialStatus?: VoiceStat }, [subscribe, ws]); const active = speakers.filter((s) => s.speaking); - const connected = initialStatus?.connected ?? false; + const connected = status?.connected ?? false; const handleGuildChange = (e: React.ChangeEvent) => { const g = e.target.value; @@ -63,10 +64,10 @@ export default function VoiceView({ initialStatus }: { initialStatus?: VoiceStat {connected ? "Connected" : "Disconnected"} - {connected && initialStatus?.activeChannelName && ( + {connected && status?.activeChannelName && ( - {initialStatus.activeChannelName} + {status.activeChannelName} )} diff --git a/services/frontend/src/hooks/use-voice.ts b/services/frontend/src/hooks/use-voice.ts index d0ebbcb..44101bc 100644 --- a/services/frontend/src/hooks/use-voice.ts +++ b/services/frontend/src/hooks/use-voice.ts @@ -24,6 +24,7 @@ export function useVoiceStatus(initialData?: VoiceStatus) { return useSWR(STATUS_KEY, () => voiceApi.getStatus(), { shouldRetryOnError: false, fallbackData: initialData, + refreshInterval: 4000, }); }