♻️(frontend) replace toggle device config with keyboard shortcut hook

Remove ugly toggle device configuration and implement hook to determine
appropriate keyboard shortcuts based on media device kind.

Cleaner approach that encapsulates shortcut logic in reusable hook
instead of scattered configuration objects.
This commit is contained in:
lebaudantoine
2025-08-21 23:38:44 +02:00
committed by aleb_the_flash
parent 2367750395
commit f17e0a3ba0
7 changed files with 54 additions and 92 deletions

View File

@@ -660,18 +660,16 @@ export const Join = ({
})}
>
<ToggleDevice
source={Track.Source.Microphone}
kind="audioinput"
initialState={audioEnabled}
track={audioTrack}
onChange={(enabled) => saveAudioInputEnabled(enabled)}
onDeviceError={(error) => console.error(error)}
/>
<ToggleDevice
source={Track.Source.Camera}
kind="videoinput"
initialState={videoEnabled}
track={videoTrack}
onChange={(enabled) => saveVideoInputEnabled(enabled)}
onDeviceError={(error) => console.error(error)}
/>
</div>
<div

View File

@@ -1,33 +1,32 @@
import { UseTrackToggleProps } from '@livekit/components-react'
import { ToggleDevice as BaseToggleDevice } from '../../livekit/components/controls/Device/ToggleDevice'
import {
TOGGLE_DEVICE_CONFIG,
ToggleSource,
} from '../../livekit/config/ToggleDeviceConfig'
import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client'
import { LocalAudioTrack, LocalVideoTrack, Track } from 'livekit-client'
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { useCallback, useState } from 'react'
type ToggleDeviceProps<T extends ToggleSource> = UseTrackToggleProps<T> & {
type ToggleSource = Exclude<
Track.Source,
| Track.Source.ScreenShareAudio
| Track.Source.Unknown
| Track.Source.ScreenShare
>
type ToggleDeviceProps<T extends ToggleSource> = Pick<
UseTrackToggleProps<T>,
'onChange' | 'initialState'
> & {
track?: LocalAudioTrack | LocalVideoTrack
source: ToggleSource
kind: MediaDeviceKind
variant?: NonNullable<ButtonRecipeProps>['variant']
}
export const ToggleDevice = <T extends ToggleSource>({
track,
kind,
onChange,
...props
initialState,
}: ToggleDeviceProps<T>) => {
const config = TOGGLE_DEVICE_CONFIG[props.source]
if (!config) {
throw new Error('Invalid source')
}
const [isTrackEnabled, setIsTrackEnabled] = useState(
props.initialState ?? false
)
const [isTrackEnabled, setIsTrackEnabled] = useState(initialState ?? false)
const toggle = useCallback(async () => {
try {
@@ -49,7 +48,7 @@ export const ToggleDevice = <T extends ToggleSource>({
<BaseToggleDevice
enabled={isTrackEnabled}
toggle={toggle}
config={config}
kind={kind}
variant="whiteCircle"
errorVariant="errorCircle"
toggleButtonProps={{

View File

@@ -8,7 +8,6 @@ import { ToggleDevice } from './ToggleDevice'
import { css } from '@/styled-system/css'
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
import Source = Track.Source
import * as React from 'react'
import { SelectDevice } from './SelectDevice'
@@ -24,16 +23,6 @@ export const AudioDevicesControl = ({
hideMenu,
...props
}: AudioDevicesControlProps) => {
const config: ToggleDeviceConfig = {
kind: 'audioinput',
shortcut: {
key: 'd',
ctrlKey: true,
},
longPress: {
key: 'Space',
},
}
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const {
@@ -67,7 +56,7 @@ export const AudioDevicesControl = ({
>
<ToggleDevice
{...trackProps}
config={config}
kind="audioinput"
variant="primaryDark"
toggle={trackProps.toggle}
toggleButtonProps={{

View File

@@ -14,21 +14,21 @@ import {
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
import { openPermissionsDialog } from '@/stores/permissions'
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
import { useDeviceShortcut } from '../../../hooks/useDeviceShortcut'
export type ToggleDeviceProps = {
enabled: boolean
toggle: () => void
config: ToggleDeviceConfig
kind: 'audioinput' | 'videoinput'
variant?: NonNullable<ButtonRecipeProps>['variant']
errorVariant?: NonNullable<ButtonRecipeProps>['variant']
toggleButtonProps?: Partial<ToggleButtonProps>
}
export const ToggleDevice = ({
config,
kind,
enabled,
toggle,
variant = 'primaryDark',
@@ -37,8 +37,6 @@ export const ToggleDevice = ({
}: ToggleDeviceProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const { kind, shortcut, longPress } = config
const [pushToTalk, setPushToTalk] = useState(false)
const onKeyDown = () => {
@@ -54,16 +52,21 @@ export const ToggleDevice = ({
const deviceIcons = useDeviceIcons(kind)
const cannotUseDevice = useCannotUseDevice(kind)
const deviceShortcut = useDeviceShortcut(kind)
useRegisterKeyboardShortcut({ shortcut, handler: toggle })
useLongPress({ keyCode: longPress?.key, onKeyDown, onKeyUp })
useRegisterKeyboardShortcut({ shortcut: deviceShortcut, handler: toggle })
useLongPress({
keyCode: kind === 'audioinput' ? 'Space' : undefined,
onKeyDown,
onKeyUp,
})
const toggleLabel = useMemo(() => {
const label = t(enabled ? 'disable' : 'enable', {
keyPrefix: `join.${kind}`,
})
return shortcut ? appendShortcutLabel(label, shortcut) : label
}, [enabled, kind, shortcut, t])
return deviceShortcut ? appendShortcutLabel(label, deviceShortcut) : label
}, [enabled, kind, deviceShortcut, t])
const Icon =
enabled && !cannotUseDevice ? deviceIcons.toggleOn : deviceIcons.toggleOff

View File

@@ -9,7 +9,6 @@ import { css } from '@/styled-system/css'
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import { BackgroundProcessorFactory } from '../../blur'
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
import Source = Track.Source
import * as React from 'react'
import { SelectDevice } from './SelectDevice'
@@ -25,14 +24,6 @@ export const VideoDeviceControl = ({
hideMenu,
...props
}: VideoDeviceControlProps) => {
const config: ToggleDeviceConfig = {
kind: 'videoinput',
shortcut: {
key: 'e',
ctrlKey: true,
},
}
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const { userChoices, saveVideoInputDeviceId, saveVideoInputEnabled } =
@@ -90,7 +81,7 @@ export const VideoDeviceControl = ({
>
<ToggleDevice
{...trackProps}
config={config}
kind="videoinput"
variant="primaryDark"
toggle={toggle}
toggleButtonProps={{

View File

@@ -1,39 +0,0 @@
import { Track } from 'livekit-client'
import { Shortcut } from '@/features/shortcuts/types'
export type ToggleSource = Exclude<
Track.Source,
| Track.Source.ScreenShareAudio
| Track.Source.Unknown
| Track.Source.ScreenShare
>
export type ToggleDeviceConfig = {
kind: MediaDeviceKind
shortcut?: Shortcut
longPress?: Shortcut
}
type ToggleDeviceConfigMap = {
[key in ToggleSource]: ToggleDeviceConfig
}
export const TOGGLE_DEVICE_CONFIG = {
[Track.Source.Microphone]: {
kind: 'audioinput',
shortcut: {
key: 'd',
ctrlKey: true,
},
longPress: {
key: 'Space',
},
},
[Track.Source.Camera]: {
kind: 'videoinput',
shortcut: {
key: 'e',
ctrlKey: true,
},
},
} as const satisfies ToggleDeviceConfigMap

View File

@@ -0,0 +1,21 @@
import { useMemo } from 'react'
import { Shortcut } from '@/features/shortcuts/types'
export const useDeviceShortcut = (kind: MediaDeviceKind) => {
return useMemo<Shortcut | undefined>(() => {
switch (kind) {
case 'audioinput':
return {
key: 'e',
ctrlKey: true,
}
case 'videoinput':
return {
key: 'd',
ctrlKey: true,
}
default:
return undefined
}
}, [kind])
}