♻️(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 { Spinner } from '@/primitives/Spinner'
|
||||||
import { ApiAccessLevel } from '../api/ApiRoom'
|
import { ApiAccessLevel } from '../api/ApiRoom'
|
||||||
import { useLoginHint } from '@/hooks/useLoginHint'
|
import { useLoginHint } from '@/hooks/useLoginHint'
|
||||||
import { useSnapshot } from 'valtio'
|
import { openPermissionsDialog } from '@/stores/permissions'
|
||||||
import { openPermissionsDialog, permissionsStore } from '@/stores/permissions'
|
|
||||||
import { ToggleDevice } from './join/ToggleDevice'
|
import { ToggleDevice } from './join/ToggleDevice'
|
||||||
import { useResolveInitiallyDefaultDeviceId } from '../livekit/hooks/useResolveInitiallyDefaultDeviceId'
|
import { useResolveInitiallyDefaultDeviceId } from '../livekit/hooks/useResolveInitiallyDefaultDeviceId'
|
||||||
import { isSafari } from '@/utils/livekit'
|
import { isSafari } from '@/utils/livekit'
|
||||||
import type { LocalUserChoices } from '@/stores/userChoices'
|
import type { LocalUserChoices } from '@/stores/userChoices'
|
||||||
|
import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice'
|
||||||
|
|
||||||
const onError = (e: Error) => console.error('ERROR', e)
|
const onError = (e: Error) => console.error('ERROR', e)
|
||||||
|
|
||||||
@@ -350,13 +350,8 @@ export const Join = ({
|
|||||||
enterRoom()
|
enterRoom()
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = useSnapshot(permissionsStore)
|
const isCameraDeniedOrPrompted = useCannotUseDevice('videoinput')
|
||||||
|
const isMicrophoneDeniedOrPrompted = useCannotUseDevice('audioinput')
|
||||||
const isCameraDeniedOrPrompted =
|
|
||||||
permissions.isCameraDenied || permissions.isCameraPrompted
|
|
||||||
|
|
||||||
const isMicrophoneDeniedOrPrompted =
|
|
||||||
permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
|
|
||||||
|
|
||||||
const hintMessage = useMemo(() => {
|
const hintMessage = useMemo(() => {
|
||||||
if (isCameraDeniedOrPrompted) {
|
if (isCameraDeniedOrPrompted) {
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import {
|
|||||||
} from '../../livekit/config/ToggleDeviceConfig'
|
} from '../../livekit/config/ToggleDeviceConfig'
|
||||||
import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client'
|
import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client'
|
||||||
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||||
import { useCallback, useMemo, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { permissionsStore } from '@/stores/permissions'
|
|
||||||
|
|
||||||
type ToggleDeviceProps<T extends ToggleSource> = UseTrackToggleProps<T> & {
|
type ToggleDeviceProps<T extends ToggleSource> = UseTrackToggleProps<T> & {
|
||||||
track?: LocalAudioTrack | LocalVideoTrack
|
track?: LocalAudioTrack | LocalVideoTrack
|
||||||
@@ -31,17 +29,6 @@ export const ToggleDevice = <T extends ToggleSource>({
|
|||||||
props.initialState ?? false
|
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 () => {
|
const toggle = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (isTrackEnabled) {
|
if (isTrackEnabled) {
|
||||||
@@ -61,7 +48,6 @@ export const ToggleDevice = <T extends ToggleSource>({
|
|||||||
return (
|
return (
|
||||||
<BaseToggleDevice
|
<BaseToggleDevice
|
||||||
enabled={isTrackEnabled}
|
enabled={isTrackEnabled}
|
||||||
isPermissionDeniedOrPrompted={isPermissionDeniedOrPrompted}
|
|
||||||
toggle={toggle}
|
toggle={toggle}
|
||||||
config={config}
|
config={config}
|
||||||
variant="whiteCircle"
|
variant="whiteCircle"
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { Track } from 'livekit-client'
|
|||||||
import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx'
|
import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
|
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||||
import { permissionsStore } from '@/stores/permissions'
|
|
||||||
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
|
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
|
||||||
import Source = Track.Source
|
import Source = Track.Source
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
@@ -58,10 +57,7 @@ export const AudioDevicesControl = ({
|
|||||||
...props,
|
...props,
|
||||||
})
|
})
|
||||||
|
|
||||||
const permissions = useSnapshot(permissionsStore)
|
const cannotUseDevice = useCannotUseDevice('audioinput')
|
||||||
const isPermissionDeniedOrPrompted =
|
|
||||||
permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
|
|
||||||
|
|
||||||
const selectLabel = t('audioinput.choose')
|
const selectLabel = t('audioinput.choose')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -76,7 +72,6 @@ export const AudioDevicesControl = ({
|
|||||||
config={config}
|
config={config}
|
||||||
variant="primaryDark"
|
variant="primaryDark"
|
||||||
toggle={trackProps.toggle}
|
toggle={trackProps.toggle}
|
||||||
isPermissionDeniedOrPrompted={isPermissionDeniedOrPrompted}
|
|
||||||
toggleButtonProps={{
|
toggleButtonProps={{
|
||||||
...(hideMenu
|
...(hideMenu
|
||||||
? {
|
? {
|
||||||
@@ -88,15 +83,12 @@ export const AudioDevicesControl = ({
|
|||||||
{!hideMenu && (
|
{!hideMenu && (
|
||||||
<Popover variant="dark" withArrow={false}>
|
<Popover variant="dark" withArrow={false}>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isPermissionDeniedOrPrompted}
|
|
||||||
tooltip={selectLabel}
|
tooltip={selectLabel}
|
||||||
aria-label={selectLabel}
|
aria-label={selectLabel}
|
||||||
groupPosition="right"
|
groupPosition="right"
|
||||||
square
|
square
|
||||||
variant={
|
variant={
|
||||||
trackProps.enabled && !isPermissionDeniedOrPrompted
|
trackProps.enabled && !cannotUseDevice ? 'primaryDark' : 'error2'
|
||||||
? 'primaryDark'
|
|
||||||
: 'error2'
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RiArrowUpSLine />
|
<RiArrowUpSLine />
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ 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 } from '@/primitives/Select'
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { permissionsStore } from '@/stores/permissions'
|
|
||||||
import { Placement } from '@react-types/overlays'
|
import { Placement } from '@react-types/overlays'
|
||||||
|
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||||
|
|
||||||
type DeviceItems = Array<{ value: string; label: string }>
|
type DeviceItems = Array<{ value: string; label: string }>
|
||||||
|
|
||||||
@@ -100,8 +99,6 @@ export const SelectDevice = ({
|
|||||||
}: SelectDeviceProps) => {
|
}: SelectDeviceProps) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
const permissions = useSnapshot(permissionsStore)
|
|
||||||
|
|
||||||
const contextProps = useMemo<SelectDeviceContext>(() => {
|
const contextProps = useMemo<SelectDeviceContext>(() => {
|
||||||
if (context == 'room') {
|
if (context == 'room') {
|
||||||
return { variant: 'dark', placement: 'top' }
|
return { variant: 'dark', placement: 'top' }
|
||||||
@@ -126,22 +123,11 @@ export const SelectDevice = ({
|
|||||||
}
|
}
|
||||||
}, [kind])
|
}, [kind])
|
||||||
|
|
||||||
const isPermissionDeniedOrPrompted = useMemo(() => {
|
const cannotUseDevice = useCannotUseDevice(kind)
|
||||||
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])
|
|
||||||
|
|
||||||
if (!config) return null
|
if (!config) return null
|
||||||
|
|
||||||
if (isPermissionDeniedOrPrompted || permissions.isLoading) {
|
if (cannotUseDevice) {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
aria-label={t(`${kind}.permissionsNeeded`)}
|
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 { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
|
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
|
||||||
|
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||||
import { BackgroundProcessorFactory } from '../../blur'
|
import { BackgroundProcessorFactory } from '../../blur'
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { permissionsStore } from '@/stores/permissions'
|
|
||||||
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
|
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
|
||||||
import Source = Track.Source
|
import Source = Track.Source
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
@@ -53,10 +52,7 @@ export const VideoDeviceControl = ({
|
|||||||
...props,
|
...props,
|
||||||
})
|
})
|
||||||
|
|
||||||
const permissions = useSnapshot(permissionsStore)
|
const cannotUseDevice = useCannotUseDevice('videoinput')
|
||||||
|
|
||||||
const isPermissionDeniedOrPrompted =
|
|
||||||
permissions.isCameraDenied || permissions.isCameraPrompted
|
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
/**
|
/**
|
||||||
@@ -99,7 +95,6 @@ export const VideoDeviceControl = ({
|
|||||||
config={config}
|
config={config}
|
||||||
variant="primaryDark"
|
variant="primaryDark"
|
||||||
toggle={toggle}
|
toggle={toggle}
|
||||||
isPermissionDeniedOrPrompted={isPermissionDeniedOrPrompted}
|
|
||||||
toggleButtonProps={{
|
toggleButtonProps={{
|
||||||
...(hideMenu
|
...(hideMenu
|
||||||
? {
|
? {
|
||||||
@@ -111,15 +106,12 @@ export const VideoDeviceControl = ({
|
|||||||
{!hideMenu && (
|
{!hideMenu && (
|
||||||
<Popover variant="dark" withArrow={false}>
|
<Popover variant="dark" withArrow={false}>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isPermissionDeniedOrPrompted}
|
|
||||||
tooltip={selectLabel}
|
tooltip={selectLabel}
|
||||||
aria-label={selectLabel}
|
aria-label={selectLabel}
|
||||||
groupPosition="right"
|
groupPosition="right"
|
||||||
square
|
square
|
||||||
variant={
|
variant={
|
||||||
trackProps.enabled && !isPermissionDeniedOrPrompted
|
trackProps.enabled && !cannotUseDevice ? 'primaryDark' : 'error2'
|
||||||
? 'primaryDark'
|
|
||||||
: 'error2'
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RiArrowUpSLine />
|
<RiArrowUpSLine />
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
|||||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
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'
|
||||||
|
|
||||||
export type ToggleDeviceProps = {
|
export type ToggleDeviceProps = {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
isPermissionDeniedOrPrompted?: boolean
|
|
||||||
toggle: () => void
|
toggle: () => void
|
||||||
config: ToggleDeviceConfig
|
config: ToggleDeviceConfig
|
||||||
variant?: NonNullable<ButtonRecipeProps>['variant']
|
variant?: NonNullable<ButtonRecipeProps>['variant']
|
||||||
@@ -33,7 +33,6 @@ export const ToggleDevice = ({
|
|||||||
variant = 'primaryDark',
|
variant = 'primaryDark',
|
||||||
errorVariant = 'error2',
|
errorVariant = 'error2',
|
||||||
toggleButtonProps,
|
toggleButtonProps,
|
||||||
isPermissionDeniedOrPrompted,
|
|
||||||
}: ToggleDeviceProps) => {
|
}: ToggleDeviceProps) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
@@ -52,6 +51,8 @@ export const ToggleDevice = ({
|
|||||||
setPushToTalk(false)
|
setPushToTalk(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cannotUseDevice = useCannotUseDevice(kind)
|
||||||
|
|
||||||
useRegisterKeyboardShortcut({ shortcut, handler: toggle })
|
useRegisterKeyboardShortcut({ shortcut, handler: toggle })
|
||||||
useLongPress({ keyCode: longPress?.key, onKeyDown, onKeyUp })
|
useLongPress({ keyCode: longPress?.key, onKeyDown, onKeyUp })
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ 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 && !isPermissionDeniedOrPrompted ? iconOn : iconOff
|
const Icon = enabled && !cannotUseDevice ? iconOn : iconOff
|
||||||
|
|
||||||
const context = useMaybeRoomContext()
|
const context = useMaybeRoomContext()
|
||||||
if (kind === 'audioinput' && pushToTalk && context) {
|
if (kind === 'audioinput' && pushToTalk && context) {
|
||||||
@@ -71,19 +72,15 @@ export const ToggleDevice = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
{isPermissionDeniedOrPrompted && <PermissionNeededButton />}
|
{cannotUseDevice && <PermissionNeededButton />}
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
isSelected={!enabled}
|
isSelected={!enabled}
|
||||||
variant={
|
variant={enabled && !cannotUseDevice ? variant : errorVariant}
|
||||||
enabled && !isPermissionDeniedOrPrompted ? variant : errorVariant
|
|
||||||
}
|
|
||||||
shySelected
|
shySelected
|
||||||
onPress={() =>
|
onPress={() => (cannotUseDevice ? openPermissionsDialog() : toggle())}
|
||||||
isPermissionDeniedOrPrompted ? openPermissionsDialog() : toggle()
|
|
||||||
}
|
|
||||||
aria-label={toggleLabel}
|
aria-label={toggleLabel}
|
||||||
tooltip={
|
tooltip={
|
||||||
isPermissionDeniedOrPrompted
|
cannotUseDevice
|
||||||
? t('tooltip', { keyPrefix: 'permissionsButton' })
|
? t('tooltip', { keyPrefix: 'permissionsButton' })
|
||||||
: toggleLabel
|
: 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