mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-04 14:45:40 +00:00
♻️(frontend) extract lobby logic into a dedicated component
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.
This commit is contained in:
committed by
aleb_the_flash
parent
8f27b89d21
commit
0dd2478c3e
@@ -11,10 +11,7 @@ import {
|
|||||||
MediaDeviceFailure,
|
MediaDeviceFailure,
|
||||||
Track,
|
Track,
|
||||||
} from 'livekit-client'
|
} from 'livekit-client'
|
||||||
import { H } from '@/primitives/H'
|
import { Button, Dialog, Text } from '@/primitives'
|
||||||
import { Field } from '@/primitives/Field'
|
|
||||||
import { Button, Dialog, Form, Text } from '@/primitives'
|
|
||||||
import { VStack } from '@/styled-system/jsx'
|
|
||||||
import { Heading } from 'react-aria-components'
|
import { Heading } from 'react-aria-components'
|
||||||
import { RiImageCircleAiFill } from '@remixicon/react'
|
import { RiImageCircleAiFill } from '@remixicon/react'
|
||||||
import {
|
import {
|
||||||
@@ -22,18 +19,10 @@ import {
|
|||||||
EffectsConfigurationProps,
|
EffectsConfigurationProps,
|
||||||
} from '../livekit/components/effects/EffectsConfiguration'
|
} from '../livekit/components/effects/EffectsConfiguration'
|
||||||
import { SelectDevice } from '../livekit/components/controls/Device/SelectDevice'
|
import { SelectDevice } from '../livekit/components/controls/Device/SelectDevice'
|
||||||
|
import { Lobby } from './Lobby'
|
||||||
import { ToggleDevice } from '../livekit/components/controls/Device/ToggleDevice'
|
import { ToggleDevice } from '../livekit/components/controls/Device/ToggleDevice'
|
||||||
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
||||||
import { isMobileBrowser } from '@livekit/components-core'
|
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 {
|
import {
|
||||||
notePermissionDeniedFromGum,
|
notePermissionDeniedFromGum,
|
||||||
openPermissionsDialog,
|
openPermissionsDialog,
|
||||||
@@ -52,13 +41,9 @@ import {
|
|||||||
userChoicesStore,
|
userChoicesStore,
|
||||||
} from '@/stores/userChoices'
|
} from '@/stores/userChoices'
|
||||||
|
|
||||||
import { saveUsername, userStore } from '@/stores/user'
|
|
||||||
|
|
||||||
import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice'
|
import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice'
|
||||||
import { useSyncTrackDeviceId } from '../livekit/hooks/useSyncTrackDeviceId'
|
import { useSyncTrackDeviceId } from '../livekit/hooks/useSyncTrackDeviceId'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { useUser } from '@/features/auth/api/useUser'
|
|
||||||
import { useConfig } from '@/api/useConfig'
|
|
||||||
|
|
||||||
const onError = (e: Error, kind?: PermissionKind) => {
|
const onError = (e: Error, kind?: PermissionKind) => {
|
||||||
reportError('join_preview_failure', e, { path: 'join_preview' })
|
reportError('join_preview_failure', e, { path: 'join_preview' })
|
||||||
@@ -130,9 +115,6 @@ export const Join = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
const { data: configData } = useConfig()
|
|
||||||
const { isLoggedIn, user } = useUser()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
audioEnabled,
|
audioEnabled,
|
||||||
videoEnabled,
|
videoEnabled,
|
||||||
@@ -142,8 +124,6 @@ export const Join = ({
|
|||||||
processorConfig,
|
processorConfig,
|
||||||
} = useSnapshot(userChoicesStore)
|
} = useSnapshot(userChoicesStore)
|
||||||
|
|
||||||
const { username } = useSnapshot(userStore)
|
|
||||||
|
|
||||||
const initialUserChoices = useRef<LocalUserChoices | null>(null)
|
const initialUserChoices = useRef<LocalUserChoices | null>(null)
|
||||||
|
|
||||||
if (initialUserChoices.current === null) {
|
if (initialUserChoices.current === null) {
|
||||||
@@ -306,63 +286,6 @@ export const Join = ({
|
|||||||
}
|
}
|
||||||
}, [videoTrack, videoEnabled])
|
}, [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 isCameraDeniedOrPrompted = useCannotUseDevice('videoinput')
|
||||||
const isMicrophoneDeniedOrPrompted = useCannotUseDevice('audioinput')
|
const isMicrophoneDeniedOrPrompted = useCannotUseDevice('audioinput')
|
||||||
|
|
||||||
@@ -401,85 +324,6 @@ export const Join = ({
|
|||||||
return null
|
return null
|
||||||
}, [isMicrophoneDeniedOrPrompted, isCameraDeniedOrPrompted])
|
}, [isMicrophoneDeniedOrPrompted, isCameraDeniedOrPrompted])
|
||||||
|
|
||||||
const renderWaitingState = () => {
|
|
||||||
switch (status) {
|
|
||||||
case ApiLobbyStatus.TIMEOUT:
|
|
||||||
return (
|
|
||||||
<VStack alignItems="center" textAlign="center">
|
|
||||||
<H lvl={1} margin={false} centered>
|
|
||||||
{t('timeoutInvite.title')}
|
|
||||||
</H>
|
|
||||||
<Text as="p" variant="note">
|
|
||||||
{t('timeoutInvite.body')}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
)
|
|
||||||
|
|
||||||
case ApiLobbyStatus.DENIED:
|
|
||||||
return (
|
|
||||||
<VStack alignItems="center" textAlign="center">
|
|
||||||
<H lvl={1} margin={false} centered>
|
|
||||||
{t('denied.title')}
|
|
||||||
</H>
|
|
||||||
<Text as="p" variant="note">
|
|
||||||
{t('denied.body')}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
)
|
|
||||||
|
|
||||||
case ApiLobbyStatus.WAITING:
|
|
||||||
return (
|
|
||||||
<VStack alignItems="center" textAlign="center">
|
|
||||||
<H lvl={1} margin={false} centered>
|
|
||||||
{t('waiting.title')}
|
|
||||||
</H>
|
|
||||||
<Text
|
|
||||||
as="p"
|
|
||||||
variant="note"
|
|
||||||
className={css({ marginBottom: '1.5rem' })}
|
|
||||||
>
|
|
||||||
{t('waiting.body')}
|
|
||||||
</Text>
|
|
||||||
<Spinner />
|
|
||||||
</VStack>
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
submitLabel={t('joinLabel')}
|
|
||||||
submitButtonProps={{
|
|
||||||
fullWidth: true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<VStack marginBottom={1}>
|
|
||||||
<H lvl={1} margin="sm" centered>
|
|
||||||
{t('heading')}
|
|
||||||
</H>
|
|
||||||
{(!isLoggedIn ||
|
|
||||||
configData?.authenticated_users_can_edit_display_name) && (
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
onChange={saveUsername}
|
|
||||||
label={t('usernameLabel')}
|
|
||||||
id="input-name"
|
|
||||||
defaultValue={username || user?.full_name}
|
|
||||||
validate={(value) => !value && t('errors.usernameEmpty')}
|
|
||||||
wrapperProps={{
|
|
||||||
noMargin: true,
|
|
||||||
fullWidth: true,
|
|
||||||
}}
|
|
||||||
autoComplete="name"
|
|
||||||
maxLength={50}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Screen footer={false}>
|
<Screen footer={false}>
|
||||||
<div
|
<div
|
||||||
@@ -788,7 +632,7 @@ export const Join = ({
|
|||||||
margin: '1rem 1rem 1rem 0.5rem',
|
margin: '1rem 1rem 1rem 0.5rem',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{renderWaitingState()}
|
<Lobby roomId={roomId} enterRoom={enterRoom} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Screen>
|
</Screen>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<VStack alignItems="center" textAlign="center">
|
||||||
|
<H lvl={1} margin={false} centered>
|
||||||
|
{t('timeoutInvite.title')}
|
||||||
|
</H>
|
||||||
|
<Text as="p" variant="note">
|
||||||
|
{t('timeoutInvite.body')}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
|
||||||
|
case ApiLobbyStatus.DENIED:
|
||||||
|
return (
|
||||||
|
<VStack alignItems="center" textAlign="center">
|
||||||
|
<H lvl={1} margin={false} centered>
|
||||||
|
{t('denied.title')}
|
||||||
|
</H>
|
||||||
|
<Text as="p" variant="note">
|
||||||
|
{t('denied.body')}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
|
||||||
|
case ApiLobbyStatus.WAITING:
|
||||||
|
return (
|
||||||
|
<VStack alignItems="center" textAlign="center">
|
||||||
|
<H lvl={1} margin={false} centered>
|
||||||
|
{t('waiting.title')}
|
||||||
|
</H>
|
||||||
|
<Text
|
||||||
|
as="p"
|
||||||
|
variant="note"
|
||||||
|
className={css({ marginBottom: '1.5rem' })}
|
||||||
|
>
|
||||||
|
{t('waiting.body')}
|
||||||
|
</Text>
|
||||||
|
<Spinner />
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitLabel={t('joinLabel')}
|
||||||
|
submitButtonProps={{
|
||||||
|
fullWidth: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VStack marginBottom={1}>
|
||||||
|
<H lvl={1} margin="sm" centered>
|
||||||
|
{t('heading')}
|
||||||
|
</H>
|
||||||
|
{(!isLoggedIn ||
|
||||||
|
configData?.authenticated_users_can_edit_display_name) && (
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
onChange={saveUsername}
|
||||||
|
label={t('usernameLabel')}
|
||||||
|
aria-label={t('usernameLabel')}
|
||||||
|
id="input-name"
|
||||||
|
defaultValue={username || user?.full_name}
|
||||||
|
validate={(value) => !value && t('errors.usernameEmpty')}
|
||||||
|
wrapperProps={{
|
||||||
|
noMargin: true,
|
||||||
|
fullWidth: true,
|
||||||
|
}}
|
||||||
|
autoComplete="name"
|
||||||
|
maxLength={50}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user