From 7f1f573af8b59ba66667fb7af145169f7a6cb6e9 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Thu, 16 Jan 2025 14:54:44 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(front)=20revamp=20join=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We want this screen to have a better ux, the join button was invisible on some small screen sizes, also we want to align the style of this screen with the ui of the video conference previously made. --- .../src/features/rooms/components/Join.tsx | 376 +++++++++++++++++- .../controls/SelectToggleDevice.tsx | 23 +- .../prefabs/ControlBar/DesktopControlBar.tsx | 4 +- src/frontend/src/locales/de/rooms.json | 5 +- src/frontend/src/locales/en/rooms.json | 7 +- src/frontend/src/locales/fr/rooms.json | 7 +- src/frontend/src/primitives/Field.tsx | 2 +- 7 files changed, 399 insertions(+), 25 deletions(-) diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index e52bf610..0e333d47 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -1,8 +1,27 @@ import { useTranslation } from 'react-i18next' -import { PreJoin, type LocalUserChoices } from '@livekit/components-react' +import { + ParticipantPlaceholder, + usePersistentUserChoices, + usePreviewTracks, + type LocalUserChoices, +} from '@livekit/components-react' +import { css } from '@/styled-system/css' +import { log } from '@livekit/components-core' +import { defaultUserChoices } from '@livekit/components-core' import { Screen } from '@/layout/Screen' -import { CenteredContent } from '@/layout/CenteredContent' import { useUser } from '@/features/auth' +import React from 'react' +import { + facingModeFromLocalTrack, + LocalVideoTrack, + Track, +} from 'livekit-client' +import { H } from '@/primitives/H' +import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleDevice' +import { Field } from '@/primitives/Field' +import { Button } from '@/primitives' + +const onError = (e: Error) => console.error('ERROR', e) export const Join = ({ onSubmit, @@ -11,20 +30,349 @@ export const Join = ({ }) => { const { t } = useTranslation('rooms') const { user } = useUser() + const defaults: Partial = { username: user?.full_name } + const persistUserChoices = true + const joinLabel = t('join.joinLabel') + const userLabel = t('join.usernameLabel') + + const [userChoices, setUserChoices] = React.useState(defaultUserChoices) + + // TODO: Remove and pipe `defaults` object directly into `usePersistentUserChoices` once we fully switch from type `LocalUserChoices` to `UserChoices`. + const partialDefaults: Partial = { + ...(defaults.audioDeviceId !== undefined && { + audioDeviceId: defaults.audioDeviceId, + }), + ...(defaults.videoDeviceId !== undefined && { + videoDeviceId: defaults.videoDeviceId, + }), + ...(defaults.audioEnabled !== undefined && { + audioEnabled: defaults.audioEnabled, + }), + ...(defaults.videoEnabled !== undefined && { + videoEnabled: defaults.videoEnabled, + }), + ...(defaults.username !== undefined && { username: defaults.username }), + } + + const { + userChoices: initialUserChoices, + saveAudioInputDeviceId, + saveAudioInputEnabled, + saveVideoInputDeviceId, + saveVideoInputEnabled, + saveUsername, + } = usePersistentUserChoices({ + defaults: partialDefaults, + preventSave: !persistUserChoices, + preventLoad: !persistUserChoices, + }) + + // Initialize device settings + const [audioEnabled, setAudioEnabled] = React.useState( + initialUserChoices.audioEnabled + ) + const [videoEnabled, setVideoEnabled] = React.useState( + initialUserChoices.videoEnabled + ) + const [audioDeviceId, setAudioDeviceId] = React.useState( + initialUserChoices.audioDeviceId + ) + const [videoDeviceId, setVideoDeviceId] = React.useState( + initialUserChoices.videoDeviceId + ) + const [username, setUsername] = React.useState(initialUserChoices.username) + + // Save user choices to persistent storage. + React.useEffect(() => { + saveAudioInputEnabled(audioEnabled) + }, [audioEnabled, saveAudioInputEnabled]) + React.useEffect(() => { + saveVideoInputEnabled(videoEnabled) + }, [videoEnabled, saveVideoInputEnabled]) + React.useEffect(() => { + saveAudioInputDeviceId(audioDeviceId) + }, [audioDeviceId, saveAudioInputDeviceId]) + React.useEffect(() => { + saveVideoInputDeviceId(videoDeviceId) + }, [videoDeviceId, saveVideoInputDeviceId]) + React.useEffect(() => { + saveUsername(username) + }, [username, saveUsername]) + + const tracks = usePreviewTracks( + { + audio: audioEnabled + ? { deviceId: initialUserChoices.audioDeviceId } + : false, + video: videoEnabled + ? { deviceId: initialUserChoices.videoDeviceId } + : false, + }, + onError + ) + + const videoEl = React.useRef(null) + + const videoTrack = React.useMemo( + () => + tracks?.filter( + (track) => track.kind === Track.Kind.Video + )[0] as LocalVideoTrack, + [tracks] + ) + + const audioTrack = React.useMemo( + () => + tracks?.filter( + (track) => track.kind === Track.Kind.Audio + )[0] as LocalVideoTrack, + [tracks] + ) + + const facingMode = React.useMemo(() => { + if (videoTrack) { + const { facingMode } = facingModeFromLocalTrack(videoTrack) + return facingMode + } else { + return 'undefined' + } + }, [videoTrack]) + + React.useEffect(() => { + if (videoEl.current && videoTrack) { + videoTrack.unmute() + videoTrack.attach(videoEl.current) + } + + return () => { + videoTrack?.detach() + } + }, [videoTrack]) + + const [isValid, setIsValid] = React.useState() + + const handleValidation = React.useCallback((values: LocalUserChoices) => { + return values.username !== '' + }, []) + + React.useEffect(() => { + const newUserChoices = { + username, + videoEnabled, + videoDeviceId, + audioEnabled, + audioDeviceId, + } + setUserChoices(newUserChoices) + setIsValid(handleValidation(newUserChoices)) + }, [ + username, + videoEnabled, + handleValidation, + audioEnabled, + audioDeviceId, + videoDeviceId, + ]) + + function handleSubmit() { + if (handleValidation(userChoices)) { + if (typeof onSubmit === 'function') { + onSubmit(userChoices) + } + } else { + log.warn('Validation failed with: ', userChoices) + } + } return ( - - - - + +
+
+
+
+ {videoTrack && ( + // eslint-disable-next-line jsx-a11y/media-has-caption +
+
+ setAudioEnabled(enabled)} + onDeviceError={(error) => console.error(error)} + onActiveDeviceChange={(deviceId) => + setAudioDeviceId(deviceId ?? '') + } + variant="tertiary" + /> + { + setVideoEnabled(enabled) + }} + onDeviceError={(error) => console.error(error)} + onActiveDeviceChange={(deviceId) => + setVideoDeviceId(deviceId ?? '') + } + variant="tertiary" + /> +
+
+ +
+
+ + {t('join.heading')} + + setUsername(value)} + validate={(value) => { + return !value ?

{t('join.errors.usernameEmpty')}

: null + }} + className={css({ + width: '100%', + })} + wrapperProps={{ + noMargin: true, + fullWidth: true, + }} + labelProps={{ + center: true, + }} + maxLength={50} + /> + + +
+
+
) } diff --git a/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx b/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx index 3a45baac..84fbaffa 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx @@ -13,13 +13,14 @@ import { RiVideoOffLine, RiVideoOnLine, } from '@remixicon/react' -import { Track } from 'livekit-client' +import { LocalAudioTrack, LocalVideoTrack, Track } from 'livekit-client' import { Shortcut } from '@/features/shortcuts/types' import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx' import { css } from '@/styled-system/css' import { ButtonRecipeProps } from '@/primitives/buttonRecipe' +import { useEffect } from 'react' export type ToggleSource = Exclude< Track.Source, @@ -66,6 +67,8 @@ const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = { type SelectToggleDeviceProps = UseTrackToggleProps & { + track?: LocalAudioTrack | LocalVideoTrack | undefined + initialDeviceId?: string onActiveDeviceChange: (deviceId: string) => void source: SelectToggleSource variant?: NonNullable['variant'] @@ -74,10 +77,12 @@ type SelectToggleDeviceProps = } export const SelectToggleDevice = ({ + track, + initialDeviceId, onActiveDeviceChange, hideMenu, variant = 'primaryDark', - menuVariant = 'light', + menuVariant = 'light', ...props }: SelectToggleDeviceProps) => { const config = selectToggleDeviceConfig[props.source] @@ -88,7 +93,19 @@ export const SelectToggleDevice = ({ const trackProps = useTrackToggle(props) const { devices, activeDeviceId, setActiveMediaDevice } = - useMediaDeviceSelect({ kind: config.kind }) + useMediaDeviceSelect({ kind: config.kind, track }) + + /** + * When providing only track outside of a room context, activeDeviceId is undefined. + * So we need to initialize it with the initialDeviceId. + * nb: I don't understand why useMediaDeviceSelect cannot infer it from track device id. + */ + useEffect(() => { + if (initialDeviceId !== undefined) { + setActiveMediaDevice(initialDeviceId) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setActiveMediaDevice]) const selectLabel = t('choose', { keyPrefix: `join.${config.kind}` }) diff --git a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/DesktopControlBar.tsx b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/DesktopControlBar.tsx index 0dab2b99..9cb1da10 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/DesktopControlBar.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/DesktopControlBar.tsx @@ -62,7 +62,7 @@ export function DesktopControlBar({ onActiveDeviceChange={(deviceId) => saveAudioInputDeviceId(deviceId ?? '') } - variant="dark" + menuVariant="dark" /> saveVideoInputDeviceId(deviceId ?? '') } - variant="dark" + menuVariant="dark" /> {browserSupportsScreenSharing && (