mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-02 21:58:29 +00:00
✨(frontend) let authenticated users manage the lobby on trusted rooms
Frontend counterpart of the trusted-lobby backend feature: on `trusted` rooms, any authenticated participant sees the waiting notification and can accept or deny entry requests, not only admins and owners. Gating moves from role to capability: `useCanManageLobby` mirrors the backend permission and derives from `useRoomData()`. Room metadata is already synced into the query cache, so an access-level change mid-meeting recomputes the capability on every client with no new sync mechanism. It is only a UI gate; the backend re-checks everything per request and fails closed. Fetching moves into a single room-level `LobbyProvider`: the hook was previously instantiated by two components and only worked because React Query deduplicated their queries. The provider owns one query and an explicit state machine - ways in (connection established, ParticipantWaiting broadcast, panel opened, rights regained while the panel is open) all arm and fetch; ways out (rights lost, server 401/403) disarm and clear the cached list so nothing stale can render. Polling is tiered by audience since managers grow from a few admins to potentially the whole room: 1s when acting (panel open), 10s when the notification is shown, and zero when the list is empty - decided in the refetchInterval callback because structural sharing suppresses data-keyed effects on identical empty responses. A quiet room costs nothing; fetches are triggered by uncorrelated human events, never synchronized across the room (the rights-regained trigger is panel-gated for this reason).
This commit is contained in:
committed by
aleb_the_flash
parent
7369379106
commit
943b81676b
@@ -1,61 +1,31 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useRoomContext } from '@livekit/components-react'
|
|
||||||
import { RoomEvent } from 'livekit-client'
|
|
||||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
|
import { useCanManageLobby } from '@/features/rooms/livekit/hooks/useCanManageLobby'
|
||||||
import { useEnterRoom } from '../api/enterRoom'
|
import { useEnterRoom } from '../api/enterRoom'
|
||||||
import {
|
import {
|
||||||
useListWaitingParticipants,
|
useListWaitingParticipants,
|
||||||
type WaitingParticipant,
|
type WaitingParticipant,
|
||||||
} from '../../participants/api/listWaitingParticipants'
|
} from '../../participants/api/listWaitingParticipants'
|
||||||
import { decodeNotificationDataReceived } from '@/features/notifications/utils'
|
|
||||||
import { NotificationType } from '@/features/notifications/NotificationType'
|
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
import { reportError } from '@/features/analytics/telemetry'
|
||||||
|
|
||||||
export const POLL_INTERVAL_MS = 1000
|
|
||||||
|
|
||||||
export const useWaitingParticipants = () => {
|
export const useWaitingParticipants = () => {
|
||||||
const [listEnabled, setListEnabled] = useState(true)
|
|
||||||
|
|
||||||
const roomData = useRoomData()
|
const roomData = useRoomData()
|
||||||
const roomId = roomData?.id || '' // FIXME - bad practice
|
const roomId = roomData?.id || '' // FIXME - bad practice
|
||||||
|
|
||||||
const room = useRoomContext()
|
const canManageLobby = useCanManageLobby()
|
||||||
const isAdminOrOwner = useIsAdminOrOwner()
|
|
||||||
|
|
||||||
const handleDataReceived = useCallback((payload: Uint8Array) => {
|
|
||||||
const notification = decodeNotificationDataReceived(payload)
|
|
||||||
if (notification?.type === NotificationType.ParticipantWaiting) {
|
|
||||||
setListEnabled(true)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isAdminOrOwner) {
|
|
||||||
room.on(RoomEvent.DataReceived, handleDataReceived)
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
room.off(RoomEvent.DataReceived, handleDataReceived)
|
|
||||||
}
|
|
||||||
}, [isAdminOrOwner, room, handleDataReceived])
|
|
||||||
|
|
||||||
const { data: waitingData, refetch: refetchWaiting } =
|
const { data: waitingData, refetch: refetchWaiting } =
|
||||||
useListWaitingParticipants(roomId, {
|
useListWaitingParticipants(roomId, {
|
||||||
retry: false,
|
retry: false,
|
||||||
enabled: listEnabled && isAdminOrOwner,
|
enabled: false,
|
||||||
refetchInterval: POLL_INTERVAL_MS,
|
|
||||||
refetchIntervalInBackground: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const waitingParticipants = useMemo(
|
const waitingParticipants = useMemo(
|
||||||
() => waitingData?.participants || [],
|
() => (canManageLobby ? waitingData?.participants || [] : []),
|
||||||
[waitingData]
|
[waitingData, canManageLobby]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!waitingParticipants.length) setListEnabled(false)
|
|
||||||
}, [waitingParticipants])
|
|
||||||
|
|
||||||
const { mutateAsync: enterRoom } = useEnterRoom()
|
const { mutateAsync: enterRoom } = useEnterRoom()
|
||||||
|
|
||||||
const handleParticipantEntry = async (
|
const handleParticipantEntry = async (
|
||||||
@@ -74,8 +44,6 @@ export const useWaitingParticipants = () => {
|
|||||||
allowEntry: boolean
|
allowEntry: boolean
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setListEnabled(false)
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
waitingParticipants.map((participant) =>
|
waitingParticipants.map((participant) =>
|
||||||
enterRoom({
|
enterRoom({
|
||||||
@@ -89,7 +57,6 @@ export const useWaitingParticipants = () => {
|
|||||||
await refetchWaiting()
|
await refetchWaiting()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportError('generic_failure', e)
|
reportError('generic_failure', e)
|
||||||
setListEnabled(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { useCallback, useEffect } from 'react'
|
||||||
|
import { useConnectionState, useRoomContext } from '@livekit/components-react'
|
||||||
|
import { ConnectionState, RoomEvent } from 'livekit-client'
|
||||||
|
import { useCanManageLobby } from '@/features/rooms/livekit/hooks/useCanManageLobby'
|
||||||
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
|
import { useListWaitingParticipants } from '@/features/participants/api/listWaitingParticipants'
|
||||||
|
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||||
|
import { decodeNotificationDataReceived } from '@/features/notifications/utils'
|
||||||
|
import { NotificationType } from '@/features/notifications'
|
||||||
|
import { usePrevious } from '@/hooks/usePrevious'
|
||||||
|
import { keys } from '@/api/queryKeys'
|
||||||
|
import { queryClient } from '@/api/queryClient'
|
||||||
|
import { ApiError } from '@/api/ApiError'
|
||||||
|
|
||||||
|
export const POLL_INTERVAL_MS = 1000
|
||||||
|
export const LAZY_POLL_INTERVAL_MS = 10_000
|
||||||
|
|
||||||
|
export const LobbyProvider = () => {
|
||||||
|
const room = useRoomContext()
|
||||||
|
|
||||||
|
const canManageLobby = useCanManageLobby()
|
||||||
|
const roomData = useRoomData()
|
||||||
|
const { isParticipantsOpen } = useSidePanel()
|
||||||
|
const isConnected = useConnectionState(room) === ConnectionState.Connected
|
||||||
|
|
||||||
|
const roomId = roomData?.id || '' // FIXME - bad practice
|
||||||
|
|
||||||
|
const { error: waitingError, refetch: refetchWaiting } =
|
||||||
|
useListWaitingParticipants(roomId, {
|
||||||
|
retry: false,
|
||||||
|
enabled: canManageLobby && isConnected && !!roomId,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchInterval: (query) => {
|
||||||
|
if (!query.state.data?.participants?.length) return false
|
||||||
|
if (isParticipantsOpen) return POLL_INTERVAL_MS
|
||||||
|
return LAZY_POLL_INTERVAL_MS
|
||||||
|
},
|
||||||
|
refetchIntervalInBackground: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Triggers: each one-shot, idempotent, deduped by React Query if
|
||||||
|
// concurrent. The interval takes over whenever a fetch finds waiters.
|
||||||
|
const fetchIfManager = useCallback(() => {
|
||||||
|
if (canManageLobby) refetchWaiting()
|
||||||
|
}, [canManageLobby, refetchWaiting])
|
||||||
|
|
||||||
|
// 1. Connection established (join or reconnect)
|
||||||
|
useEffect(() => {
|
||||||
|
room.on(RoomEvent.Connected, fetchIfManager)
|
||||||
|
room.on(RoomEvent.Reconnected, fetchIfManager)
|
||||||
|
return () => {
|
||||||
|
room.off(RoomEvent.Connected, fetchIfManager)
|
||||||
|
room.off(RoomEvent.Reconnected, fetchIfManager)
|
||||||
|
}
|
||||||
|
}, [room, fetchIfManager])
|
||||||
|
|
||||||
|
// 2. Someone started waiting (LiveKit broadcast).
|
||||||
|
const handleDataReceived = useCallback(
|
||||||
|
(payload: Uint8Array) => {
|
||||||
|
const notification = decodeNotificationDataReceived(payload)
|
||||||
|
if (notification?.type === NotificationType.ParticipantWaiting) {
|
||||||
|
fetchIfManager()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchIfManager]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canManageLobby) {
|
||||||
|
room.on(RoomEvent.DataReceived, handleDataReceived)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
room.off(RoomEvent.DataReceived, handleDataReceived)
|
||||||
|
}
|
||||||
|
}, [canManageLobby, room, handleDataReceived])
|
||||||
|
|
||||||
|
// 3. Rights regained.
|
||||||
|
const prevCanManageLobby = usePrevious(canManageLobby)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!prevCanManageLobby && canManageLobby && isConnected) {
|
||||||
|
fetchIfManager()
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
prevCanManageLobby,
|
||||||
|
canManageLobby,
|
||||||
|
isParticipantsOpen,
|
||||||
|
fetchIfManager,
|
||||||
|
isConnected,
|
||||||
|
])
|
||||||
|
|
||||||
|
const clearWaitingList = useCallback(() => {
|
||||||
|
const queryKey = [keys.waitingParticipants, roomId]
|
||||||
|
queryClient.cancelQueries({ queryKey })
|
||||||
|
queryClient.setQueryData(queryKey, { participants: [] })
|
||||||
|
}, [roomId])
|
||||||
|
|
||||||
|
// Rights lost mid-meeting (covers trusted -> restricted/public).
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevCanManageLobby && !canManageLobby) clearWaitingList()
|
||||||
|
}, [prevCanManageLobby, canManageLobby, clearWaitingList])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
waitingError instanceof ApiError &&
|
||||||
|
[401, 403].includes(waitingError.statusCode)
|
||||||
|
) {
|
||||||
|
clearWaitingList()
|
||||||
|
}
|
||||||
|
}, [waitingError, clearWaitingList])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useUser } from '@/features/auth/api/useUser'
|
||||||
|
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
|
||||||
|
import { useIsAdminOrOwner } from './useIsAdminOrOwner'
|
||||||
|
import { useRoomData } from './useRoomData'
|
||||||
|
|
||||||
|
export const useCanManageLobby = () => {
|
||||||
|
const isAdminOrOwner = useIsAdminOrOwner()
|
||||||
|
const { isLoggedIn } = useUser()
|
||||||
|
const roomData = useRoomData()
|
||||||
|
|
||||||
|
return (
|
||||||
|
(isAdminOrOwner ||
|
||||||
|
(isLoggedIn === true &&
|
||||||
|
roomData?.access_level === ApiAccessLevel.TRUSTED)) &&
|
||||||
|
roomData?.access_level !== ApiAccessLevel.PUBLIC
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ import { PinAnnouncer } from '@/features/layout/components/PinAnnouncer'
|
|||||||
import { ChatProvider } from '@/features/chat/components/ChatProvider'
|
import { ChatProvider } from '@/features/chat/components/ChatProvider'
|
||||||
import { SyncDevicePreferences } from '@/features/rooms/livekit/components/SyncDevicePreferences'
|
import { SyncDevicePreferences } from '@/features/rooms/livekit/components/SyncDevicePreferences'
|
||||||
import { RoomSilentMicDetector } from '@/features/rooms/components/SilentMicDetector'
|
import { RoomSilentMicDetector } from '@/features/rooms/components/SilentMicDetector'
|
||||||
|
import { LobbyProvider } from '@/features/rooms/components/LobbyProvider'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@@ -120,6 +121,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
<RoomSilentMicDetector />
|
<RoomSilentMicDetector />
|
||||||
<MediaStateObserver />
|
<MediaStateObserver />
|
||||||
<ChatProvider />
|
<ChatProvider />
|
||||||
|
<LobbyProvider />
|
||||||
<VideoResolutionSubscription />
|
<VideoResolutionSubscription />
|
||||||
<div
|
<div
|
||||||
className="lk-video-conference"
|
className="lk-video-conference"
|
||||||
|
|||||||
Reference in New Issue
Block a user