♻️(frontend) unify toggle components into single flexible implementation

Replace separate prejoin and room toggle components with unified
component that's adaptable and easier to evolve without overfitting.

Adds responsibilities to join component but eliminates duplication. Join
component needs future refactoring as complexity is growing
significantly.
This commit is contained in:
lebaudantoine
2025-08-22 00:45:11 +02:00
committed by aleb_the_flash
parent f17e0a3ba0
commit 6f3339fbdc
5 changed files with 60 additions and 80 deletions

View File

@@ -21,6 +21,7 @@ import {
EffectsConfigurationProps,
} 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'
@@ -34,7 +35,6 @@ import { Spinner } from '@/primitives/Spinner'
import { ApiAccessLevel } from '../api/ApiRoom'
import { useLoginHint } from '@/hooks/useLoginHint'
import { openPermissionsDialog } from '@/stores/permissions'
import { ToggleDevice } from './join/ToggleDevice'
import { useResolveInitiallyDefaultDeviceId } from '../livekit/hooks/useResolveInitiallyDefaultDeviceId'
import { isSafari } from '@/utils/livekit'
import type { LocalUserChoices } from '@/stores/userChoices'
@@ -661,15 +661,29 @@ export const Join = ({
>
<ToggleDevice
kind="audioinput"
initialState={audioEnabled}
track={audioTrack}
onChange={(enabled) => saveAudioInputEnabled(enabled)}
context="join"
enabled={audioEnabled}
toggle={async () => {
saveAudioInputEnabled(!audioEnabled)
if (audioEnabled) {
await audioTrack?.mute()
} else {
await audioTrack?.unmute()
}
}}
/>
<ToggleDevice
kind="videoinput"
initialState={videoEnabled}
track={videoTrack}
onChange={(enabled) => saveVideoInputEnabled(enabled)}
context="join"
enabled={videoEnabled}
toggle={async () => {
saveVideoInputEnabled(!videoEnabled)
if (videoEnabled) {
await videoTrack?.mute()
} else {
await videoTrack?.unmute()
}
}}
/>
</div>
<div

View File

@@ -1,59 +0,0 @@
import { UseTrackToggleProps } from '@livekit/components-react'
import { ToggleDevice as BaseToggleDevice } from '../../livekit/components/controls/Device/ToggleDevice'
import { LocalAudioTrack, LocalVideoTrack, Track } from 'livekit-client'
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { useCallback, useState } from 'react'
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
kind: MediaDeviceKind
variant?: NonNullable<ButtonRecipeProps>['variant']
}
export const ToggleDevice = <T extends ToggleSource>({
track,
kind,
onChange,
initialState,
}: ToggleDeviceProps<T>) => {
const [isTrackEnabled, setIsTrackEnabled] = useState(initialState ?? false)
const toggle = useCallback(async () => {
try {
if (isTrackEnabled) {
setIsTrackEnabled(false)
onChange?.(false, true)
await track?.mute()
} else {
setIsTrackEnabled(true)
onChange?.(true, true)
await track?.unmute()
}
} catch (error) {
console.error('Failed to toggle track:', error)
}
}, [track, onChange, isTrackEnabled])
return (
<BaseToggleDevice
enabled={isTrackEnabled}
toggle={toggle}
kind={kind}
variant="whiteCircle"
errorVariant="errorCircle"
toggleButtonProps={{
groupPosition: undefined,
}}
/>
)
}

View File

@@ -57,8 +57,7 @@ export const AudioDevicesControl = ({
<ToggleDevice
{...trackProps}
kind="audioinput"
variant="primaryDark"
toggle={trackProps.toggle}
toggle={trackProps.toggle as () => Promise<void>}
toggleButtonProps={{
...(hideMenu
? {

View File

@@ -17,26 +17,52 @@ import { openPermissionsDialog } from '@/stores/permissions'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
import { useDeviceShortcut } from '../../../hooks/useDeviceShortcut'
import { ToggleSource, CaptureOptionsBySource } from '@livekit/components-core'
export type ToggleDeviceProps = {
enabled: boolean
toggle: () => void
kind: 'audioinput' | 'videoinput'
type ToggleDeviceStyleProps = {
variant?: NonNullable<ButtonRecipeProps>['variant']
errorVariant?: NonNullable<ButtonRecipeProps>['variant']
toggleButtonProps?: Partial<ToggleButtonProps>
}
export const ToggleDevice = ({
export type ToggleDeviceProps<T extends ToggleSource> = {
enabled: boolean
toggle: (
forceState?: boolean,
captureOptions?: CaptureOptionsBySource<T>
) => Promise<void | boolean | undefined> | void
context?: 'room' | 'join'
kind: 'audioinput' | 'videoinput'
toggleButtonProps?: Partial<ToggleButtonProps>
}
export const ToggleDevice = <T extends ToggleSource>({
kind,
enabled,
toggle,
variant = 'primaryDark',
errorVariant = 'error2',
toggleButtonProps,
}: ToggleDeviceProps) => {
context = 'room',
...props
}: ToggleDeviceProps<T>) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const { variant, errorVariant, toggleButtonProps } =
useMemo<ToggleDeviceStyleProps>(() => {
if (context === 'join') {
return {
variant: 'whiteCircle',
errorVariant: 'errorCircle',
toggleButtonProps: {
groupPosition: undefined,
},
} as ToggleDeviceStyleProps
}
return {
variant: 'primaryDark',
errorVariant: 'error2',
toggleButtonProps: undefined,
} as ToggleDeviceStyleProps
}, [context])
const [pushToTalk, setPushToTalk] = useState(false)
const onKeyDown = () => {
@@ -71,8 +97,8 @@ export const ToggleDevice = ({
const Icon =
enabled && !cannotUseDevice ? deviceIcons.toggleOn : deviceIcons.toggleOff
const context = useMaybeRoomContext()
if (kind === 'audioinput' && pushToTalk && context) {
const roomContext = useMaybeRoomContext()
if (kind === 'audioinput' && pushToTalk && roomContext) {
return <ActiveSpeakerWrapper />
}
@@ -92,6 +118,7 @@ export const ToggleDevice = ({
}
groupPosition="left"
{...toggleButtonProps}
{...props}
>
<Icon />
</ToggleButton>

View File

@@ -82,7 +82,6 @@ export const VideoDeviceControl = ({
<ToggleDevice
{...trackProps}
kind="videoinput"
variant="primaryDark"
toggle={toggle}
toggleButtonProps={{
...(hideMenu