mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-12 19:56:53 +00:00
✨(frontend) prompt for permissions when toggling a denied device
When a user clicks the microphone or camera toggle while the corresponding permission is denied, trigger a permission prompt via `getUserMedia` instead of silently doing nothing. This gives users a clear path back to granting access without having to dig into the browser settings themselves.
This commit is contained in:
committed by
aleb_the_flash
parent
68a5e84f5d
commit
5723f29cef
@@ -13,6 +13,7 @@ and this project adheres to
|
||||
- 📈(frontend) capture media diagnostics on media errors
|
||||
- ✨(frontend) add an audio gauge to the microphone select menu
|
||||
- ✨(frontend) add a sound tester to the output select menu
|
||||
- ✨(frontend) prompt for permissions when toggling a denied device
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
+24
-8
@@ -1,7 +1,7 @@
|
||||
import { ToggleButton } from '@/primitives'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { appendShortcutLabel } from '@/features/shortcuts/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PermissionNeededButton } from './PermissionNeededButton'
|
||||
@@ -16,6 +16,7 @@ import type { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||
import type { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
import { openPermissionsDialog } from '@/stores/permissions'
|
||||
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||
import { requestDevicePermission } from '../../../hooks/useJoinTracks'
|
||||
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
|
||||
import { useDeviceShortcut } from '../../../hooks/useDeviceShortcut'
|
||||
import type {
|
||||
@@ -93,6 +94,27 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
const deviceShortcut = useDeviceShortcut(kind)
|
||||
const announce = useScreenReaderAnnounce()
|
||||
|
||||
const isRequestingPermission = useRef(false)
|
||||
|
||||
const onPress = async () => {
|
||||
if (!cannotUseDevice) {
|
||||
toggle()
|
||||
return
|
||||
}
|
||||
if (isRequestingPermission.current) return
|
||||
isRequestingPermission.current = true
|
||||
try {
|
||||
const granted = await requestDevicePermission(kind)
|
||||
if (granted) {
|
||||
toggle()
|
||||
} else {
|
||||
openPermissionsDialog(kind)
|
||||
}
|
||||
} finally {
|
||||
isRequestingPermission.current = false
|
||||
}
|
||||
}
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: deviceShortcut?.id,
|
||||
handler: async () => {
|
||||
@@ -147,13 +169,7 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
isDisabled || cannotUseDevice || !enabled ? errorVariant : variant
|
||||
}
|
||||
shySelected
|
||||
onPress={() => {
|
||||
if (cannotUseDevice) {
|
||||
openPermissionsDialog(kind)
|
||||
} else {
|
||||
toggle()
|
||||
}
|
||||
}}
|
||||
onPress={onPress}
|
||||
aria-label={toggleLabel}
|
||||
tooltip={
|
||||
cannotUseDevice
|
||||
|
||||
@@ -33,6 +33,11 @@ const VOICE_AUDIO_CONSTRAINTS = {
|
||||
sampleSize: 16,
|
||||
} as const
|
||||
|
||||
const PERMISSION_KIND: Record<'audioinput' | 'videoinput', PermissionKind> = {
|
||||
audioinput: 'microphone',
|
||||
videoinput: 'camera',
|
||||
}
|
||||
|
||||
export const onJoinPreviewError = (e: Error, kind?: PermissionKind) => {
|
||||
reportError('join_preview_failure', e, { path: 'join_preview' })
|
||||
if (
|
||||
@@ -49,6 +54,22 @@ const disableVideo = () => saveVideoInputEnabled(false)
|
||||
const stopAll = (stream: MediaStream) =>
|
||||
stream.getTracks().forEach((track) => track.stop())
|
||||
|
||||
export const requestDevicePermission = async (
|
||||
kind: 'audioinput' | 'videoinput'
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const track =
|
||||
kind === 'audioinput'
|
||||
? await createLocalAudioTrack()
|
||||
: await createLocalVideoTrack()
|
||||
track.stop()
|
||||
return true
|
||||
} catch (error) {
|
||||
onJoinPreviewError(error as Error, PERMISSION_KIND[kind])
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests camera and microphone once on mount (one combined call → at
|
||||
* most one browser dialog) and releases them immediately. Returns true
|
||||
|
||||
Reference in New Issue
Block a user