♻️(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:
committed by
aleb_the_flash
parent
ebf676529f
commit
22c68da2af
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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`)}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
}
|
||||
Reference in New Issue
Block a user