diff --git a/src/frontend/src/features/rooms/components/Permissions.tsx b/src/frontend/src/features/rooms/components/Permissions.tsx index 63b9bb9b..a6433596 100644 --- a/src/frontend/src/features/rooms/components/Permissions.tsx +++ b/src/frontend/src/features/rooms/components/Permissions.tsx @@ -4,7 +4,7 @@ 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 { closePermissionsDialog, permissionsStore } from '@/stores/permissions' import { useTranslation } from 'react-i18next' import { injectIconIntoTranslation } from '@/utils/translation' @@ -41,7 +41,7 @@ export const Permissions = () => { permissions.isCameraGranted && permissions.isMicrophoneGranted ) { - permissionsStore.isPermissionDialogOpen = false + closePermissionsDialog() } }, [permissions]) @@ -56,7 +56,7 @@ export const Permissions = () => { aria-label={t(`heading.${permissionLabel}`, { appTitle, })} - onClose={() => (permissionsStore.isPermissionDialogOpen = false)} + onClose={closePermissionsDialog} >
{ + const { t } = useTranslation('rooms', { keyPrefix: 'permissionsButton' }) + return ( +
+ +
+ ) +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx b/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx index 240f36e6..8158d0cf 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx @@ -25,9 +25,11 @@ import { Shortcut } from '@/features/shortcuts/types' import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx' import { css } from '@/styled-system/css' import { ButtonRecipeProps } from '@/primitives/buttonRecipe' -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' import { usePersistentUserChoices } from '../../hooks/usePersistentUserChoices' import { BackgroundProcessorFactory } from '../blur' +import { useSnapshot } from 'valtio' +import { permissionsStore } from '@/stores/permissions' export type ToggleSource = Exclude< Track.Source, @@ -101,6 +103,18 @@ export const SelectToggleDevice = ({ const { userChoices } = usePersistentUserChoices() + const permissions = useSnapshot(permissionsStore) + const isPermissionDeniedOrPrompted = useMemo(() => { + switch (config.kind) { + case 'audioinput': + return ( + permissions.isMicrophoneDenied || permissions.isMicrophonePrompted + ) + case 'videoinput': + return permissions.isCameraDenied || permissions.isCameraPrompted + } + }, [permissions, config.kind]) + const toggle = () => { if (props.source === Track.Source.Camera) { /** @@ -161,6 +175,7 @@ export const SelectToggleDevice = ({ config={config} variant={variant} toggle={toggle} + isPermissionDeniedOrPrompted={isPermissionDeniedOrPrompted} toggleButtonProps={{ ...(hideMenu ? { @@ -172,11 +187,16 @@ export const SelectToggleDevice = ({ {!hideMenu && ( diff --git a/src/frontend/src/features/rooms/livekit/components/controls/ToggleDevice.tsx b/src/frontend/src/features/rooms/livekit/components/controls/ToggleDevice.tsx index 411e88f7..cfe42590 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/ToggleDevice.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/ToggleDevice.tsx @@ -4,6 +4,7 @@ import { useMemo, useState } from 'react' import { appendShortcutLabel } from '@/features/shortcuts/utils' import { useTranslation } from 'react-i18next' import { SelectToggleDeviceConfig } from './SelectToggleDevice' +import { PermissionNeededButton } from './PermissionNeededButton' import useLongPress from '@/features/shortcuts/useLongPress' import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker' import { @@ -13,9 +14,11 @@ import { } from '@livekit/components-react' import { ButtonRecipeProps } from '@/primitives/buttonRecipe' import { ToggleButtonProps } from '@/primitives/ToggleButton' +import { openPermissionsDialog } from '@/stores/permissions' export type ToggleDeviceProps = { enabled: boolean + isPermissionDeniedOrPrompted?: boolean toggle: () => void config: SelectToggleDeviceConfig variant?: NonNullable['variant'] @@ -28,6 +31,7 @@ export const ToggleDevice = ({ toggle, variant = 'primaryDark', toggleButtonProps, + isPermissionDeniedOrPrompted, }: ToggleDeviceProps) => { const { t } = useTranslation('rooms', { keyPrefix: 'join' }) @@ -56,7 +60,7 @@ export const ToggleDevice = ({ return shortcut ? appendShortcutLabel(label, shortcut) : label }, [enabled, kind, shortcut, t]) - const Icon = enabled ? iconOn : iconOff + const Icon = enabled && !isPermissionDeniedOrPrompted ? iconOn : iconOff const context = useMaybeRoomContext() if (kind === 'audioinput' && pushToTalk && context) { @@ -64,18 +68,27 @@ export const ToggleDevice = ({ } return ( - toggle()} - aria-label={toggleLabel} - tooltip={toggleLabel} - groupPosition="left" - {...toggleButtonProps} - > - - +
+ {isPermissionDeniedOrPrompted && } + + isPermissionDeniedOrPrompted ? openPermissionsDialog() : toggle() + } + aria-label={toggleLabel} + tooltip={ + isPermissionDeniedOrPrompted + ? t('tooltip', { keyPrefix: 'permissionsButton' }) + : toggleLabel + } + groupPosition="left" + {...toggleButtonProps} + > + + +
) } diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index 638c74e5..0bd34242 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -94,6 +94,10 @@ } } }, + "permissionsButton": { + "tooltip": "Mehr Infos", + "ariaLabel": "Berechtigungsproblem. Mehr Infos anzeigen" + }, "error": { "createRoom": { "heading": "Authentifizierung erforderlich", diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index eb80028f..7ceff7a9 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -94,6 +94,10 @@ } } }, + "permissionsButton": { + "tooltip": "More info", + "ariaLabel": "Permissions issue. Show more info" + }, "error": { "createRoom": { "heading": "Authentication Required", diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 74b65dde..69b9004a 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -94,6 +94,10 @@ } } }, + "permissionsButton": { + "tooltip": "Plus d'infos", + "ariaLabel": "Problème de permissions. Afficher plus d'infos" + }, "error": { "createRoom": { "heading": "Authentification requise", diff --git a/src/frontend/src/locales/nl/rooms.json b/src/frontend/src/locales/nl/rooms.json index ef65b2fa..b7fe6b67 100644 --- a/src/frontend/src/locales/nl/rooms.json +++ b/src/frontend/src/locales/nl/rooms.json @@ -94,6 +94,10 @@ } } }, + "permissionsButton": { + "tooltip": "Meer info", + "ariaLabel": "Probleem met machtigingen. Meer info weergeven" + }, "error": { "createRoom": { "heading": "Verificatie vereist", diff --git a/src/frontend/src/primitives/buttonRecipe.ts b/src/frontend/src/primitives/buttonRecipe.ts index 5d6e2fa4..59b14a2e 100644 --- a/src/frontend/src/primitives/buttonRecipe.ts +++ b/src/frontend/src/primitives/buttonRecipe.ts @@ -270,6 +270,20 @@ export const buttonRecipe = cva({ color: 'primary !important', }, }, + permission: { + position: 'relative', + // background: 'None !important', + borderRadius: '100%', + // border: 'none !important', + color: 'amber.500', + width: 'fit-content', + height: 'fit-content', + padding: '0 !important', + margin: '0 !important', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, }, invisible: { true: { diff --git a/src/frontend/src/stores/permissions.ts b/src/frontend/src/stores/permissions.ts index 32641c11..1d8091c1 100644 --- a/src/frontend/src/stores/permissions.ts +++ b/src/frontend/src/stores/permissions.ts @@ -8,19 +8,30 @@ type PermissionState = | 'denied' | 'unavailable' -type State = { +type BaseState = { cameraPermission: PermissionState microphonePermission: PermissionState isLoading: boolean isPermissionDialogOpen: boolean } -export const permissionsStore = proxy({ +type DerivedState = { + isCameraGranted: boolean + isMicrophoneGranted: boolean + isCameraDenied: boolean + isMicrophoneDenied: boolean + isCameraPrompted: boolean + isMicrophonePrompted: boolean +} + +type State = BaseState & DerivedState + +export const permissionsStore = proxy({ cameraPermission: undefined, microphonePermission: undefined, isLoading: true, isPermissionDialogOpen: false, -}) +}) as State derive( { @@ -31,8 +42,20 @@ derive( isCameraDenied: (get) => get(permissionsStore).cameraPermission == 'denied', isMicrophoneDenied: (get) => get(permissionsStore).microphonePermission == 'denied', + isCameraPrompted: (get) => + get(permissionsStore).cameraPermission == 'prompt', + isMicrophonePrompted: (get) => + get(permissionsStore).microphonePermission == 'prompt', }, { proxy: permissionsStore, } ) + +export const openPermissionsDialog = () => { + permissionsStore.isPermissionDialogOpen = true +} + +export const closePermissionsDialog = () => { + permissionsStore.isPermissionDialogOpen = false +}