(backend) extend live synchronization to lobby access level updates

Extend the existing live synchronization mechanism beyond room
configuration to also include lobby access level changes.

This ensures that all owners and admins sharing a room maintain a
consistent and up-to-date view of room state in the frontend,
including configuration and access control updates.
This commit is contained in:
lebaudantoine
2026-05-15 19:05:48 +02:00
committed by aleb_the_flash
parent aab90650f1
commit 32fbedd358
3 changed files with 45 additions and 13 deletions
+10 -2
View File
@@ -309,12 +309,20 @@ class RoomViewSet(
"""Persist the room update, then sync metadata to LiveKit.""" """Persist the room update, then sync metadata to LiveKit."""
old_configuration = serializer.instance.configuration old_configuration = serializer.instance.configuration
old_access_level = serializer.instance.access_level
room = serializer.save() room = serializer.save()
if room.configuration == old_configuration: if (
room.configuration == old_configuration
and room.access_level == old_access_level
):
return return
metadata = {"configuration": room.configuration} metadata = {
"configuration": room.configuration,
"access_level": room.access_level,
}
try: try:
RoomManagement().update_metadata( RoomManagement().update_metadata(
@@ -117,6 +117,7 @@ def test_api_rooms_update_administrators(mock_update_metadata):
mock_update_metadata.assert_called_once_with( mock_update_metadata.assert_called_once_with(
room_name=str(room.id), room_name=str(room.id),
metadata={ metadata={
"access_level": "public",
"configuration": {"can_publish_sources": ["camera", "microphone"]}, "configuration": {"can_publish_sources": ["camera", "microphone"]},
}, },
) )
@@ -153,6 +154,7 @@ def test_api_rooms_update_administrators_configuration_only(mock_update_metadata
mock_update_metadata.assert_called_once_with( mock_update_metadata.assert_called_once_with(
room_name=str(room.id), room_name=str(room.id),
metadata={ metadata={
"access_level": "restricted",
"configuration": {"can_publish_sources": ["camera", "microphone"]}, "configuration": {"can_publish_sources": ["camera", "microphone"]},
}, },
) )
@@ -160,7 +162,7 @@ def test_api_rooms_update_administrators_configuration_only(mock_update_metadata
@patch.object(RoomManagement, "update_metadata") @patch.object(RoomManagement, "update_metadata")
def test_api_rooms_update_administrators_access_level_only(mock_update_metadata): def test_api_rooms_update_administrators_access_level_only(mock_update_metadata):
"""Should not sync LiveKit metadata when only access level changes.""" """Should sync LiveKit metadata when only access level changes."""
user = UserFactory() user = UserFactory()
room = RoomFactory( room = RoomFactory(
access_level=RoomAccessLevel.RESTRICTED, access_level=RoomAccessLevel.RESTRICTED,
@@ -185,7 +187,13 @@ def test_api_rooms_update_administrators_access_level_only(mock_update_metadata)
assert room.access_level == RoomAccessLevel.PUBLIC assert room.access_level == RoomAccessLevel.PUBLIC
assert room.configuration == {"can_publish_sources": ["camera"]} assert room.configuration == {"can_publish_sources": ["camera"]}
mock_update_metadata.assert_not_called() mock_update_metadata.assert_called_once_with(
room_name=str(room.id),
metadata={
"access_level": "public",
"configuration": {"can_publish_sources": ["camera"]},
},
)
@patch.object(RoomManagement, "update_metadata") @patch.object(RoomManagement, "update_metadata")
@@ -395,7 +403,10 @@ def test_api_rooms_update_livekit_room_not_found(mock_update_metadata):
mock_update_metadata.assert_called_once_with( mock_update_metadata.assert_called_once_with(
room_name=str(room.id), room_name=str(room.id),
metadata={"configuration": {"can_publish_sources": ["camera"]}}, metadata={
"access_level": room.access_level,
"configuration": {"can_publish_sources": ["camera"]},
},
) )
@@ -421,5 +432,8 @@ def test_api_rooms_update_livekit_sync_failure(mock_update_metadata):
mock_update_metadata.assert_called_once_with( mock_update_metadata.assert_called_once_with(
room_name=str(room.id), room_name=str(room.id),
metadata={"configuration": {"can_publish_sources": ["camera"]}}, metadata={
"access_level": room.access_level,
"configuration": {"can_publish_sources": ["camera"]},
},
) )
@@ -4,7 +4,11 @@ import { useEffect } from 'react'
import { RoomEvent } from 'livekit-client' import { RoomEvent } from 'livekit-client'
import { queryClient } from '@/api/queryClient' import { queryClient } from '@/api/queryClient'
import { keys } from '@/api/queryKeys' import { keys } from '@/api/queryKeys'
import type { ApiRoom, RoomConfiguration } from '@/features/rooms/api/ApiRoom' import {
ApiAccessLevel,
ApiRoom,
RoomConfiguration,
} from '@/features/rooms/api/ApiRoom'
import { useRoomContext } from '@livekit/components-react' import { useRoomContext } from '@livekit/components-react'
import { useRoomData } from './useRoomData' import { useRoomData } from './useRoomData'
@@ -14,6 +18,7 @@ import { useRoomData } from './useRoomData'
*/ */
type RoomLiveKitMetadata = { type RoomLiveKitMetadata = {
configuration?: RoomConfiguration configuration?: RoomConfiguration
access_level?: ApiAccessLevel
} }
const parseMetadata = (raw: string | undefined): RoomLiveKitMetadata | null => { const parseMetadata = (raw: string | undefined): RoomLiveKitMetadata | null => {
@@ -46,19 +51,24 @@ export const useSyncLiveKitMetadata = () => {
const applyMetadata = (raw: string | undefined) => { const applyMetadata = (raw: string | undefined) => {
const parsed = parseMetadata(raw) const parsed = parseMetadata(raw)
// No configuration key → leave the cache alone. Don't clobber the if (!parsed) return
// value we already loaded from the API with `undefined`.
if (!parsed?.configuration) return
queryClient.setQueryData<ApiRoom>([keys.room, roomSlug], (prev) => { queryClient.setQueryData<ApiRoom>([keys.room, roomSlug], (prev) => {
if (!prev) return prev if (!prev) return prev
const nextConfiguration = parsed.configuration ?? prev.configuration
const nextAccessLevel = parsed.access_level ?? prev.access_level
if ( if (
JSON.stringify(prev.configuration) === nextConfiguration === prev.configuration &&
JSON.stringify(parsed.configuration) nextAccessLevel === prev.access_level
) { ) {
return prev return prev
} }
return { ...prev, configuration: parsed.configuration }
return {
...prev,
configuration: nextConfiguration,
access_level: nextAccessLevel,
}
}) })
} }