(frontend) add permissions watcher to sync valtio store with browser

Introduce permissions watcher that continuously monitors browser
permission changes and keeps the valtio global store synchronized
with actual browser permission state.
This commit is contained in:
lebaudantoine
2025-08-08 14:48:05 +02:00
committed by aleb_the_flash
parent 95190ec690
commit f1b20d7981
4 changed files with 94 additions and 5 deletions

View File

@@ -0,0 +1,11 @@
import { useWatchPermissions } from '@/features/rooms/hooks/useWatchPermissions'
/**
* Singleton component - ensures permissions sync runs only once across the app.
* WARNING: This component should only be instantiated once in the interface.
* Multiple instances may cause unexpected behavior or performance issues.
*/
export const Permissions = () => {
useWatchPermissions()
return null
}

View File

@@ -0,0 +1,66 @@
import { useEffect } from 'react'
import { permissionsStore } from '@/stores/permissions'
export const useWatchPermissions = () => {
useEffect(() => {
let cleanup: (() => void) | undefined
let isCancelled = false
const checkPermissions = async () => {
try {
if (!navigator.permissions) {
if (!isCancelled) {
permissionsStore.cameraPermission = 'unavailable'
permissionsStore.microphonePermission = 'unavailable'
}
return
}
const [cameraPermission, microphonePermission] = await Promise.all([
navigator.permissions.query({ name: 'camera' }),
navigator.permissions.query({ name: 'microphone' }),
])
if (isCancelled) return
permissionsStore.cameraPermission = cameraPermission.state
permissionsStore.microphonePermission = microphonePermission.state
const handleCameraChange = (e: Event) => {
const target = e.target as PermissionStatus
permissionsStore.cameraPermission = target.state
}
const handleMicrophoneChange = (e: Event) => {
const target = e.target as PermissionStatus
permissionsStore.microphonePermission = target.state
}
cameraPermission.addEventListener('change', handleCameraChange)
microphonePermission.addEventListener('change', handleMicrophoneChange)
cleanup = () => {
cameraPermission.removeEventListener('change', handleCameraChange)
microphonePermission.removeEventListener(
'change',
handleMicrophoneChange
)
}
} catch (error) {
if (!isCancelled) {
console.error('Error checking permissions:', error)
}
} finally {
if (!isCancelled) {
permissionsStore.isLoading = false
}
}
}
checkPermissions()
return () => {
isCancelled = true
cleanup?.()
}
}, [])
}

View File

@@ -1,15 +1,25 @@
import { useEffect, useState } from 'react'
import { ReactNode, useEffect, useState } from 'react'
import { useLocation, useParams } from 'wouter'
import { ErrorScreen } from '@/components/ErrorScreen'
import { useUser, UserAware } from '@/features/auth'
import { Conference } from '../components/Conference'
import { Join } from '../components/Join'
import { Permissions } from '../components/Permissions'
import { useKeyboardShortcuts } from '@/features/shortcuts/useKeyboardShortcuts'
import {
isRoomValid,
normalizeRoomId,
} from '@/features/rooms/utils/isRoomValid'
const BaseRoom = ({ children }: { children: ReactNode }) => {
return (
<UserAware>
<Permissions />
{children}
</UserAware>
)
}
export const Room = () => {
const { isLoggedIn } = useUser()
const [hasSubmittedEntry, setHasSubmittedEntry] = useState(false)
@@ -47,19 +57,19 @@ export const Room = () => {
if (!hasSubmittedEntry && !skipJoinScreen) {
return (
<UserAware>
<BaseRoom>
<Join enterRoom={() => setHasSubmittedEntry(true)} roomId={roomId} />
</UserAware>
</BaseRoom>
)
}
return (
<UserAware>
<BaseRoom>
<Conference
initialRoomData={initialRoomData}
roomId={roomId}
mode={mode}
/>
</UserAware>
</BaseRoom>
)
}

View File

@@ -10,9 +10,11 @@ type PermissionState =
type State = {
cameraPermission: PermissionState
microphonePermission: PermissionState
isLoading: boolean
}
export const permissionsStore = proxy<State>({
cameraPermission: undefined,
microphonePermission: undefined,
isLoading: true,
})