✨(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:
committed by
aleb_the_flash
parent
95190ec690
commit
f1b20d7981
11
src/frontend/src/features/rooms/components/Permissions.tsx
Normal file
11
src/frontend/src/features/rooms/components/Permissions.tsx
Normal 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
|
||||
}
|
||||
66
src/frontend/src/features/rooms/hooks/useWatchPermissions.ts
Normal file
66
src/frontend/src/features/rooms/hooks/useWatchPermissions.ts
Normal 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?.()
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user