From 4a18e188e4c0b85c14e30d8693d59b86629743a7 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 24 Jun 2025 19:11:44 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20refactor=20usePe?= =?UTF-8?q?rsistentUserChoice=20to=20fix=20state=20sync=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I may have introduced a misusage of the usePersistentUserChoice hook. I ended using it while expecting it to be a global state, it wasn't. Fix broken global state that caused user choice desync. Use LiveKit default persistence functions similar to notification store approach. Carefully handles existing localStorage data to prevent regressions. Note: Audio output persistence will be added in future commits. --- .../features/rooms/components/Conference.tsx | 2 +- .../src/features/rooms/components/Join.tsx | 60 +++++------- .../controls/SelectToggleDevice.tsx | 2 +- .../livekit/hooks/usePersistentUserChoices.ts | 92 ++++++------------- .../livekit/prefabs/ControlBar/ControlBar.tsx | 7 +- .../src/features/rooms/routes/Room.tsx | 11 +-- src/frontend/src/stores/userChoices.ts | 21 +++++ 7 files changed, 76 insertions(+), 119 deletions(-) create mode 100644 src/frontend/src/stores/userChoices.ts 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) +})