♻️(frontend) refactor settings from context to valtio global store

Replace settings context provider with valtio global store for easier
access outside room components and better long-term maintainability.

Prepares for upcoming prejoin screen settings access by making settings
globally available without React context limitations.
This commit is contained in:
lebaudantoine
2025-08-22 13:05:50 +02:00
committed by aleb_the_flash
parent e80b9c2485
commit c1c2d0260d
10 changed files with 70 additions and 79 deletions

View File

@@ -1,8 +1,8 @@
import { useSettingsDialog } from '../SettingsDialogContext'
import { Button } from '@/primitives'
import { RiSettings3Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { SettingsDialogExtendedKey } from '@/features/settings/type'
import { useSettingsDialog } from '@/features/settings/hook/useSettingsDialog'
export const SettingsButton = ({
settingTab,
@@ -12,7 +12,7 @@ export const SettingsButton = ({
onPress?: () => void
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
const { setDialogOpen, setDefaultSelectedKey } = useSettingsDialog()
const { openSettingsDialog } = useSettingsDialog()
return (
<Button
@@ -22,8 +22,7 @@ export const SettingsButton = ({
aria-label={t(`settings.${settingTab}`)}
variant="primaryDark"
onPress={() => {
setDefaultSelectedKey(settingTab)
setDialogOpen(true)
openSettingsDialog(settingTab)
onPress?.()
}}
>

View File

@@ -2,16 +2,16 @@ import { RiSettings3Line } from '@remixicon/react'
import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe'
import { useSettingsDialog } from '../SettingsDialogContext'
import { useSettingsDialog } from '@/features/settings/hook/useSettingsDialog'
export const SettingsMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { setDialogOpen } = useSettingsDialog()
const { openSettingsDialog } = useSettingsDialog()
return (
<MenuItem
className={menuRecipe({ icon: true, variant: 'dark' }).item}
onAction={() => setDialogOpen(true)}
onAction={() => openSettingsDialog()}
>
<RiSettings3Line size={20} />
{t('settings')}

View File

@@ -1,57 +0,0 @@
import React, { createContext, useContext, useState } from 'react'
import { SettingsDialogExtended } from '@/features/settings/components/SettingsDialogExtended'
import { SettingsDialogExtendedKey } from '@/features/settings/type'
const SettingsDialogContext = createContext<
| {
dialogOpen: boolean
defaultSelectedKey?: SettingsDialogExtendedKey
setDefaultSelectedKey: React.Dispatch<
React.SetStateAction<SettingsDialogExtendedKey | undefined>
>
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>
}
| undefined
>(undefined)
export const SettingsDialogProvider: React.FC<{
children: React.ReactNode
}> = ({ children }) => {
const [defaultSelectedKey, setDefaultSelectedKey] = useState<
SettingsDialogExtendedKey | undefined
>(undefined)
const [dialogOpen, setDialogOpen] = useState(false)
return (
<SettingsDialogContext.Provider
value={{
dialogOpen,
setDialogOpen,
defaultSelectedKey,
setDefaultSelectedKey,
}}
>
{children}
<SettingsDialogExtended
isOpen={dialogOpen}
defaultSelectedKey={defaultSelectedKey}
onOpenChange={(v) => {
if (!v) {
setDefaultSelectedKey(undefined)
}
setDialogOpen(v)
}}
/>
</SettingsDialogContext.Provider>
)
}
// eslint-disable-next-line react-refresh/only-export-components
export const useSettingsDialog = () => {
const context = useContext(SettingsDialogContext)
if (!context) {
throw new Error(
'useSettingsDialog must be used within a SettingsDialogProvider'
)
}
return context
}

View File

@@ -3,7 +3,6 @@ import * as React from 'react'
import { MobileControlBar } from './MobileControlBar'
import { DesktopControlBar } from './DesktopControlBar'
import { SettingsDialogProvider } from '../../components/controls/SettingsDialogContext'
import { useIsMobile } from '@/utils/useIsMobile'
export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -16,15 +15,11 @@ export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
*/
export function ControlBar({ onDeviceError }: ControlBarProps) {
const isMobile = useIsMobile()
return (
<SettingsDialogProvider>
{isMobile ? (
<MobileControlBar onDeviceError={onDeviceError} />
) : (
<DesktopControlBar onDeviceError={onDeviceError} />
)}
</SettingsDialogProvider>
)
if (isMobile) {
return <MobileControlBar onDeviceError={onDeviceError} />
}
return <DesktopControlBar onDeviceError={onDeviceError} />
}
export type ControlBarAuxProps = Pick<ControlBarProps, 'onDeviceError'>

View File

@@ -18,13 +18,13 @@ import { ChatToggle } from '../../components/controls/ChatToggle'
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
import { useSidePanel } from '../../hooks/useSidePanel'
import { LinkButton } from '@/primitives'
import { useSettingsDialog } from '../../components/controls/SettingsDialogContext'
import { ResponsiveMenu } from './ResponsiveMenu'
import { ToolsToggle } from '../../components/controls/ToolsToggle'
import { CameraSwitchButton } from '../../components/controls/CameraSwitchButton'
import { useConfig } from '@/api/useConfig'
import { AudioDevicesControl } from '../../components/controls/Device/AudioDevicesControl'
import { VideoDeviceControl } from '../../components/controls/Device/VideoDeviceControl'
import { useSettingsDialog } from '@/features/settings/hook/useSettingsDialog'
export function MobileControlBar({
onDeviceError,
@@ -33,7 +33,7 @@ export function MobileControlBar({
const [isMenuOpened, setIsMenuOpened] = React.useState(false)
const browserSupportsScreenSharing = supportsScreenSharing()
const { toggleEffects } = useSidePanel()
const { setDialogOpen } = useSettingsDialog()
const { openSettingsDialog } = useSettingsDialog()
const { data } = useConfig()
@@ -152,7 +152,7 @@ export function MobileControlBar({
)}
<Button
onPress={() => {
setDialogOpen(true)
openSettingsDialog()
setIsMenuOpened(false)
}}
variant="primaryTextDark"

View File

@@ -33,6 +33,7 @@ import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
import { useConnectionObserver } from '../hooks/useConnectionObserver'
import { useNoiseReduction } from '../hooks/useNoiseReduction'
import { useVideoResolutionSubscription } from '../hooks/useVideoResolutionSubscription'
import { SettingsDialogProvider } from '@/features/settings/components/SettingsDialogProvider'
const LayoutWrapper = styled(
'div',
@@ -240,6 +241,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
<RoomAudioRenderer />
<ConnectionStateToast />
<RecordingStateToast />
<SettingsDialogProvider />
</div>
)
}

View File

@@ -48,7 +48,7 @@ export type SettingsDialogExtended = Pick<
DialogProps,
'isOpen' | 'onOpenChange'
> & {
defaultSelectedKey?: SettingsDialogExtendedKey
defaultSelectedTab?: SettingsDialogExtendedKey
}
export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
@@ -63,7 +63,7 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
<Tabs
orientation="vertical"
className={tabsStyle}
defaultSelectedKey={props.defaultSelectedKey}
defaultSelectedKey={props.defaultSelectedTab}
>
<div
className={tabListContainerStyle}

View File

@@ -0,0 +1,20 @@
import { SettingsDialogExtended } from './SettingsDialogExtended'
import { useSnapshot } from 'valtio'
import { settingsStore } from '@/stores/settings'
export const SettingsDialogProvider = () => {
const { areSettingsOpen, defaultSelectedTab } = useSnapshot(settingsStore)
return (
<SettingsDialogExtended
isOpen={areSettingsOpen}
defaultSelectedTab={defaultSelectedTab}
onOpenChange={(v) => {
if (!v && settingsStore.defaultSelectedTab) {
settingsStore.defaultSelectedTab = undefined
}
settingsStore.areSettingsOpen = v
}}
/>
)
}

View File

@@ -0,0 +1,20 @@
import { useSnapshot } from 'valtio/index'
import { settingsStore } from '@/stores/settings'
import { SettingsDialogExtendedKey } from '@/features/settings/type'
export const useSettingsDialog = () => {
const { areSettingsOpen } = useSnapshot(settingsStore)
const openSettingsDialog = (
defaultSelectedTab?: SettingsDialogExtendedKey
) => {
if (areSettingsOpen) return
if (defaultSelectedTab)
settingsStore.defaultSelectedTab = defaultSelectedTab
settingsStore.areSettingsOpen = true
}
return {
openSettingsDialog,
}
}

View File

@@ -0,0 +1,12 @@
import { proxy } from 'valtio'
import { SettingsDialogExtendedKey } from '@/features/settings/type'
type State = {
areSettingsOpen: boolean
defaultSelectedTab?: SettingsDialogExtendedKey
}
export const settingsStore = proxy<State>({
areSettingsOpen: false,
defaultSelectedTab: undefined,
})