From 120bcdc720b8c56412802d3c6e0225c90be0d4d1 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sat, 9 Aug 2025 16:15:22 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8(frontend)=20add=20permissions=20di?= =?UTF-8?q?alog=20to=20guide=20users=20through=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce guided permissions dialog to help users understand and resolve camera/microphone access issues step-by-step. Addresses common user support requests where users cannot enable their hardware and don't understand the permission requirements. Provides clear instructions to reduce confusion and support burden. Image was quickly prototyped. It will be updated later on. --- src/frontend/package-lock.json | 10 ++ src/frontend/package.json | 1 + .../public/assets/camera_mic_permission.svg | 109 ++++++++++++++++++ .../features/rooms/components/Permissions.tsx | 94 ++++++++++++++- src/frontend/src/locales/de/rooms.json | 17 +++ src/frontend/src/locales/en/rooms.json | 17 +++ src/frontend/src/locales/fr/rooms.json | 17 +++ src/frontend/src/locales/nl/rooms.json | 17 +++ src/frontend/src/stores/permissions.ts | 18 +++ src/frontend/src/utils/translation.ts | 14 +++ 10 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 src/frontend/public/assets/camera_mic_permission.svg create mode 100644 src/frontend/src/utils/translation.ts diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 92e50db1..67318399 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -17,6 +17,7 @@ "@tanstack/react-query": "5.81.5", "@timephy/rnnoise-wasm": "1.0.0", "crisp-sdk-web": "1.0.25", + "derive-valtio": "0.2.0", "hoofd": "1.7.3", "humanize-duration": "3.33.0", "i18next": "25.3.1", @@ -5294,6 +5295,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/derive-valtio": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/derive-valtio/-/derive-valtio-0.2.0.tgz", + "integrity": "sha512-6slhaFHtfaL3t5dLYaQt6s4G2xZymhu0Ktdl7OMeVk8+46RgR8ft6FL0Tr4F31W+yPH03nJe1SSP4JFy2hSMRA==", + "license": "MIT", + "peerDependencies": { + "valtio": ">=2.0.0-rc.0" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index 27828e21..d3318a4b 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -22,6 +22,7 @@ "@tanstack/react-query": "5.81.5", "@timephy/rnnoise-wasm": "1.0.0", "crisp-sdk-web": "1.0.25", + "derive-valtio": "0.2.0", "hoofd": "1.7.3", "humanize-duration": "3.33.0", "i18next": "25.3.1", diff --git a/src/frontend/public/assets/camera_mic_permission.svg b/src/frontend/public/assets/camera_mic_permission.svg new file mode 100644 index 00000000..ea7af92f --- /dev/null +++ b/src/frontend/public/assets/camera_mic_permission.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/src/features/rooms/components/Permissions.tsx b/src/frontend/src/features/rooms/components/Permissions.tsx index cda2f7ef..63b9bb9b 100644 --- a/src/frontend/src/features/rooms/components/Permissions.tsx +++ b/src/frontend/src/features/rooms/components/Permissions.tsx @@ -1,4 +1,12 @@ import { useWatchPermissions } from '@/features/rooms/hooks/useWatchPermissions' +import { css } from '@/styled-system/css' +import { Dialog, H } from '@/primitives' +import { RiEqualizer2Line } from '@remixicon/react' +import { useEffect, useMemo } from 'react' +import { useSnapshot } from 'valtio' +import { permissionsStore } from '@/stores/permissions' +import { useTranslation } from 'react-i18next' +import { injectIconIntoTranslation } from '@/utils/translation' /** * Singleton component - ensures permissions sync runs only once across the app. @@ -6,6 +14,90 @@ import { useWatchPermissions } from '@/features/rooms/hooks/useWatchPermissions' * Multiple instances may cause unexpected behavior or performance issues. */ export const Permissions = () => { + const { t } = useTranslation('rooms', { keyPrefix: 'permissionErrorDialog' }) + useWatchPermissions() - return null + + const permissions = useSnapshot(permissionsStore) + + const permissionLabel = useMemo(() => { + if (permissions.isMicrophoneDenied && permissions.isCameraDenied) { + return 'cameraAndMicrophone' + } else if (permissions.isCameraDenied) { + return 'camera' + } else if (permissions.isMicrophoneDenied) { + return 'microphone' + } else { + return 'default' + } + }, [permissions]) + + const [descriptionBeforeIcon, descriptionAfterIcon] = + injectIconIntoTranslation(t('body.openMenu')) + + useEffect(() => { + if ( + permissions.isPermissionDialogOpen && + permissions.isCameraGranted && + permissions.isMicrophoneGranted + ) { + permissionsStore.isPermissionDialogOpen = false + } + }, [permissions]) + + const appTitle = `${import.meta.env.VITE_APP_TITLE}` + + return ( + (permissionsStore.isPermissionDialogOpen = false)} + > +
+ +
+ + {t(`heading.${permissionLabel}`, { + appTitle, + })} + +
    +
  1. + {descriptionBeforeIcon} + + + + {descriptionAfterIcon} +
  2. +
  3. {t(`body.details.${permissionLabel}`)}
  4. +
