diff --git a/src/frontend/src/features/rooms/livekit/hooks/useSyncLiveKitMetadata.ts b/src/frontend/src/features/rooms/livekit/hooks/useSyncLiveKitMetadata.ts new file mode 100644 index 00000000..154af313 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/hooks/useSyncLiveKitMetadata.ts @@ -0,0 +1,76 @@ +// features/rooms/hooks/useSyncLiveKitMetadata.ts + +import { useEffect } from 'react' +import { RoomEvent } from 'livekit-client' +import { queryClient } from '@/api/queryClient' +import { keys } from '@/api/queryKeys' +import type { ApiRoom, RoomConfiguration } from '@/features/rooms/api/ApiRoom' +import { useRoomContext } from '@livekit/components-react' +import { useRoomData } from './useRoomData' + +/** + * Shape of the LiveKit room metadata blob pushed by the backend. + * Matches RoomManagement.update_metadata → {"configuration": room.configuration} + */ +type RoomLiveKitMetadata = { + configuration?: RoomConfiguration +} + +const parseMetadata = (raw: string | undefined): RoomLiveKitMetadata | null => { + if (!raw) return null + try { + return JSON.parse(raw) as RoomLiveKitMetadata + } catch { + console.warn('useSyncLiveKitMetadata: failed to parse room metadata') + return null + } +} + +/** + * Sync LiveKit room metadata into the React Query cache. + * + * The backend pushes room configuration into LiveKit's room metadata + * whenever it changes. This hook listens for those changes and patches + * the ApiRoom cache so every `useRoomData()` + * consumer sees the fresh value automatically. + * + * Mount once, at the level where the LiveKit Room instance lives. + */ +export const useSyncLiveKitMetadata = () => { + const room = useRoomContext() + const roomData = useRoomData() + const roomSlug = roomData?.slug + + useEffect(() => { + if (!room || !roomSlug) return + + const applyMetadata = (raw: string | undefined) => { + const parsed = parseMetadata(raw) + // No configuration key → leave the cache alone. Don't clobber the + // value we already loaded from the API with `undefined`. + if (!parsed?.configuration) return + + queryClient.setQueryData([keys.room, roomSlug], (prev) => { + if (!prev) return prev + if ( + JSON.stringify(prev.configuration) === + JSON.stringify(parsed.configuration) + ) { + return prev + } + return { ...prev, configuration: parsed.configuration } + }) + } + + // Apply whatever metadata is currently set (covers the case where we + // joined the room AFTER the last metadata change, so no event will fire). + applyMetadata(room.metadata) + + const handler = (raw: string) => applyMetadata(raw) + room.on(RoomEvent.RoomMetadataChanged, handler) + + return () => { + room.off(RoomEvent.RoomMetadataChanged, handler) + } + }, [room, roomSlug]) +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index c81dfc68..fb5b763a 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -32,6 +32,7 @@ import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKey import { useSettingsDialog } from '@/features/settings' import { SettingsDialogExtendedKey } from '@/features/settings/type' import { useVideoResolutionSubscription } from '../hooks/useVideoResolutionSubscription' +import { useSyncLiveKitMetadata } from '../hooks/useSyncLiveKitMetadata' import { SettingsDialogProvider } from '@/features/settings/components/SettingsDialogProvider' import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal' import { getParticipantName } from '@/features/rooms/utils/getParticipantName' @@ -90,6 +91,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) { useConnectionObserver() useRoomPageTitle() useVideoResolutionSubscription() + useSyncLiveKitMetadata() useRegisterKeyboardShortcut({ id: 'open-shortcuts',