♻️(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 <ToggleDevice
source={Track.Source.Microphone} kind="audioinput"
initialState={audioEnabled} initialState={audioEnabled}
track={audioTrack} track={audioTrack}
onChange={(enabled) => saveAudioInputEnabled(enabled)} onChange={(enabled) => saveAudioInputEnabled(enabled)}
onDeviceError={(error) => console.error(error)}
/> />
<ToggleDevice <ToggleDevice
source={Track.Source.Camera} kind="videoinput"
initialState={videoEnabled} initialState={videoEnabled}
track={videoTrack} track={videoTrack}
onChange={(enabled) => saveVideoInputEnabled(enabled)} onChange={(enabled) => saveVideoInputEnabled(enabled)}
onDeviceError={(error) => console.error(error)}
/> />
</div> </div>
<div <div

View File

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

View File

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

View File

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

View File

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