From f3c2babd4b4c198723440f17521f9208f3e352a8 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 May 2026 11:56:38 -0400 Subject: [PATCH] fix(call): show mic-denied error before joining instead of crashing Check navigator.permissions for microphone state before the call starts. If the user has blocked microphone access, disable the Join button and show an inline message explaining how to fix it in browser settings. Subscribes to permission change events so the UI updates if they grant access without refreshing. Co-Authored-By: Claude Sonnet 4.6 --- src/app/features/call/PrescreenControls.tsx | 36 +++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/app/features/call/PrescreenControls.tsx b/src/app/features/call/PrescreenControls.tsx index 1174bbf1c..f313fec7f 100644 --- a/src/app/features/call/PrescreenControls.tsx +++ b/src/app/features/call/PrescreenControls.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Box, Button, Icon, Icons, Spinner, Text } from 'folds'; import { SequenceCard } from '../../components/sequence-card'; import * as css from './styles.css'; @@ -7,6 +7,28 @@ import { useIsDirectRoom, useRoom } from '../../hooks/useRoom'; import { useCallEmbed, useCallJoined, useCallStart } from '../../hooks/useCallEmbed'; import { useCallPreferences } from '../../state/hooks/callPreferences'; +type MediaPermState = 'granted' | 'denied' | 'prompt' | 'unknown'; + +function useMediaPermissions(): MediaPermState { + const [state, setState] = useState('unknown'); + + useEffect(() => { + if (!navigator.permissions) { + setState('unknown'); + return; + } + navigator.permissions + .query({ name: 'microphone' as PermissionName }) + .then((result) => { + setState(result.state as MediaPermState); + result.onchange = () => setState(result.state as MediaPermState); + }) + .catch(() => setState('unknown')); + }, []); + + return state; +} + type PrescreenControlsProps = { canJoin?: boolean; }; @@ -21,7 +43,10 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) { const startCall = useCallStart(direct); const joining = callEmbed?.roomId === room.roomId && !callJoined; - const disabled = inOtherCall || !canJoin; + const micPermission = useMediaPermissions(); + const micDenied = micPermission === 'denied'; + + const disabled = inOtherCall || !canJoin || micDenied; const { microphone, video, sound, toggleMicrophone, toggleVideo, toggleSound } = useCallPreferences(); @@ -45,7 +70,12 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) { - + + {micDenied && ( + + Microphone access is blocked. Enable it in your browser settings to join. + + )}