♻️(frontend) extract media icon logic into optimized reusable hook

Create simple hook to assign icons to toggle/select components based on
media kind using dictionary lookup for optimization.

Eliminates duplicate icon assignment logic across components with
straightforward, performant implementation that's easy to maintain.
This commit is contained in:
lebaudantoine
2025-08-21 22:58:31 +02:00
committed by aleb_the_flash
parent 5eb70384e3
commit 2367750395
7 changed files with 72 additions and 73 deletions

View File

@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next'
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
import { Button, Popover } from '@/primitives'
import { RiArrowUpSLine, RiMicLine, RiMicOffLine } from '@remixicon/react'
import { RiArrowUpSLine } from '@remixicon/react'
import { Track } from 'livekit-client'
import { ToggleDevice } from './ToggleDevice'
@@ -26,8 +26,6 @@ export const AudioDevicesControl = ({
}: AudioDevicesControlProps) => {
const config: ToggleDeviceConfig = {
kind: 'audioinput',
iconOn: RiMicLine,
iconOff: RiMicOffLine,
shortcut: {
key: 'd',
ctrlKey: true,

View File

@@ -1,22 +1,13 @@
import {
RemixiconComponentType,
RiMicLine,
RiVideoOnLine,
RiVolumeDownLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useMediaDeviceSelect } from '@livekit/components-react'
import { useEffect, useMemo } from 'react'
import { Select } from '@/primitives/Select'
import { Select, SelectProps } from '@/primitives/Select'
import { Placement } from '@react-types/overlays'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import { useDeviceIcons } from '@/features/rooms/livekit/hooks/useDeviceIcons'
type DeviceItems = Array<{ value: string; label: string }>
type DeviceConfig = {
icon: RemixiconComponentType
}
type SelectDeviceContext = {
variant?: 'light' | 'dark'
placement?: Placement
@@ -29,20 +20,19 @@ type SelectDeviceProps = {
context?: 'join' | 'room'
}
type SelectDevicePermissionsProps = SelectDeviceProps & {
config: DeviceConfig
contextProps: SelectDeviceContext
}
type SelectDevicePermissionsProps<T> = SelectDeviceProps &
Pick<SelectProps<T>, 'placement' | 'variant'>
const SelectDevicePermissions = ({
const SelectDevicePermissions = <T extends string | number>({
id,
kind,
config,
onSubmit,
contextProps,
}: SelectDevicePermissionsProps) => {
...props
}: SelectDevicePermissionsProps<T>) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const deviceIcons = useDeviceIcons(kind)
const { devices, activeDeviceId, setActiveMediaDevice } =
useMediaDeviceSelect({ kind: kind, requestPermissions: true })
@@ -75,7 +65,7 @@ const SelectDevicePermissions = ({
label=""
isDisabled={items.length === 0}
items={items}
iconComponent={config?.icon}
iconComponent={deviceIcons.select}
placeholder={
items.length === 0
? t('selectDevice.loading')
@@ -86,7 +76,7 @@ const SelectDevicePermissions = ({
onSubmit?.(key as string)
setActiveMediaDevice(key as string)
}}
{...contextProps}
{...props}
/>
)
}
@@ -106,27 +96,9 @@ export const SelectDevice = ({
return {}
}, [context])
const config = useMemo<DeviceConfig | undefined>(() => {
switch (kind) {
case 'audioinput':
return {
icon: RiMicLine,
}
case 'audiooutput':
return {
icon: RiVolumeDownLine,
}
case 'videoinput':
return {
icon: RiVideoOnLine,
}
}
}, [kind])
const deviceIcons = useDeviceIcons(kind)
const cannotUseDevice = useCannotUseDevice(kind)
if (!config) return null
if (cannotUseDevice) {
return (
<Select
@@ -134,8 +106,8 @@ export const SelectDevice = ({
label=""
isDisabled={true}
items={[]}
iconComponent={config?.icon}
placeholder={t('selectDevice.permissionsNeeded')}
iconComponent={deviceIcons.select}
{...contextProps}
/>
)
@@ -146,8 +118,7 @@ export const SelectDevice = ({
id={id}
onSubmit={onSubmit}
kind={kind}
config={config}
contextProps={contextProps}
{...contextProps}
/>
)
}

View File

@@ -16,6 +16,7 @@ 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'
export type ToggleDeviceProps = {
enabled: boolean
@@ -36,7 +37,7 @@ export const ToggleDevice = ({
}: ToggleDeviceProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const { kind, shortcut, iconOn, iconOff, longPress } = config
const { kind, shortcut, longPress } = config
const [pushToTalk, setPushToTalk] = useState(false)
@@ -51,6 +52,7 @@ export const ToggleDevice = ({
setPushToTalk(false)
}
const deviceIcons = useDeviceIcons(kind)
const cannotUseDevice = useCannotUseDevice(kind)
useRegisterKeyboardShortcut({ shortcut, handler: toggle })
@@ -63,7 +65,8 @@ export const ToggleDevice = ({
return shortcut ? appendShortcutLabel(label, shortcut) : label
}, [enabled, kind, shortcut, t])
const Icon = enabled && !cannotUseDevice ? iconOn : iconOff
const Icon =
enabled && !cannotUseDevice ? deviceIcons.toggleOn : deviceIcons.toggleOff
const context = useMaybeRoomContext()
if (kind === 'audioinput' && pushToTalk && context) {

View File

@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next'
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
import { Button, Popover } from '@/primitives'
import { RiArrowUpSLine, RiVideoOffLine, RiVideoOnLine } from '@remixicon/react'
import { RiArrowUpSLine } from '@remixicon/react'
import { Track, VideoCaptureOptions } from 'livekit-client'
import { ToggleDevice } from './ToggleDevice'
@@ -27,8 +27,6 @@ export const VideoDeviceControl = ({
}: VideoDeviceControlProps) => {
const config: ToggleDeviceConfig = {
kind: 'videoinput',
iconOn: RiVideoOnLine,
iconOff: RiVideoOffLine,
shortcut: {
key: 'e',
ctrlKey: true,

View File

@@ -1,11 +1,4 @@
import { Track } from 'livekit-client'
import {
RemixiconComponentType,
RiMicLine,
RiMicOffLine,
RiVideoOffLine,
RiVideoOnLine,
} from '@remixicon/react'
import { Shortcut } from '@/features/shortcuts/types'
export type ToggleSource = Exclude<
@@ -17,8 +10,6 @@ export type ToggleSource = Exclude<
export type ToggleDeviceConfig = {
kind: MediaDeviceKind
iconOn: RemixiconComponentType
iconOff: RemixiconComponentType
shortcut?: Shortcut
longPress?: Shortcut
}
@@ -30,8 +21,6 @@ type ToggleDeviceConfigMap = {
export const TOGGLE_DEVICE_CONFIG = {
[Track.Source.Microphone]: {
kind: 'audioinput',
iconOn: RiMicLine,
iconOff: RiMicOffLine,
shortcut: {
key: 'd',
ctrlKey: true,
@@ -42,8 +31,6 @@ export const TOGGLE_DEVICE_CONFIG = {
},
[Track.Source.Camera]: {
kind: 'videoinput',
iconOn: RiVideoOnLine,
iconOff: RiVideoOffLine,
shortcut: {
key: 'e',
ctrlKey: true,

View File

@@ -0,0 +1,37 @@
import {
RemixiconComponentType,
RiMicLine,
RiMicOffLine,
RiVideoOffLine,
RiVideoOnLine,
RiVolumeDownLine,
RiVolumeMuteLine,
} from '@remixicon/react'
export interface DeviceIcons {
toggleOn: RemixiconComponentType
toggleOff: RemixiconComponentType
select: RemixiconComponentType
}
const ICONS: Record<MediaDeviceKind | 'default', DeviceIcons> = {
audioinput: {
toggleOn: RiMicLine,
toggleOff: RiMicOffLine,
select: RiMicLine,
},
videoinput: {
toggleOn: RiVideoOnLine,
toggleOff: RiVideoOffLine,
select: RiVideoOnLine,
},
audiooutput: {
toggleOn: RiVolumeDownLine,
toggleOff: RiVolumeMuteLine,
select: RiVolumeDownLine,
},
default: { toggleOn: RiMicLine, toggleOff: RiMicOffLine, select: RiMicLine },
}
export const useDeviceIcons = (kind: MediaDeviceKind): DeviceIcons =>
ICONS[kind] ?? ICONS.default

View File

@@ -6,7 +6,7 @@ import {
ListBox,
ListBoxItem,
Select as RACSelect,
SelectProps,
SelectProps as RACSelectProps,
SelectValue,
} from 'react-aria-components'
import { Box } from './Box'
@@ -88,6 +88,18 @@ const StyledIcon = styled('div', {
},
})
export type SelectProps<T> = Omit<
RACSelectProps<object>,
'items' | 'label' | 'errors'
> & {
iconComponent?: RemixiconComponentType
label: ReactNode
items: Array<{ value: T; label: ReactNode }>
errors?: ReactNode
placement?: Placement
variant?: 'light' | 'dark'
}
export const Select = <T extends string | number>({
label,
iconComponent,
@@ -96,14 +108,7 @@ export const Select = <T extends string | number>({
placement,
variant = 'light',
...props
}: Omit<SelectProps<object>, 'items' | 'label' | 'errors'> & {
iconComponent?: RemixiconComponentType
label: ReactNode
items: Array<{ value: T; label: ReactNode }>
errors?: ReactNode
placement?: Placement
variant?: 'light' | 'dark'
}) => {
}: SelectProps<T>) => {
const IconComponent = iconComponent
return (
<RACSelect {...props}>