(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."""
old_configuration = serializer.instance.configuration
old_access_level = serializer.instance.access_level
room = serializer.save()
if room.configuration == old_configuration:
if (
room.configuration == old_configuration
and room.access_level == old_access_level
):
return
metadata = {"configuration": room.configuration}
metadata = {
"configuration": room.configuration,
"access_level": room.access_level,
}
try:
RoomManagement().update_metadata(
@@ -117,6 +117,7 @@ def test_api_rooms_update_administrators(mock_update_metadata):
mock_update_metadata.assert_called_once_with(
room_name=str(room.id),
metadata={
"access_level": "public",
"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(
room_name=str(room.id),
metadata={
"access_level": "restricted",
"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")
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()
room = RoomFactory(
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.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")
@@ -395,7 +403,10 @@ def test_api_rooms_update_livekit_room_not_found(mock_update_metadata):
mock_update_metadata.assert_called_once_with(
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(
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 { queryClient } from '@/api/queryClient'
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 { useRoomData } from './useRoomData'
@@ -14,6 +18,7 @@ import { useRoomData } from './useRoomData'
*/
type RoomLiveKitMetadata = {
configuration?: RoomConfiguration
access_level?: ApiAccessLevel
}
const parseMetadata = (raw: string | undefined): RoomLiveKitMetadata | null => {
@@ -46,19 +51,24 @@ export const useSyncLiveKitMetadata = () => {
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
if (!parsed) return
queryClient.setQueryData<ApiRoom>([keys.room, roomSlug], (prev) => {
if (!prev) return prev
const nextConfiguration = parsed.configuration ?? prev.configuration
const nextAccessLevel = parsed.access_level ?? prev.access_level
if (
JSON.stringify(prev.configuration) ===
JSON.stringify(parsed.configuration)
nextConfiguration === prev.configuration &&
nextAccessLevel === prev.access_level
) {
return prev
}
return { ...prev, configuration: parsed.configuration }
return {
...prev,
configuration: nextConfiguration,
access_level: nextAccessLevel,
}
})
}