From 04ec967a9919048771323623d4de55f2504d5221 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 26 May 2026 16:27:48 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F(frontend)=20handle=20actions?= =?UTF-8?q?=20on=20userChoices=20store=20directly=20in=20the=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Valtio actions on the store were initially defined inside a hook, which was a bad idea: it does not follow Valtio's recommendation of defining actions at the module level, and the function definitions were re-created every time the hook re-rendered, which happened on every state update. These action has nothing related to React. Additionally, the snapshot should be used more directly so Valtio can understand and optimize which parts of the proxy are of interest to the snapshot being made. --- .../src/features/home/routes/Home.tsx | 7 ++- .../src/features/rooms/components/Join.tsx | 39 +++++++++-------- .../controls/Device/AudioDevicesControl.tsx | 18 ++++---- .../controls/Device/VideoDeviceControl.tsx | 20 +++++---- .../effects/EffectsConfiguration.tsx | 16 ++----- .../rooms/livekit/hooks/useNoiseReduction.ts | 7 ++- .../livekit/hooks/usePersistentUserChoices.ts | 43 ------------------- .../hooks/useVideoResolutionSubscription.ts | 7 ++- .../settings/components/tabs/AccountTab.tsx | 3 +- .../settings/components/tabs/AudioTab.tsx | 16 ++++--- .../settings/components/tabs/VideoTab.tsx | 26 +++++------ src/frontend/src/stores/userChoices.ts | 42 ++++++++++++++++++ 12 files changed, 121 insertions(+), 123 deletions(-) delete mode 100644 src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts diff --git a/src/frontend/src/features/home/routes/Home.tsx b/src/frontend/src/features/home/routes/Home.tsx index 5a50daf8..cc2f7f28 100644 --- a/src/frontend/src/features/home/routes/Home.tsx +++ b/src/frontend/src/features/home/routes/Home.tsx @@ -16,11 +16,12 @@ import { ReactNode, useEffect, useState } from 'react' import { css } from '@/styled-system/css' import { menuRecipe } from '@/primitives/menuRecipe.ts' -import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices' import { useConfig } from '@/api/useConfig' import { LoginButton } from '@/components/LoginButton' import { ApiRoom } from '@/features/rooms/api/ApiRoom' import { LoadingScreen } from '@/components/LoadingScreen' +import { useSnapshot } from 'valtio' +import { userChoicesStore } from '@/stores/userChoices' const Columns = ({ children }: { children?: ReactNode }) => { return ( @@ -148,9 +149,7 @@ const IntroText = styled('div', { }) const CreateMeetingMenu = () => { - const { - userChoices: { username }, - } = usePersistentUserChoices() + const { username } = useSnapshot(userChoicesStore) const { t } = useTranslation('home') const { mutateAsync: createRoom } = useCreateRoom() diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index 3d56f192..fd1a9964 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -22,7 +22,6 @@ import { } from '../livekit/components/effects/EffectsConfiguration' import { SelectDevice } from '../livekit/components/controls/Device/SelectDevice' import { ToggleDevice } from '../livekit/components/controls/Device/ToggleDevice' -import { usePersistentUserChoices } from '../livekit/hooks/usePersistentUserChoices' import { BackgroundProcessorFactory } from '../livekit/components/blur' import { isMobileBrowser } from '@livekit/components-core' import { fetchRoom } from '@/features/rooms/api/fetchRoom' @@ -37,8 +36,20 @@ import { useLoginHint } from '@/hooks/useLoginHint' import { openPermissionsDialog } from '@/stores/permissions' import { useResolveInitiallyDefaultDeviceId } from '../livekit/hooks/useResolveInitiallyDefaultDeviceId' import { isSafari } from '@/utils/livekit' -import type { LocalUserChoices } from '@/stores/userChoices' + +import { + type LocalUserChoices, + saveAudioInputDeviceId, + saveAudioInputEnabled, + saveAudioOutputDeviceId, + saveUsername, + saveVideoInputDeviceId, + saveVideoInputEnabled, + userChoicesStore, +} from '@/stores/userChoices' + import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice' +import { useSnapshot } from 'valtio' const onError = (e: Error) => console.error('ERROR', e) @@ -104,22 +115,14 @@ export const Join = ({ const { t } = useTranslation('rooms', { keyPrefix: 'join' }) const { - userChoices: { - audioEnabled, - videoEnabled, - audioDeviceId, - audioOutputDeviceId, - videoDeviceId, - processorConfig, - username, - }, - saveAudioInputEnabled, - saveAudioOutputDeviceId, - saveVideoInputEnabled, - saveAudioInputDeviceId, - saveVideoInputDeviceId, - saveUsername, - } = usePersistentUserChoices() + audioEnabled, + videoEnabled, + audioDeviceId, + audioOutputDeviceId, + videoDeviceId, + processorConfig, + username, + } = useSnapshot(userChoicesStore) const initialUserChoices = useRef(null) diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx index 6132bbc0..b6b52815 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx @@ -6,7 +6,6 @@ import { Track } from 'livekit-client' import { ToggleDevice } from './ToggleDevice' import { css } from '@/styled-system/css' -import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices' import { useCanPublishTrack } from '../../../hooks/useCanPublishTrack' import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice' import * as React from 'react' @@ -16,6 +15,14 @@ import { SettingsDialogExtendedKey } from '@/features/settings/type' import { TrackSource } from '@livekit/protocol' import Source = Track.Source import { isSafari } from '@/utils/livekit' +import { + saveAudioInputDeviceId, + saveAudioInputEnabled, + saveAudioOutputDeviceId, + userChoicesStore, +} from '@/stores/userChoices' + +import { useSnapshot } from 'valtio' type AudioDevicesControlProps = Omit< UseTrackToggleProps, @@ -30,17 +37,12 @@ export const AudioDevicesControl = ({ }: AudioDevicesControlProps) => { const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' }) - const { - userChoices: { audioDeviceId, audioOutputDeviceId }, - saveAudioInputDeviceId, - saveAudioInputEnabled, - saveAudioOutputDeviceId, - } = usePersistentUserChoices() + const { audioDeviceId, audioOutputDeviceId } = useSnapshot(userChoicesStore) const onChange = React.useCallback( (enabled: boolean, isUserInitiated: boolean) => isUserInitiated ? saveAudioInputEnabled(enabled) : null, - [saveAudioInputEnabled] + [] ) const trackProps = useTrackToggle({ diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx index bae6dcd7..62f2f294 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx @@ -6,7 +6,6 @@ import { Track, type VideoCaptureOptions } from 'livekit-client' import { ToggleDevice } from './ToggleDevice' import { css } from '@/styled-system/css' -import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices' import { useCanPublishTrack } from '../../../hooks/useCanPublishTrack' import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice' import { useSidePanel } from '../../../hooks/useSidePanel' @@ -17,6 +16,13 @@ import { SelectDevice } from './SelectDevice' import { SettingsButton } from './SettingsButton' import { SettingsDialogExtendedKey } from '@/features/settings/type' import { TrackSource } from '@livekit/protocol' +import { useSnapshot } from 'valtio' + +import { + saveVideoInputDeviceId, + saveVideoInputEnabled, + userChoicesStore, +} from '@/stores/userChoices' const EffectsButton = ({ onPress }: { onPress: () => void }) => { const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' }) @@ -51,13 +57,12 @@ export const VideoDeviceControl = ({ }: VideoDeviceControlProps) => { const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' }) - const { userChoices, saveVideoInputDeviceId, saveVideoInputEnabled } = - usePersistentUserChoices() + const { videoDeviceId, processorConfig } = useSnapshot(userChoicesStore) const onChange = React.useCallback( (enabled: boolean, isUserInitiated: boolean) => isUserInitiated ? saveVideoInputEnabled(enabled) : null, - [saveVideoInputEnabled] + [] ) const trackProps = useTrackToggle({ @@ -82,9 +87,8 @@ export const VideoDeviceControl = ({ * * See https://github.com/numerique-gouv/meet/pull/309#issuecomment-2622404121 */ - const processor = BackgroundProcessorFactory.fromProcessorConfig( - userChoices.processorConfig - ) + const processor = + BackgroundProcessorFactory.fromProcessorConfig(processorConfig) const toggle = trackProps.toggle as ( forceState: boolean, @@ -152,7 +156,7 @@ export const VideoDeviceControl = ({ diff --git a/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx b/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx index 1a537c2f..52f55880 100644 --- a/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx +++ b/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx @@ -30,9 +30,9 @@ import { useDeleteFile } from '@/features/files/api/deleteFile.ts' import { useUser } from '@/features/auth/api/useUser' import { ApiFileItem } from '@/features/files/api/types.ts' import { useConfig } from '@/api/useConfig.ts' -import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices.ts' import { proxy, useSnapshot } from 'valtio' import { Spinner } from '@/primitives/Spinner.tsx' +import { userChoicesStore, saveProcessorConfig } from '@/stores/userChoices' enum BlurRadius { NONE = 0, @@ -114,10 +114,7 @@ export const EffectsConfiguration = ({ > | null>(null) const effectAnnouncementId = useRef(0) - const { - saveProcessorConfig, - userChoices: { processorConfig }, - } = usePersistentUserChoices() + const { processorConfig } = useSnapshot(userChoicesStore) const selectedId = useMemo( () => @@ -247,14 +244,7 @@ export const EffectsConfiguration = ({ setTimeout(() => setProcessorPending(false)) } }, - [ - enabled, - saveProcessorConfig, - selectedId, - toggle, - updateEffectStatusMessage, - videoTrack, - ] + [enabled, selectedId, toggle, updateEffectStatusMessage, videoTrack] ) const { data: appConfig } = useConfig() diff --git a/src/frontend/src/features/rooms/livekit/hooks/useNoiseReduction.ts b/src/frontend/src/features/rooms/livekit/hooks/useNoiseReduction.ts index 64388f67..5fb38521 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useNoiseReduction.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useNoiseReduction.ts @@ -2,16 +2,15 @@ import { useEffect } from 'react' import { Track } from 'livekit-client' import { useRoomContext } from '@livekit/components-react' import { RnnNoiseProcessor } from '../processors/RnnNoiseProcessor' -import { usePersistentUserChoices } from './usePersistentUserChoices' import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable' +import { useSnapshot } from 'valtio' +import { userChoicesStore } from '@/stores/userChoices' export const useNoiseReduction = () => { const room = useRoomContext() const noiseReductionAvailable = useNoiseReductionAvailable() - const { - userChoices: { noiseReductionEnabled }, - } = usePersistentUserChoices() + const { noiseReductionEnabled } = useSnapshot(userChoicesStore) const audioTrack = room.localParticipant.getTrackPublication( Track.Source.Microphone diff --git a/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts b/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts deleted file mode 100644 index 8edc2b82..00000000 --- a/src/frontend/src/features/rooms/livekit/hooks/usePersistentUserChoices.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useSnapshot } from 'valtio' -import { userChoicesStore } from '@/stores/userChoices' -import type { VideoResolution } from '@/stores/userChoices' -import { ProcessorConfig } from '@/features/rooms/livekit/components/blur' -import type { VideoQuality } from 'livekit-client' - -export function usePersistentUserChoices() { - const userChoicesSnap = useSnapshot(userChoicesStore) - - return { - userChoices: userChoicesSnap, - saveAudioInputEnabled: (isEnabled: boolean) => { - userChoicesStore.audioEnabled = isEnabled - }, - saveVideoInputEnabled: (isEnabled: boolean) => { - userChoicesStore.videoEnabled = isEnabled - }, - saveAudioInputDeviceId: (deviceId: string) => { - userChoicesStore.audioDeviceId = deviceId - }, - saveAudioOutputDeviceId: (deviceId: string) => { - userChoicesStore.audioOutputDeviceId = deviceId - }, - saveVideoInputDeviceId: (deviceId: string) => { - userChoicesStore.videoDeviceId = deviceId - }, - saveVideoPublishResolution: (resolution: VideoResolution) => { - userChoicesStore.videoPublishResolution = resolution - }, - saveVideoSubscribeQuality: (quality: VideoQuality) => { - userChoicesStore.videoSubscribeQuality = quality - }, - saveUsername: (username: string) => { - userChoicesStore.username = username - }, - saveNoiseReductionEnabled: (enabled: boolean) => { - userChoicesStore.noiseReductionEnabled = enabled - }, - saveProcessorConfig: (processorConfig: ProcessorConfig | undefined) => { - userChoicesStore.processorConfig = processorConfig - }, - } -} diff --git a/src/frontend/src/features/rooms/livekit/hooks/useVideoResolutionSubscription.ts b/src/frontend/src/features/rooms/livekit/hooks/useVideoResolutionSubscription.ts index 878d2430..0ca5abde 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useVideoResolutionSubscription.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useVideoResolutionSubscription.ts @@ -1,5 +1,4 @@ import { useEffect } from 'react' -import { usePersistentUserChoices } from './usePersistentUserChoices' import { useRoomContext } from '@livekit/components-react' import { type RemoteParticipant, @@ -8,15 +7,15 @@ import { Track, VideoQuality, } from 'livekit-client' +import { useSnapshot } from 'valtio' +import { userChoicesStore } from '@/stores/userChoices' /** * This hook sets initial video quality for new participants as they join. * LiveKit doesn't allow handling video quality preferences at the room level. */ export const useVideoResolutionSubscription = () => { - const { - userChoices: { videoSubscribeQuality }, - } = usePersistentUserChoices() + const { videoSubscribeQuality } = useSnapshot(userChoicesStore) const room = useRoomContext() diff --git a/src/frontend/src/features/settings/components/tabs/AccountTab.tsx b/src/frontend/src/features/settings/components/tabs/AccountTab.tsx index 7376371d..1109c86d 100644 --- a/src/frontend/src/features/settings/components/tabs/AccountTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/AccountTab.tsx @@ -7,15 +7,14 @@ import { TabPanel, TabPanelProps } from '@/primitives/Tabs' import { HStack } from '@/styled-system/jsx' import { useState } from 'react' import { LoginButton } from '@/components/LoginButton' -import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices' import { useRenameParticipant } from '@/features/rooms/api/renameParticipant' +import { saveUsername } from '@/stores/userChoices' export type AccountTabProps = Pick & Pick export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => { const { t } = useTranslation('settings') - const { saveUsername } = usePersistentUserChoices() const room = useRoomContext() const { user, isLoggedIn, logout } = useUser() diff --git a/src/frontend/src/features/settings/components/tabs/AudioTab.tsx b/src/frontend/src/features/settings/components/tabs/AudioTab.tsx index ab3a7ffd..19d7f36d 100644 --- a/src/frontend/src/features/settings/components/tabs/AudioTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/AudioTab.tsx @@ -10,10 +10,16 @@ import { isSafari } from '@/utils/livekit' import { useTranslation } from 'react-i18next' import { SoundTester } from '@/components/SoundTester' import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker' -import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices' import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable' import posthog from 'posthog-js' import { RowWrapper } from './layout/RowWrapper' +import { useSnapshot } from 'valtio' +import { + saveAudioInputDeviceId, + saveAudioOutputDeviceId, + saveNoiseReductionEnabled, + userChoicesStore, +} from '@/stores/userChoices' export type AudioTabProps = Pick & Pick @@ -24,12 +30,8 @@ export const AudioTab = ({ id }: AudioTabProps) => { const { t } = useTranslation('settings') const { localParticipant } = useRoomContext() - const { - userChoices: { noiseReductionEnabled, audioDeviceId, audioOutputDeviceId }, - saveAudioInputDeviceId, - saveNoiseReductionEnabled, - saveAudioOutputDeviceId, - } = usePersistentUserChoices() + const { noiseReductionEnabled, audioDeviceId, audioOutputDeviceId } = + useSnapshot(userChoicesStore) const isSpeaking = useIsSpeaking(localParticipant) diff --git a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx index ae9608e1..7a002333 100644 --- a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx @@ -3,7 +3,6 @@ import { DialogProps, Field } from '@/primitives' import { TabPanel, type TabPanelProps } from '@/primitives/Tabs' import { useMediaDeviceSelect, useRoomContext } from '@livekit/components-react' import { useTranslation } from 'react-i18next' -import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices' import { useCallback, useEffect, useMemo, useState } from 'react' import { css } from '@/styled-system/css' import { @@ -14,8 +13,15 @@ import { VideoQuality, } from 'livekit-client' import { BackgroundProcessorFactory } from '@/features/rooms/livekit/components/blur' -import { VideoResolution } from '@/stores/userChoices' +import { + saveVideoInputDeviceId, + saveVideoPublishResolution, + saveVideoSubscribeQuality, + userChoicesStore, + VideoResolution, +} from '@/stores/userChoices' import { RowWrapper } from './layout/RowWrapper' +import { useSnapshot } from 'valtio' export type VideoTabProps = Pick & Pick @@ -29,16 +35,12 @@ export const VideoTab = ({ id }: VideoTabProps) => { const { localParticipant, remoteParticipants } = useRoomContext() const { - userChoices: { - videoDeviceId, - processorConfig, - videoPublishResolution, - videoSubscribeQuality, - }, - saveVideoInputDeviceId, - saveVideoPublishResolution, - saveVideoSubscribeQuality, - } = usePersistentUserChoices() + videoDeviceId, + processorConfig, + videoPublishResolution, + videoSubscribeQuality, + } = useSnapshot(userChoicesStore) + const [videoElement, setVideoElement] = useState( null ) diff --git a/src/frontend/src/stores/userChoices.ts b/src/frontend/src/stores/userChoices.ts index 963dea5d..bf3c0c2b 100644 --- a/src/frontend/src/stores/userChoices.ts +++ b/src/frontend/src/stores/userChoices.ts @@ -61,3 +61,45 @@ if (userChoicesStore.processorConfig?.type === ProcessorType.VIRTUAL) { }) } } + +export const saveAudioInputEnabled = (isEnabled: boolean) => { + userChoicesStore.audioEnabled = isEnabled +} + +export const saveVideoInputEnabled = (isEnabled: boolean) => { + userChoicesStore.videoEnabled = isEnabled +} + +export const saveAudioInputDeviceId = (deviceId: string) => { + userChoicesStore.audioDeviceId = deviceId +} + +export const saveAudioOutputDeviceId = (deviceId: string) => { + userChoicesStore.audioOutputDeviceId = deviceId +} + +export const saveVideoInputDeviceId = (deviceId: string) => { + userChoicesStore.videoDeviceId = deviceId +} + +export const saveVideoPublishResolution = (resolution: VideoResolution) => { + userChoicesStore.videoPublishResolution = resolution +} + +export const saveVideoSubscribeQuality = (quality: VideoQuality) => { + userChoicesStore.videoSubscribeQuality = quality +} + +export const saveUsername = (username: string) => { + userChoicesStore.username = username +} + +export const saveNoiseReductionEnabled = (enabled: boolean) => { + userChoicesStore.noiseReductionEnabled = enabled +} + +export const saveProcessorConfig = ( + processorConfig: ProcessorConfig | undefined +) => { + userChoicesStore.processorConfig = processorConfig +}