♻️(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
@@ -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
@@ -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,
}}
/>
)
}
@@ -57,8 +57,7 @@ export const AudioDevicesControl = ({
<ToggleDevice
{...trackProps}
kind="audioinput"
variant="primaryDark"
toggle={trackProps.toggle}
toggle={trackProps.toggle as () => Promise<void>}
toggleButtonProps={{
...(hideMenu
? {
@@ -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>
@@ -82,7 +82,6 @@ export const VideoDeviceControl = ({
<ToggleDevice
{...trackProps}
kind="videoinput"
variant="primaryDark"
toggle={toggle}
toggleButtonProps={{
...(hideMenu