♻️(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:
committed by
aleb_the_flash
parent
5eb70384e3
commit
2367750395
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user