diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx index 73072500..93e841bb 100644 --- a/src/frontend/src/features/rooms/components/Conference.tsx +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -15,8 +15,8 @@ import { InviteDialog } from './InviteDialog' import { VideoConference } from '../livekit/prefabs/VideoConference' import posthog from 'posthog-js' import { css } from '@/styled-system/css' -import { LocalUserChoices } from '../routes/Room' import { BackgroundProcessorFactory } from '../livekit/components/blur' +import { LocalUserChoices } from '@/stores/userChoices' export const Conference = ({ roomId, diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index 42095ed7..81097654 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -9,7 +9,6 @@ import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleD import { Field } from '@/primitives/Field' import { Button, Dialog, Text, Form } from '@/primitives' import { HStack, VStack } from '@/styled-system/jsx' -import { LocalUserChoices } from '../routes/Room' import { Heading } from 'react-aria-components' import { RiImageCircleAiFill } from '@remixicon/react' import { @@ -28,6 +27,7 @@ import { ApiLobbyStatus, ApiRequestEntry } from '../api/requestEntry' import { Spinner } from '@/primitives/Spinner' import { ApiAccessLevel } from '../api/ApiRoom' import { useLoginHint } from '@/hooks/useLoginHint' +import { LocalUserChoices } from '@/stores/userChoices' const onError = (e: Error) => console.error('ERROR', e) @@ -116,40 +116,26 @@ export const Join = ({ const { t } = useTranslation('rooms', { keyPrefix: 'join' }) const { - userChoices: initialUserChoices, + userChoices: { + audioEnabled, + videoEnabled, + audioDeviceId, + videoDeviceId, + processorSerialized, + username, + }, + saveAudioInputEnabled, + saveVideoInputEnabled, saveAudioInputDeviceId, saveVideoInputDeviceId, saveUsername, saveProcessorSerialized, - } = usePersistentUserChoices({}) + } = usePersistentUserChoices() - const [audioEnabled, setAudioEnabled] = useState(true) - const [videoEnabled, setVideoEnabled] = useState(true) - const [audioDeviceId, setAudioDeviceId] = useState( - initialUserChoices.audioDeviceId - ) - const [videoDeviceId, setVideoDeviceId] = useState( - initialUserChoices.videoDeviceId - ) - const [username, setUsername] = useState(initialUserChoices.username) const [processor, setProcessor] = useState( - BackgroundProcessorFactory.deserializeProcessor( - initialUserChoices.processorSerialized - ) + BackgroundProcessorFactory.deserializeProcessor(processorSerialized) ) - useEffect(() => { - saveAudioInputDeviceId(audioDeviceId) - }, [audioDeviceId, saveAudioInputDeviceId]) - - useEffect(() => { - saveVideoInputDeviceId(videoDeviceId) - }, [videoDeviceId, saveVideoInputDeviceId]) - - useEffect(() => { - saveUsername(username) - }, [username, saveUsername]) - useEffect(() => { saveProcessorSerialized(processor?.serialize()) }, [ @@ -161,8 +147,8 @@ export const Join = ({ const tracks = usePreviewTracks( { - audio: { deviceId: initialUserChoices.audioDeviceId }, - video: { deviceId: initialUserChoices.videoDeviceId }, + audio: { deviceId: audioDeviceId }, + video: { deviceId: videoDeviceId }, }, onError ) @@ -351,9 +337,9 @@ export const Join = ({ !value && t('errors.usernameEmpty')} wrapperProps={{ noMargin: true, @@ -474,11 +460,11 @@ export const Join = ({ source={Track.Source.Microphone} initialState={audioEnabled} track={audioTrack} - initialDeviceId={initialUserChoices.audioDeviceId} - onChange={(enabled) => setAudioEnabled(enabled)} + initialDeviceId={audioDeviceId} + onChange={(enabled) => saveAudioInputEnabled(enabled)} onDeviceError={(error) => console.error(error)} onActiveDeviceChange={(deviceId) => - setAudioDeviceId(deviceId ?? '') + saveAudioInputDeviceId(deviceId ?? '') } variant="tertiary" /> @@ -486,11 +472,11 @@ export const Join = ({ source={Track.Source.Camera} initialState={videoEnabled} track={videoTrack} - initialDeviceId={initialUserChoices.videoDeviceId} - onChange={(enabled) => setVideoEnabled(enabled)} + initialDeviceId={videoDeviceId} + onChange={(enabled) => saveVideoInputEnabled(enabled)} onDeviceError={(error) => console.error(error)} onActiveDeviceChange={(deviceId) => - setVideoDeviceId(deviceId ?? '') + saveVideoInputDeviceId(deviceId ?? '') } variant="tertiary" /> 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 46dd442c..240f36e6 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx @@ -99,7 +99,7 @@ export const SelectToggleDevice = ({ const { t } = useTranslation('rooms', { keyPrefix: 'join' }) const trackProps = useTrackToggle(props) - const { userChoices } = usePersistentUserChoices({}) + const { userChoices } = usePersistentUserChoices() const toggle = () => { if (props.source === Track.Source.Camera) { diff --git a/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts b/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts index afe43715..074adf2c 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts @@ -1,71 +1,31 @@ -import { UsePersistentUserChoicesOptions } from '@livekit/components-react' -import React from 'react' -import { LocalUserChoices } from '../../routes/Room' -import { saveUserChoices, loadUserChoices } from '@livekit/components-core' -import { ProcessorSerialized } from '../components/blur' +import { useSnapshot } from 'valtio' +import { userChoicesStore } from '@/stores/userChoices' +import { ProcessorSerialized } from '@/features/rooms/livekit/components/blur' -/** - * From @livekit/component-react - * - * A hook that provides access to user choices stored in local storage, such as - * selected media devices and their current state (on or off), as well as the user name. - * @alpha - */ -export function usePersistentUserChoices( - options: UsePersistentUserChoicesOptions = {} -) { - const [userChoices, setSettings] = React.useState( - loadUserChoices(options.defaults, options.preventLoad ?? false) - ) - - const saveAudioInputEnabled = React.useCallback((isEnabled: boolean) => { - setSettings((prev: LocalUserChoices) => ({ - ...prev, - audioEnabled: isEnabled, - })) - }, []) - const saveVideoInputEnabled = React.useCallback((isEnabled: boolean) => { - setSettings((prev: LocalUserChoices) => ({ - ...prev, - videoEnabled: isEnabled, - })) - }, []) - const saveAudioInputDeviceId = React.useCallback((deviceId: string) => { - setSettings((prev: LocalUserChoices) => ({ - ...prev, - audioDeviceId: deviceId, - })) - }, []) - const saveVideoInputDeviceId = React.useCallback((deviceId: string) => { - setSettings((prev: LocalUserChoices) => ({ - ...prev, - videoDeviceId: deviceId, - })) - }, []) - const saveUsername = React.useCallback((username: string) => { - setSettings((prev: LocalUserChoices) => ({ ...prev, username: username })) - }, []) - const saveProcessorSerialized = React.useCallback( - (processorSerialized?: ProcessorSerialized) => { - setSettings((prev: LocalUserChoices) => ({ - ...prev, - processorSerialized, - })) - }, - [] - ) - - React.useEffect(() => { - saveUserChoices(userChoices, options.preventSave ?? false) - }, [userChoices, options.preventSave]) +export function usePersistentUserChoices() { + const userChoicesSnap = useSnapshot(userChoicesStore) return { - userChoices, - saveAudioInputEnabled, - saveVideoInputEnabled, - saveAudioInputDeviceId, - saveVideoInputDeviceId, - saveUsername, - saveProcessorSerialized, + userChoices: userChoicesSnap, + saveAudioInputEnabled: (isEnabled: boolean) => { + userChoicesStore.audioEnabled = isEnabled + }, + saveVideoInputEnabled: (isEnabled: boolean) => { + userChoicesStore.videoEnabled = isEnabled + }, + saveAudioInputDeviceId: (deviceId: string) => { + userChoicesStore.audioDeviceId = deviceId + }, + saveVideoInputDeviceId: (deviceId: string) => { + userChoicesStore.videoDeviceId = deviceId + }, + saveUsername: (username: string) => { + userChoicesStore.username = username + }, + saveProcessorSerialized: ( + processorSerialized: ProcessorSerialized | undefined + ) => { + userChoicesStore.processorSerialized = processorSerialized + }, } } diff --git a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/ControlBar.tsx b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/ControlBar.tsx index 397ec3e4..018d8e2f 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/ControlBar.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/ControlBar.tsx @@ -47,16 +47,13 @@ export interface ControlBarProps extends React.HTMLAttributes { * ``` * @public */ -export function ControlBar({ - saveUserChoices = true, - onDeviceError, -}: ControlBarProps) { +export function ControlBar({ onDeviceError }: ControlBarProps) { const { saveAudioInputEnabled, saveVideoInputEnabled, saveAudioInputDeviceId, saveVideoInputDeviceId, - } = usePersistentUserChoices({ preventSave: !saveUserChoices }) + } = usePersistentUserChoices() const microphoneOnChange = React.useCallback( (enabled: boolean, isUserInitiated: boolean) => diff --git a/src/frontend/src/features/rooms/routes/Room.tsx b/src/frontend/src/features/rooms/routes/Room.tsx index 552c3002..765d12a4 100644 --- a/src/frontend/src/features/rooms/routes/Room.tsx +++ b/src/frontend/src/features/rooms/routes/Room.tsx @@ -1,23 +1,16 @@ import { useEffect, useState } from 'react' -import { - usePersistentUserChoices, - type LocalUserChoices as LocalUserChoicesLK, -} from '@livekit/components-react' +import { usePersistentUserChoices } from '@livekit/components-react' import { useLocation, useParams } from 'wouter' import { ErrorScreen } from '@/components/ErrorScreen' import { useUser, UserAware } from '@/features/auth' import { Conference } from '../components/Conference' import { Join } from '../components/Join' import { useKeyboardShortcuts } from '@/features/shortcuts/useKeyboardShortcuts' -import { ProcessorSerialized } from '../livekit/components/blur' import { isRoomValid, normalizeRoomId, } from '@/features/rooms/utils/isRoomValid' - -export type LocalUserChoices = LocalUserChoicesLK & { - processorSerialized?: ProcessorSerialized -} +import { LocalUserChoices } from '@/stores/userChoices' export const Room = () => { const { isLoggedIn } = useUser() diff --git a/src/frontend/src/stores/userChoices.ts b/src/frontend/src/stores/userChoices.ts new file mode 100644 index 00000000..60ec9fc5 --- /dev/null +++ b/src/frontend/src/stores/userChoices.ts @@ -0,0 +1,21 @@ +import { proxy, subscribe } from 'valtio' +import { ProcessorSerialized } from '@/features/rooms/livekit/components/blur' +import { + loadUserChoices, + saveUserChoices, + LocalUserChoices as LocalUserChoicesLK, +} from '@livekit/components-core' + +export type LocalUserChoices = LocalUserChoicesLK & { + processorSerialized?: ProcessorSerialized +} + +function getUserChoicesState(): LocalUserChoices { + return loadUserChoices() +} + +export const userChoicesStore = proxy(getUserChoicesState()) + +subscribe(userChoicesStore, () => { + saveUserChoices(userChoicesStore, false) +})