♻️(frontend) extract permission checks into reusable hook by media kind

Create hook to encapsulate permission denied/prompted/loading checks
based on media kind, eliminating props drilling and simplifying code.

Returns appropriate permission state for consuming components based on
media type, cleaning up code structure with small enhancement.
This commit is contained in:
lebaudantoine
2025-08-21 21:56:17 +02:00
committed by aleb_the_flash
parent ebf676529f
commit 22c68da2af
7 changed files with 57 additions and 74 deletions

View File

@@ -33,12 +33,12 @@ import { ApiLobbyStatus, ApiRequestEntry } from '../api/requestEntry'
import { Spinner } from '@/primitives/Spinner'
import { ApiAccessLevel } from '../api/ApiRoom'
import { useLoginHint } from '@/hooks/useLoginHint'
import { useSnapshot } from 'valtio'
import { openPermissionsDialog, permissionsStore } from '@/stores/permissions'
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'
import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice'
const onError = (e: Error) => console.error('ERROR', e)
@@ -350,13 +350,8 @@ export const Join = ({
enterRoom()
}
const permissions = useSnapshot(permissionsStore)
const isCameraDeniedOrPrompted =
permissions.isCameraDenied || permissions.isCameraPrompted
const isMicrophoneDeniedOrPrompted =
permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
const isCameraDeniedOrPrompted = useCannotUseDevice('videoinput')
const isMicrophoneDeniedOrPrompted = useCannotUseDevice('audioinput')
const hintMessage = useMemo(() => {
if (isCameraDeniedOrPrompted) {

View File

@@ -6,9 +6,7 @@ import {
} from '../../livekit/config/ToggleDeviceConfig'
import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client'
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { useCallback, useMemo, useState } from 'react'
import { useSnapshot } from 'valtio'
import { permissionsStore } from '@/stores/permissions'
import { useCallback, useState } from 'react'
type ToggleDeviceProps<T extends ToggleSource> = UseTrackToggleProps<T> & {
track?: LocalAudioTrack | LocalVideoTrack
@@ -31,17 +29,6 @@ export const ToggleDevice = <T extends ToggleSource>({
props.initialState ?? false
)
const permissions = useSnapshot(permissionsStore)
const isPermissionDeniedOrPrompted = useMemo(() => {
if (config.kind == 'audioinput') {
return permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
}
if (config.kind == 'videoinput') {
return permissions.isCameraDenied || permissions.isCameraPrompted
}
}, [config, permissions])
const toggle = useCallback(async () => {
try {
if (isTrackEnabled) {
@@ -61,7 +48,6 @@ export const ToggleDevice = <T extends ToggleSource>({
return (
<BaseToggleDevice
enabled={isTrackEnabled}
isPermissionDeniedOrPrompted={isPermissionDeniedOrPrompted}
toggle={toggle}
config={config}
variant="whiteCircle"

View File

@@ -7,8 +7,7 @@ import { Track } from 'livekit-client'
import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx'
import { css } from '@/styled-system/css'
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
import { useSnapshot } from 'valtio'
import { permissionsStore } from '@/stores/permissions'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
import Source = Track.Source
import * as React from 'react'
@@ -58,10 +57,7 @@ export const AudioDevicesControl = ({
...props,
})
const permissions = useSnapshot(permissionsStore)
const isPermissionDeniedOrPrompted =
permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
const cannotUseDevice = useCannotUseDevice('audioinput')
const selectLabel = t('audioinput.choose')
return (
@@ -76,7 +72,6 @@ export const AudioDevicesControl = ({
config={config}
variant="primaryDark"
toggle={trackProps.toggle}
isPermissionDeniedOrPrompted={isPermissionDeniedOrPrompted}
toggleButtonProps={{
...(hideMenu
? {
@@ -88,15 +83,12 @@ export const AudioDevicesControl = ({
{!hideMenu && (
<Popover variant="dark" withArrow={false}>
<Button
isDisabled={isPermissionDeniedOrPrompted}
tooltip={selectLabel}
aria-label={selectLabel}
groupPosition="right"
square
variant={
trackProps.enabled && !isPermissionDeniedOrPrompted
? 'primaryDark'
: 'error2'
trackProps.enabled && !cannotUseDevice ? 'primaryDark' : 'error2'
}
>
<RiArrowUpSLine />

View File

@@ -8,9 +8,8 @@ import { useTranslation } from 'react-i18next'
import { useMediaDeviceSelect } from '@livekit/components-react'
import { useEffect, useMemo } from 'react'
import { Select } from '@/primitives/Select'
import { useSnapshot } from 'valtio'
import { permissionsStore } from '@/stores/permissions'
import { Placement } from '@react-types/overlays'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
type DeviceItems = Array<{ value: string; label: string }>
@@ -100,8 +99,6 @@ export const SelectDevice = ({
}: SelectDeviceProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const permissions = useSnapshot(permissionsStore)
const contextProps = useMemo<SelectDeviceContext>(() => {
if (context == 'room') {
return { variant: 'dark', placement: 'top' }
@@ -126,22 +123,11 @@ export const SelectDevice = ({
}
}, [kind])
const isPermissionDeniedOrPrompted = useMemo(() => {
if (kind == 'audioinput') {
return permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
}
if (kind == 'videoinput') {
return permissions.isCameraDenied || permissions.isCameraPrompted
}
if (kind == 'audiooutput') {
return permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
}
return false
}, [kind, permissions])
const cannotUseDevice = useCannotUseDevice(kind)
if (!config) return null
if (isPermissionDeniedOrPrompted || permissions.isLoading) {
if (cannotUseDevice) {
return (
<Select
aria-label={t(`${kind}.permissionsNeeded`)}

View File

@@ -7,9 +7,8 @@ import { Track, VideoCaptureOptions } from 'livekit-client'
import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice'
import { css } from '@/styled-system/css'
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import { BackgroundProcessorFactory } from '../../blur'
import { useSnapshot } from 'valtio'
import { permissionsStore } from '@/stores/permissions'
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
import Source = Track.Source
import * as React from 'react'
@@ -53,10 +52,7 @@ export const VideoDeviceControl = ({
...props,
})
const permissions = useSnapshot(permissionsStore)
const isPermissionDeniedOrPrompted =
permissions.isCameraDenied || permissions.isCameraPrompted
const cannotUseDevice = useCannotUseDevice('videoinput')
const toggle = () => {
/**
@@ -99,7 +95,6 @@ export const VideoDeviceControl = ({
config={config}
variant="primaryDark"
toggle={toggle}
isPermissionDeniedOrPrompted={isPermissionDeniedOrPrompted}
toggleButtonProps={{
...(hideMenu
? {
@@ -111,15 +106,12 @@ export const VideoDeviceControl = ({
{!hideMenu && (
<Popover variant="dark" withArrow={false}>
<Button
isDisabled={isPermissionDeniedOrPrompted}
tooltip={selectLabel}
aria-label={selectLabel}
groupPosition="right"
square
variant={
trackProps.enabled && !isPermissionDeniedOrPrompted
? 'primaryDark'
: 'error2'
trackProps.enabled && !cannotUseDevice ? 'primaryDark' : 'error2'
}
>
<RiArrowUpSLine />

View File

@@ -15,10 +15,10 @@ import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
import { openPermissionsDialog } from '@/stores/permissions'
import { ToggleDeviceConfig } from '../../config/ToggleDeviceConfig'
import { useCannotUseDevice } from '../../hooks/useCannotUseDevice'
export type ToggleDeviceProps = {
enabled: boolean
isPermissionDeniedOrPrompted?: boolean
toggle: () => void
config: ToggleDeviceConfig
variant?: NonNullable<ButtonRecipeProps>['variant']
@@ -33,7 +33,6 @@ export const ToggleDevice = ({
variant = 'primaryDark',
errorVariant = 'error2',
toggleButtonProps,
isPermissionDeniedOrPrompted,
}: ToggleDeviceProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
@@ -52,6 +51,8 @@ export const ToggleDevice = ({
setPushToTalk(false)
}
const cannotUseDevice = useCannotUseDevice(kind)
useRegisterKeyboardShortcut({ shortcut, handler: toggle })
useLongPress({ keyCode: longPress?.key, onKeyDown, onKeyUp })
@@ -62,7 +63,7 @@ export const ToggleDevice = ({
return shortcut ? appendShortcutLabel(label, shortcut) : label
}, [enabled, kind, shortcut, t])
const Icon = enabled && !isPermissionDeniedOrPrompted ? iconOn : iconOff
const Icon = enabled && !cannotUseDevice ? iconOn : iconOff
const context = useMaybeRoomContext()
if (kind === 'audioinput' && pushToTalk && context) {
@@ -71,19 +72,15 @@ export const ToggleDevice = ({
return (
<div style={{ position: 'relative' }}>
{isPermissionDeniedOrPrompted && <PermissionNeededButton />}
{cannotUseDevice && <PermissionNeededButton />}
<ToggleButton
isSelected={!enabled}
variant={
enabled && !isPermissionDeniedOrPrompted ? variant : errorVariant
}
variant={enabled && !cannotUseDevice ? variant : errorVariant}
shySelected
onPress={() =>
isPermissionDeniedOrPrompted ? openPermissionsDialog() : toggle()
}
onPress={() => (cannotUseDevice ? openPermissionsDialog() : toggle())}
aria-label={toggleLabel}
tooltip={
isPermissionDeniedOrPrompted
cannotUseDevice
? t('tooltip', { keyPrefix: 'permissionsButton' })
: toggleLabel
}

View File

@@ -0,0 +1,35 @@
import { useSnapshot } from 'valtio'
import { useMemo } from 'react'
import { permissionsStore } from '@/stores/permissions'
export const useCannotUseDevice = (kind: MediaDeviceKind) => {
const {
isLoading,
isMicrophoneDenied,
isMicrophonePrompted,
isCameraDenied,
isCameraPrompted,
} = useSnapshot(permissionsStore)
return useMemo(() => {
if (isLoading) return true
switch (kind) {
case 'audioinput':
case 'audiooutput': // audiooutput uses microphone permissions
return isMicrophoneDenied || isMicrophonePrompted
case 'videoinput':
return isCameraDenied || isCameraPrompted
default:
return false
}
}, [
kind,
isLoading,
isMicrophoneDenied,
isMicrophonePrompted,
isCameraDenied,
isCameraPrompted,
])
}