+
+
+
+ ) } diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index 0ad2ad5e..638c74e5 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -77,6 +77,23 @@ }, "close": "OK" }, + "permissionErrorDialog": { + "heading": { + "camera": "{{appTitle}} darf Ihre Kamera nicht verwenden", + "microphone": "{{appTitle}} darf Ihr Mikrofon nicht verwenden", + "cameraAndMicrophone": "{{appTitle}} darf weder Ihr Mikrofon noch Ihre Kamera verwenden", + "default": "{{appTitle}} hat keine Berechtigung für bestimmte Zugriffe" + }, + "body": { + "openMenu": "Klicken Sie auf das Einstellungen-Symbol ICON_PLACEHOLDER in der Adressleiste Ihres Browsers", + "details": { + "camera": "Zugriff auf die Kamera erlauben", + "microphone": "Zugriff auf das Mikrofon erlauben", + "cameraAndMicrophone": "Zugriff auf Kamera und Mikrofon erlauben", + "default": "Aktivieren Sie die erforderlichen Berechtigungen." + } + } + }, "error": { "createRoom": { "heading": "Authentifizierung erforderlich", diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index 4653c8ab..eb80028f 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -77,6 +77,23 @@ }, "close": "OK" }, + "permissionErrorDialog": { + "heading": { + "camera": "{{appTitle}} is not allowed to use your camera", + "microphone": "{{appTitle}} is not allowed to use your microphone", + "cameraAndMicrophone": "{{appTitle}} is not allowed to use your microphone or camera", + "default": "{{appTitle}} is not allowed to use certain permissions" + }, + "body": { + "openMenu": "Click on the settings icon ICON_PLACEHOLDER in your browser’s address bar", + "details": { + "camera": "Allow access to the camera", + "microphone": "Allow access to the microphone", + "cameraAndMicrophone": "Allow access to the camera and microphone", + "default": "Enable the necessary permissions." + } + } + }, "error": { "createRoom": { "heading": "Authentication Required", diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 032ba0ae..74b65dde 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -77,6 +77,23 @@ }, "close": "OK" }, + "permissionErrorDialog": { + "heading": { + "camera": "{{appTitle}} n'est pas autorisé à utiliser votre caméra", + "microphone": "{{appTitle}} n'est pas autorisé à utiliser votre micro", + "cameraAndMicrophone": "{{appTitle}} n'est pas autorisé à utiliser votre micro ni votre caméra", + "default": "{{appTitle}} n'est pas autorisé à utiliser certaines fonctionnalités nécessaires." + }, + "body": { + "openMenu": "Cliquez sur l'icône des paramètres ICON_PLACEHOLDER dans la barre d'adresse de votre navigateur", + "details": { + "camera": "Autorisez l'accès à la caméra", + "microphone": "Autorisez l'accès au microphone", + "cameraAndMicrophone": "Autorisez l'accès à la caméra et au microphone", + "default": "Activez les autorisations nécessaires." + } + } + }, "error": { "createRoom": { "heading": "Authentification requise", diff --git a/src/frontend/src/locales/nl/rooms.json b/src/frontend/src/locales/nl/rooms.json index 5d64a045..ef65b2fa 100644 --- a/src/frontend/src/locales/nl/rooms.json +++ b/src/frontend/src/locales/nl/rooms.json @@ -77,6 +77,23 @@ }, "close": "OK" }, + "permissionErrorDialog": { + "heading": { + "camera": "{{appTitle}} mag uw camera niet gebruiken", + "microphone": "{{appTitle}} mag uw microfoon niet gebruiken", + "cameraAndMicrophone": "{{appTitle}} mag uw microfoon en camera niet gebruiken", + "default": "{{appTitle}} heeft geen toestemming voor bepaalde rechten" + }, + "body": { + "openMenu": "Klik op het instellingenpictogram ICON_PLACEHOLDER in de adresbalk van uw browser", + "details": { + "camera": "Toegang tot de camera toestaan", + "microphone": "Toegang tot de microfoon toestaan", + "cameraAndMicrophone": "Toegang tot camera en microfoon toestaan", + "default": "Schakel de vereiste machtigingen in." + } + } + }, "error": { "createRoom": { "heading": "Verificatie vereist", diff --git a/src/frontend/src/stores/permissions.ts b/src/frontend/src/stores/permissions.ts index a91653ef..32641c11 100644 --- a/src/frontend/src/stores/permissions.ts +++ b/src/frontend/src/stores/permissions.ts @@ -1,4 +1,5 @@ import { proxy } from 'valtio' +import { derive } from 'derive-valtio' type PermissionState = | undefined @@ -11,10 +12,27 @@ type State = { cameraPermission: PermissionState microphonePermission: PermissionState isLoading: boolean + isPermissionDialogOpen: boolean } export const permissionsStore = proxy({ cameraPermission: undefined, microphonePermission: undefined, isLoading: true, + isPermissionDialogOpen: false, }) + +derive( + { + isCameraGranted: (get) => + get(permissionsStore).cameraPermission == 'granted', + isMicrophoneGranted: (get) => + get(permissionsStore).microphonePermission == 'granted', + isCameraDenied: (get) => get(permissionsStore).cameraPermission == 'denied', + isMicrophoneDenied: (get) => + get(permissionsStore).microphonePermission == 'denied', + }, + { + proxy: permissionsStore, + } +) diff --git a/src/frontend/src/utils/translation.ts b/src/frontend/src/utils/translation.ts new file mode 100644 index 00000000..fe060100 --- /dev/null +++ b/src/frontend/src/utils/translation.ts @@ -0,0 +1,14 @@ +/** + * FRAGILE: Splits translated text on placeholder to inject icons inline. + * + * Fragile because: + * - Relies on exact string matching - typos break it silently + * - Translators may accidentally modify/remove placeholders + * - No validation or error handling + */ +export const injectIconIntoTranslation = ( + translation: string, + placeholder: string = 'ICON_PLACEHOLDER' +) => { + return translation.split(placeholder) +}