From 9580f2a7440f20a915588d0dc5d089ce844bee0b 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. + + )}