From 0dd2478c3e11c9c424b3ba2c21919a9d4183e783 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sun, 9 Aug 2026 23:21:47 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20extract=20lobby?= =?UTF-8?q?=20logic=20into=20a=20dedicated=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract all the lobby-related logic from the Join component into a dedicated component. This makes the Join component easier to maintain and pushes the lobby state down closer to where it is actually used, avoiding unnecessary re-renders higher up. --- .../src/features/rooms/components/Join.tsx | 162 +---------------- .../src/features/rooms/components/Lobby.tsx | 168 ++++++++++++++++++ 2 files changed, 171 insertions(+), 159 deletions(-) create mode 100644 src/frontend/src/features/rooms/components/Lobby.tsx diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index f7ee31d6..18a1610b 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -11,10 +11,7 @@ import { MediaDeviceFailure, Track, } from 'livekit-client' -import { H } from '@/primitives/H' -import { Field } from '@/primitives/Field' -import { Button, Dialog, Form, Text } from '@/primitives' -import { VStack } from '@/styled-system/jsx' +import { Button, Dialog, Text } from '@/primitives' import { Heading } from 'react-aria-components' import { RiImageCircleAiFill } from '@remixicon/react' import { @@ -22,18 +19,10 @@ import { EffectsConfigurationProps, } from '../livekit/components/effects/EffectsConfiguration' import { SelectDevice } from '../livekit/components/controls/Device/SelectDevice' +import { Lobby } from './Lobby' import { ToggleDevice } from '../livekit/components/controls/Device/ToggleDevice' import { BackgroundProcessorFactory } from '../livekit/components/blur' import { isMobileBrowser } from '@livekit/components-core' -import { fetchRoom } from '@/features/rooms/api/fetchRoom' -import { keys } from '@/api/queryKeys' -import { useLobby } from '../hooks/useLobby' -import { useQuery } from '@tanstack/react-query' -import { queryClient } from '@/api/queryClient' -import { ApiLobbyStatus, type ApiRequestEntry } from '../api/requestEntry' -import { Spinner } from '@/primitives/Spinner' -import { ApiAccessLevel } from '../api/ApiRoom' -import { useLoginHint } from '@/hooks/useLoginHint' import { notePermissionDeniedFromGum, openPermissionsDialog, @@ -52,13 +41,9 @@ import { userChoicesStore, } from '@/stores/userChoices' -import { saveUsername, userStore } from '@/stores/user' - import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice' import { useSyncTrackDeviceId } from '../livekit/hooks/useSyncTrackDeviceId' import { useSnapshot } from 'valtio' -import { useUser } from '@/features/auth/api/useUser' -import { useConfig } from '@/api/useConfig' const onError = (e: Error, kind?: PermissionKind) => { reportError('join_preview_failure', e, { path: 'join_preview' }) @@ -130,9 +115,6 @@ export const Join = ({ }) => { const { t } = useTranslation('rooms', { keyPrefix: 'join' }) - const { data: configData } = useConfig() - const { isLoggedIn, user } = useUser() - const { audioEnabled, videoEnabled, @@ -142,8 +124,6 @@ export const Join = ({ processorConfig, } = useSnapshot(userChoicesStore) - const { username } = useSnapshot(userStore) - const initialUserChoices = useRef(null) if (initialUserChoices.current === null) { @@ -306,63 +286,6 @@ export const Join = ({ } }, [videoTrack, videoEnabled]) - // Room data strategy: - // 1. Initial fetch is performed to check access and get LiveKit configuration - // 2. Data remains valid for 6 hours to avoid unnecessary refetches - // 3. State is manually updated via queryClient when a waiting participant is accepted - // 4. No automatic refetching or revalidation occurs during this period - // todo - refactor in a hook - const { - data: roomData, - error, - isError, - refetch: refetchRoom, - } = useQuery({ - queryKey: [keys.room, roomId], - queryFn: () => fetchRoom({ roomId, username: username || user?.full_name }), - staleTime: 6 * 60 * 60 * 1000, // By default, LiveKit access tokens expire 6 hours after generation - retry: false, - enabled: false, - }) - - useEffect(() => { - if (isError && error?.statusCode == 404) { - // The room component will handle the room creation if the user is authenticated - enterRoom() - } - }, [isError, error, enterRoom]) - - const handleAccepted = (response: ApiRequestEntry) => { - queryClient.setQueryData([keys.room, roomId], { - ...roomData, - livekit: response.livekit, - }) - enterRoom() - } - - const { status, startWaiting } = useLobby({ - roomId, - username: username || user?.full_name || 'anonymous', - onAccepted: handleAccepted, - }) - - const { openLoginHint } = useLoginHint() - - const handleSubmit = async () => { - const { data } = await refetchRoom() - - if (!data?.livekit) { - // Display a message to inform the user that by logging in, they won't have to wait for room entry approval. - if (data?.access_level == ApiAccessLevel.TRUSTED) { - openLoginHint() - } - startWaiting() - return - } - - enterRoom() - } - const isCameraDeniedOrPrompted = useCannotUseDevice('videoinput') const isMicrophoneDeniedOrPrompted = useCannotUseDevice('audioinput') @@ -401,85 +324,6 @@ export const Join = ({ return null }, [isMicrophoneDeniedOrPrompted, isCameraDeniedOrPrompted]) - const renderWaitingState = () => { - switch (status) { - case ApiLobbyStatus.TIMEOUT: - return ( - - - {t('timeoutInvite.title')} - - - {t('timeoutInvite.body')} - - - ) - - case ApiLobbyStatus.DENIED: - return ( - - - {t('denied.title')} - - - {t('denied.body')} - - - ) - - case ApiLobbyStatus.WAITING: - return ( - - - {t('waiting.title')} - - - {t('waiting.body')} - - - - ) - - default: - return ( -
- - - {t('heading')} - - {(!isLoggedIn || - configData?.authenticated_users_can_edit_display_name) && ( - !value && t('errors.usernameEmpty')} - wrapperProps={{ - noMargin: true, - fullWidth: true, - }} - autoComplete="name" - maxLength={50} - /> - )} - -
- ) - } - } - return (
- {renderWaitingState()} +
diff --git a/src/frontend/src/features/rooms/components/Lobby.tsx b/src/frontend/src/features/rooms/components/Lobby.tsx new file mode 100644 index 00000000..34ceba21 --- /dev/null +++ b/src/frontend/src/features/rooms/components/Lobby.tsx @@ -0,0 +1,168 @@ +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { useQuery } from '@tanstack/react-query' +import { useSnapshot } from 'valtio' +import { css } from '@/styled-system/css' +import { VStack } from '@/styled-system/jsx' +import { H } from '@/primitives/H' +import { Field } from '@/primitives/Field' +import { Form, Text } from '@/primitives' +import { Spinner } from '@/primitives/Spinner' +import { keys } from '@/api/queryKeys' +import { queryClient } from '@/api/queryClient' +import { useLoginHint } from '@/hooks/useLoginHint' +import { useUser } from '@/features/auth/api/useUser' +import { useConfig } from '@/api/useConfig' +import { saveUsername, userStore } from '@/stores/user' +import { fetchRoom } from '../api/fetchRoom' +import { ApiAccessLevel } from '../api/ApiRoom' +import { ApiLobbyStatus, type ApiRequestEntry } from '../api/requestEntry' +import { useLobby } from '../hooks/useLobby' + +export const Lobby = ({ + roomId, + enterRoom, +}: { + roomId: string + enterRoom: () => void +}) => { + const { t } = useTranslation('rooms', { keyPrefix: 'join' }) + + const { data: configData } = useConfig() + const { isLoggedIn, user } = useUser() + const { username } = useSnapshot(userStore) + + // Room data strategy: + // 1. Initial fetch is performed to check access and get LiveKit configuration + // 2. Data remains valid for 6 hours to avoid unnecessary refetches + // 3. State is manually updated via queryClient when a waiting participant is accepted + // 4. No automatic refetching or revalidation occurs during this period + const { + data: roomData, + error, + isError, + refetch: refetchRoom, + } = useQuery({ + queryKey: [keys.room, roomId], + queryFn: () => fetchRoom({ roomId, username: username || user?.full_name }), + staleTime: 6 * 60 * 60 * 1000, // By default, LiveKit access tokens expire 6 hours after generation + retry: false, + enabled: false, + }) + + useEffect(() => { + if (isError && error?.statusCode == 404) { + // The room component will handle the room creation if the user is authenticated + enterRoom() + } + }, [isError, error, enterRoom]) + + const handleAccepted = (response: ApiRequestEntry) => { + queryClient.setQueryData([keys.room, roomId], { + ...roomData, + livekit: response.livekit, + }) + enterRoom() + } + + const { status, startWaiting } = useLobby({ + roomId, + username: username || user?.full_name || 'anonymous', + onAccepted: handleAccepted, + }) + + const { openLoginHint } = useLoginHint() + + const handleSubmit = async () => { + const { data } = await refetchRoom() + + if (!data?.livekit) { + // Display a message to inform the user that by logging in, they won't have to wait for room entry approval. + if (data?.access_level == ApiAccessLevel.TRUSTED) { + openLoginHint() + } + startWaiting() + return + } + + enterRoom() + } + + switch (status) { + case ApiLobbyStatus.TIMEOUT: + return ( + + + {t('timeoutInvite.title')} + + + {t('timeoutInvite.body')} + + + ) + + case ApiLobbyStatus.DENIED: + return ( + + + {t('denied.title')} + + + {t('denied.body')} + + + ) + + case ApiLobbyStatus.WAITING: + return ( + + + {t('waiting.title')} + + + {t('waiting.body')} + + + + ) + + default: + return ( +
+ + + {t('heading')} + + {(!isLoggedIn || + configData?.authenticated_users_can_edit_display_name) && ( + !value && t('errors.usernameEmpty')} + wrapperProps={{ + noMargin: true, + fullWidth: true, + }} + autoComplete="name" + maxLength={50} + /> + )} + +
+ ) + } +}