From f1b20d798165529ee0e6bd6210348697c98da81b Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Fri, 8 Aug 2025 14:48:05 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20permissions=20watch?= =?UTF-8?q?er=20to=20sync=20valtio=20store=20with=20browser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce permissions watcher that continuously monitors browser permission changes and keeps the valtio global store synchronized with actual browser permission state. --- .../features/rooms/components/Permissions.tsx | 11 ++++ .../rooms/hooks/useWatchPermissions.ts | 66 +++++++++++++++++++ .../src/features/rooms/routes/Room.tsx | 20 ++++-- src/frontend/src/stores/permissions.ts | 2 + 4 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/frontend/src/features/rooms/components/Permissions.tsx create mode 100644 src/frontend/src/features/rooms/hooks/useWatchPermissions.ts diff --git a/src/frontend/src/features/rooms/components/Permissions.tsx b/src/frontend/src/features/rooms/components/Permissions.tsx new file mode 100644 index 00000000..cda2f7ef --- /dev/null +++ b/src/frontend/src/features/rooms/components/Permissions.tsx @@ -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 +} diff --git a/src/frontend/src/features/rooms/hooks/useWatchPermissions.ts b/src/frontend/src/features/rooms/hooks/useWatchPermissions.ts new file mode 100644 index 00000000..5811059e --- /dev/null +++ b/src/frontend/src/features/rooms/hooks/useWatchPermissions.ts @@ -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?.() + } + }, []) +} diff --git a/src/frontend/src/features/rooms/routes/Room.tsx b/src/frontend/src/features/rooms/routes/Room.tsx index 41d423f7..130ee0dc 100644 --- a/src/frontend/src/features/rooms/routes/Room.tsx +++ b/src/frontend/src/features/rooms/routes/Room.tsx @@ -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 ( + + + {children} + + ) +} + export const Room = () => { const { isLoggedIn } = useUser() const [hasSubmittedEntry, setHasSubmittedEntry] = useState(false) @@ -47,19 +57,19 @@ export const Room = () => { if (!hasSubmittedEntry && !skipJoinScreen) { return ( - + setHasSubmittedEntry(true)} roomId={roomId} /> - + ) } return ( - + - + ) } diff --git a/src/frontend/src/stores/permissions.ts b/src/frontend/src/stores/permissions.ts index 1f1f79d6..a91653ef 100644 --- a/src/frontend/src/stores/permissions.ts +++ b/src/frontend/src/stores/permissions.ts @@ -10,9 +10,11 @@ type PermissionState = type State = { cameraPermission: PermissionState microphonePermission: PermissionState + isLoading: boolean } export const permissionsStore = proxy({ cameraPermission: undefined, microphonePermission: undefined, + isLoading: true, })