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,
})