diff --git a/src/app/components/Modal500.tsx b/src/app/components/Modal500.tsx index 59844cc..a289650 100644 --- a/src/app/components/Modal500.tsx +++ b/src/app/components/Modal500.tsx @@ -1,6 +1,6 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useCallback, useState } from 'react'; import FocusTrap from 'focus-trap-react'; -import { Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds'; +import { Modal, Overlay, OverlayBackdrop, OverlayCenter, PopOutContainerProvider } from 'folds'; import { stopPropagation } from '../utils/keyboard'; type Modal500Props = { @@ -8,23 +8,8 @@ type Modal500Props = { children: ReactNode; }; export function Modal500({ requestClose, children }: Modal500Props) { - const handleClickOutside = (e: MouseEvent | TouchEvent | FocusEvent) => { - const target = e.target as HTMLElement; - - // Don't close if clicking on a popout menu, overlay, or other portaled content - // Check for common portal container elements and popout menus - if ( - target.closest('[role="menu"]') || - target.closest('[role="dialog"]') || - target.closest('[data-floating-ui-portal]') || - target.closest('.folds-overlay') || - target.closest('.popout') - ) { - return false; - } - - return true; - }; + const [modalEl, setModalEl] = useState(null); + const modalRef = useCallback((el: HTMLDivElement | null) => setModalEl(el), []); return ( }> @@ -32,13 +17,15 @@ export function Modal500({ requestClose, children }: Modal500Props) { - - {children} + + + {children} + diff --git a/src/app/features/settings/audio/Audio.tsx b/src/app/features/settings/audio/Audio.tsx index 79485a7..9f0e659 100644 --- a/src/app/features/settings/audio/Audio.tsx +++ b/src/app/features/settings/audio/Audio.tsx @@ -1,8 +1,9 @@ /* eslint-disable no-console */ -import React, { useEffect, useState, MouseEventHandler } from 'react'; +import React, { useCallback, useEffect, useRef, useState, MouseEventHandler } from 'react'; import { Box, Button, + color, config, Header, Icon, @@ -450,6 +451,10 @@ export function Audio({ requestClose }: AudioProps) { const [speakers, setSpeakers] = useState([]); const [settings, setSettings] = useState(loadAudioSettings); const [permissionStatus, setPermissionStatus] = useState<'granted' | 'denied' | 'prompt'>('prompt'); + const [micTestState, setMicTestState] = useState<'idle' | 'testing' | 'done' | 'error'>('idle'); + const [micLevel, setMicLevel] = useState(0); + const [speakerTestState, setSpeakerTestState] = useState<'idle' | 'testing' | 'done' | 'error'>('idle'); + const micAnimFrameRef = useRef(null); const [showRemoteCursor, setShowRemoteCursor] = useSetting(settingsAtom, 'showRemoteCursor'); const [livekitServiceUrl, setLivekitServiceUrl] = useSetting(settingsAtom, 'livekitServiceUrl'); @@ -555,58 +560,73 @@ export function Audio({ requestClose }: AudioProps) { saveAudioSettings(newSettings); }; - const handleTestMicrophone = async () => { + const handleTestMicrophone = useCallback(async () => { + if (micTestState === 'testing') return; + setMicTestState('testing'); + setMicLevel(0); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: settings.microphoneId ? { deviceId: { exact: settings.microphoneId } } : true, }); - - // Create audio context to visualize const audioContext = new AudioContext(); const analyser = audioContext.createAnalyser(); + analyser.fftSize = 256; + const dataArray = new Uint8Array(analyser.frequencyBinCount); const microphone = audioContext.createMediaStreamSource(stream); microphone.connect(analyser); - - // Quick test - just log that it works - console.log('🎤 Microphone test: Audio stream acquired'); - - // Stop after a short delay + + const tick = () => { + analyser.getByteFrequencyData(dataArray); + const avg = dataArray.reduce((a, b) => a + b, 0) / dataArray.length; + setMicLevel(Math.min(100, Math.round((avg / 255) * 100 * 3))); + micAnimFrameRef.current = requestAnimationFrame(tick); + }; + micAnimFrameRef.current = requestAnimationFrame(tick); + setTimeout(() => { + if (micAnimFrameRef.current !== null) { + cancelAnimationFrame(micAnimFrameRef.current); + micAnimFrameRef.current = null; + } stream.getTracks().forEach((track) => track.stop()); audioContext.close(); - console.log('🎤 Microphone test: Complete'); - }, 2000); + setMicLevel(0); + setMicTestState('done'); + setTimeout(() => setMicTestState('idle'), 2000); + }, 3000); } catch (e) { console.error('Microphone test failed:', e); + setMicTestState('error'); + setTimeout(() => setMicTestState('idle'), 2000); } - }; + }, [micTestState, settings.microphoneId]); - const handleTestSpeaker = async () => { + const handleTestSpeaker = useCallback(async () => { + if (speakerTestState === 'testing') return; + setSpeakerTestState('testing'); try { - // Create a simple test tone const audioContext = new AudioContext(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); - oscillator.connect(gainNode); gainNode.connect(audioContext.destination); - - oscillator.frequency.value = 440; // A4 note - gainNode.gain.value = 0.1; // Low volume - + oscillator.frequency.value = 440; + gainNode.gain.setValueAtTime(0, audioContext.currentTime); + gainNode.gain.linearRampToValueAtTime(0.15, audioContext.currentTime + 0.1); + gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1.0); oscillator.start(); - console.log('🔊 Speaker test: Playing tone'); - - // Stop after a short delay + oscillator.stop(audioContext.currentTime + 1.0); setTimeout(() => { - oscillator.stop(); audioContext.close(); - console.log('🔊 Speaker test: Complete'); - }, 1000); + setSpeakerTestState('done'); + setTimeout(() => setSpeakerTestState('idle'), 2000); + }, 1200); } catch (e) { console.error('Speaker test failed:', e); + setSpeakerTestState('error'); + setTimeout(() => setSpeakerTestState('idle'), 2000); } - }; + }, [speakerTestState]); return ( @@ -666,15 +686,25 @@ export function Audio({ requestClose }: AudioProps) { selectedId={settings.microphoneId} onSelect={handleMicrophoneSelect} /> - + + + {micTestState === 'testing' && ( + + + + )} + } /> @@ -703,12 +733,15 @@ export function Audio({ requestClose }: AudioProps) { /> }