♻️(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 { useTranslation } from 'react-i18next'
|
||||||
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
||||||
import { Button, Popover } from '@/primitives'
|
import { Button, Popover } from '@/primitives'
|
||||||
import { RiArrowUpSLine, RiMicLine, RiMicOffLine } from '@remixicon/react'
|
import { RiArrowUpSLine } from '@remixicon/react'
|
||||||
import { Track } from 'livekit-client'
|
import { Track } from 'livekit-client'
|
||||||
|
|
||||||
import { ToggleDevice } from './ToggleDevice'
|
import { ToggleDevice } from './ToggleDevice'
|
||||||
@@ -26,8 +26,6 @@ export const AudioDevicesControl = ({
|
|||||||
}: AudioDevicesControlProps) => {
|
}: AudioDevicesControlProps) => {
|
||||||
const config: ToggleDeviceConfig = {
|
const config: ToggleDeviceConfig = {
|
||||||
kind: 'audioinput',
|
kind: 'audioinput',
|
||||||
iconOn: RiMicLine,
|
|
||||||
iconOff: RiMicOffLine,
|
|
||||||
shortcut: {
|
shortcut: {
|
||||||
key: 'd',
|
key: 'd',
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
import {
|
|
||||||
RemixiconComponentType,
|
|
||||||
RiMicLine,
|
|
||||||
RiVideoOnLine,
|
|
||||||
RiVolumeDownLine,
|
|
||||||
} from '@remixicon/react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useMediaDeviceSelect } from '@livekit/components-react'
|
import { useMediaDeviceSelect } from '@livekit/components-react'
|
||||||
import { useEffect, useMemo } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { Select } from '@/primitives/Select'
|
import { Select, SelectProps } from '@/primitives/Select'
|
||||||
import { Placement } from '@react-types/overlays'
|
import { Placement } from '@react-types/overlays'
|
||||||
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||||
|
import { useDeviceIcons } from '@/features/rooms/livekit/hooks/useDeviceIcons'
|
||||||
|
|
||||||
type DeviceItems = Array<{ value: string; label: string }>
|
type DeviceItems = Array<{ value: string; label: string }>
|
||||||
|
|
||||||
type DeviceConfig = {
|
|
||||||
icon: RemixiconComponentType
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectDeviceContext = {
|
type SelectDeviceContext = {
|
||||||
variant?: 'light' | 'dark'
|
variant?: 'light' | 'dark'
|
||||||
placement?: Placement
|
placement?: Placement
|
||||||
@@ -29,20 +20,19 @@ type SelectDeviceProps = {
|
|||||||
context?: 'join' | 'room'
|
context?: 'join' | 'room'
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectDevicePermissionsProps = SelectDeviceProps & {
|
type SelectDevicePermissionsProps<T> = SelectDeviceProps &
|
||||||
config: DeviceConfig
|
Pick<SelectProps<T>, 'placement' | 'variant'>
|
||||||
contextProps: SelectDeviceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
const SelectDevicePermissions = ({
|
const SelectDevicePermissions = <T extends string | number>({
|
||||||
id,
|
id,
|
||||||
kind,
|
kind,
|
||||||
config,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
contextProps,
|
...props
|
||||||
}: SelectDevicePermissionsProps) => {
|
}: SelectDevicePermissionsProps<T>) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
|
const deviceIcons = useDeviceIcons(kind)
|
||||||
|
|
||||||
const { devices, activeDeviceId, setActiveMediaDevice } =
|
const { devices, activeDeviceId, setActiveMediaDevice } =
|
||||||
useMediaDeviceSelect({ kind: kind, requestPermissions: true })
|
useMediaDeviceSelect({ kind: kind, requestPermissions: true })
|
||||||
|
|
||||||
@@ -75,7 +65,7 @@ const SelectDevicePermissions = ({
|
|||||||
label=""
|
label=""
|
||||||
isDisabled={items.length === 0}
|
isDisabled={items.length === 0}
|
||||||
items={items}
|
items={items}
|
||||||
iconComponent={config?.icon}
|
iconComponent={deviceIcons.select}
|
||||||
placeholder={
|
placeholder={
|
||||||
items.length === 0
|
items.length === 0
|
||||||
? t('selectDevice.loading')
|
? t('selectDevice.loading')
|
||||||
@@ -86,7 +76,7 @@ const SelectDevicePermissions = ({
|
|||||||
onSubmit?.(key as string)
|
onSubmit?.(key as string)
|
||||||
setActiveMediaDevice(key as string)
|
setActiveMediaDevice(key as string)
|
||||||
}}
|
}}
|
||||||
{...contextProps}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -106,27 +96,9 @@ export const SelectDevice = ({
|
|||||||
return {}
|
return {}
|
||||||
}, [context])
|
}, [context])
|
||||||
|
|
||||||
const config = useMemo<DeviceConfig | undefined>(() => {
|
const deviceIcons = useDeviceIcons(kind)
|
||||||
switch (kind) {
|
|
||||||
case 'audioinput':
|
|
||||||
return {
|
|
||||||
icon: RiMicLine,
|
|
||||||
}
|
|
||||||
case 'audiooutput':
|
|
||||||
return {
|
|
||||||
icon: RiVolumeDownLine,
|
|
||||||
}
|
|
||||||
case 'videoinput':
|
|
||||||
return {
|
|
||||||
icon: RiVideoOnLine,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [kind])
|
|
||||||
|
|
||||||
const cannotUseDevice = useCannotUseDevice(kind)
|
const cannotUseDevice = useCannotUseDevice(kind)
|
||||||
|
|
||||||
if (!config) return null
|
|
||||||
|
|
||||||
if (cannotUseDevice) {
|
if (cannotUseDevice) {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -134,8 +106,8 @@ export const SelectDevice = ({
|
|||||||
label=""
|
label=""
|
||||||
isDisabled={true}
|
isDisabled={true}
|
||||||
items={[]}
|
items={[]}
|
||||||
iconComponent={config?.icon}
|
|
||||||
placeholder={t('selectDevice.permissionsNeeded')}
|
placeholder={t('selectDevice.permissionsNeeded')}
|
||||||
|
iconComponent={deviceIcons.select}
|
||||||
{...contextProps}
|
{...contextProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -146,8 +118,7 @@ export const SelectDevice = ({
|
|||||||
id={id}
|
id={id}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
config={config}
|
{...contextProps}
|
||||||
contextProps={contextProps}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
|||||||
import { openPermissionsDialog } from '@/stores/permissions'
|
import { openPermissionsDialog } from '@/stores/permissions'
|
||||||
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
|
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
|
||||||
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||||
|
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
|
||||||
|
|
||||||
export type ToggleDeviceProps = {
|
export type ToggleDeviceProps = {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@@ -36,7 +37,7 @@ export const ToggleDevice = ({
|
|||||||
}: ToggleDeviceProps) => {
|
}: ToggleDeviceProps) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
const { kind, shortcut, iconOn, iconOff, longPress } = config
|
const { kind, shortcut, longPress } = config
|
||||||
|
|
||||||
const [pushToTalk, setPushToTalk] = useState(false)
|
const [pushToTalk, setPushToTalk] = useState(false)
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ export const ToggleDevice = ({
|
|||||||
setPushToTalk(false)
|
setPushToTalk(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deviceIcons = useDeviceIcons(kind)
|
||||||
const cannotUseDevice = useCannotUseDevice(kind)
|
const cannotUseDevice = useCannotUseDevice(kind)
|
||||||
|
|
||||||
useRegisterKeyboardShortcut({ shortcut, handler: toggle })
|
useRegisterKeyboardShortcut({ shortcut, handler: toggle })
|
||||||
@@ -63,7 +65,8 @@ export const ToggleDevice = ({
|
|||||||
return shortcut ? appendShortcutLabel(label, shortcut) : label
|
return shortcut ? appendShortcutLabel(label, shortcut) : label
|
||||||
}, [enabled, kind, shortcut, t])
|
}, [enabled, kind, shortcut, t])
|
||||||
|
|
||||||
const Icon = enabled && !cannotUseDevice ? iconOn : iconOff
|
const Icon =
|
||||||
|
enabled && !cannotUseDevice ? deviceIcons.toggleOn : deviceIcons.toggleOff
|
||||||
|
|
||||||
const context = useMaybeRoomContext()
|
const context = useMaybeRoomContext()
|
||||||
if (kind === 'audioinput' && pushToTalk && context) {
|
if (kind === 'audioinput' && pushToTalk && context) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
||||||
import { Button, Popover } from '@/primitives'
|
import { Button, Popover } from '@/primitives'
|
||||||
import { RiArrowUpSLine, RiVideoOffLine, RiVideoOnLine } from '@remixicon/react'
|
import { RiArrowUpSLine } from '@remixicon/react'
|
||||||
import { Track, VideoCaptureOptions } from 'livekit-client'
|
import { Track, VideoCaptureOptions } from 'livekit-client'
|
||||||
|
|
||||||
import { ToggleDevice } from './ToggleDevice'
|
import { ToggleDevice } from './ToggleDevice'
|
||||||
@@ -27,8 +27,6 @@ export const VideoDeviceControl = ({
|
|||||||
}: VideoDeviceControlProps) => {
|
}: VideoDeviceControlProps) => {
|
||||||
const config: ToggleDeviceConfig = {
|
const config: ToggleDeviceConfig = {
|
||||||
kind: 'videoinput',
|
kind: 'videoinput',
|
||||||
iconOn: RiVideoOnLine,
|
|
||||||
iconOff: RiVideoOffLine,
|
|
||||||
shortcut: {
|
shortcut: {
|
||||||
key: 'e',
|
key: 'e',
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import { Track } from 'livekit-client'
|
import { Track } from 'livekit-client'
|
||||||
import {
|
|
||||||
RemixiconComponentType,
|
|
||||||
RiMicLine,
|
|
||||||
RiMicOffLine,
|
|
||||||
RiVideoOffLine,
|
|
||||||
RiVideoOnLine,
|
|
||||||
} from '@remixicon/react'
|
|
||||||
import { Shortcut } from '@/features/shortcuts/types'
|
import { Shortcut } from '@/features/shortcuts/types'
|
||||||
|
|
||||||
export type ToggleSource = Exclude<
|
export type ToggleSource = Exclude<
|
||||||
@@ -17,8 +10,6 @@ export type ToggleSource = Exclude<
|
|||||||
|
|
||||||
export type ToggleDeviceConfig = {
|
export type ToggleDeviceConfig = {
|
||||||
kind: MediaDeviceKind
|
kind: MediaDeviceKind
|
||||||
iconOn: RemixiconComponentType
|
|
||||||
iconOff: RemixiconComponentType
|
|
||||||
shortcut?: Shortcut
|
shortcut?: Shortcut
|
||||||
longPress?: Shortcut
|
longPress?: Shortcut
|
||||||
}
|
}
|
||||||
@@ -30,8 +21,6 @@ type ToggleDeviceConfigMap = {
|
|||||||
export const TOGGLE_DEVICE_CONFIG = {
|
export const TOGGLE_DEVICE_CONFIG = {
|
||||||
[Track.Source.Microphone]: {
|
[Track.Source.Microphone]: {
|
||||||
kind: 'audioinput',
|
kind: 'audioinput',
|
||||||
iconOn: RiMicLine,
|
|
||||||
iconOff: RiMicOffLine,
|
|
||||||
shortcut: {
|
shortcut: {
|
||||||
key: 'd',
|
key: 'd',
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
@@ -42,8 +31,6 @@ export const TOGGLE_DEVICE_CONFIG = {
|
|||||||
},
|
},
|
||||||
[Track.Source.Camera]: {
|
[Track.Source.Camera]: {
|
||||||
kind: 'videoinput',
|
kind: 'videoinput',
|
||||||
iconOn: RiVideoOnLine,
|
|
||||||
iconOff: RiVideoOffLine,
|
|
||||||
shortcut: {
|
shortcut: {
|
||||||
key: 'e',
|
key: 'e',
|
||||||
ctrlKey: true,
|
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,
|
ListBox,
|
||||||
ListBoxItem,
|
ListBoxItem,
|
||||||
Select as RACSelect,
|
Select as RACSelect,
|
||||||
SelectProps,
|
SelectProps as RACSelectProps,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
import { Box } from './Box'
|
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>({
|
export const Select = <T extends string | number>({
|
||||||
label,
|
label,
|
||||||
iconComponent,
|
iconComponent,
|
||||||
@@ -96,14 +108,7 @@ export const Select = <T extends string | number>({
|
|||||||
placement,
|
placement,
|
||||||
variant = 'light',
|
variant = 'light',
|
||||||
...props
|
...props
|
||||||
}: Omit<SelectProps<object>, 'items' | 'label' | 'errors'> & {
|
}: SelectProps<T>) => {
|
||||||
iconComponent?: RemixiconComponentType
|
|
||||||
label: ReactNode
|
|
||||||
items: Array<{ value: T; label: ReactNode }>
|
|
||||||
errors?: ReactNode
|
|
||||||
placement?: Placement
|
|
||||||
variant?: 'light' | 'dark'
|
|
||||||
}) => {
|
|
||||||
const IconComponent = iconComponent
|
const IconComponent = iconComponent
|
||||||
return (
|
return (
|
||||||
<RACSelect {...props}>
|
<RACSelect {...props}>
|
||||||
|
|||||||
Reference in New Issue
Block a user