From 69050714599afe8393d44951cac828a5273c5d77 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sun, 3 May 2026 01:56:24 +0200 Subject: [PATCH] =?UTF-8?q?refactor(web):=20UserAreaPanel=20uses=20useAudi?= =?UTF-8?q?oDevices=20=E2=80=94=20kills=20unconditional=20getUserMedia=20p?= =?UTF-8?q?robe;=20reuses=20shared=20DropdownItem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/ChannelSidebar.tsx | 158 +++++++++--------- 1 file changed, 77 insertions(+), 81 deletions(-) diff --git a/packages/web/src/components/layout/ChannelSidebar.tsx b/packages/web/src/components/layout/ChannelSidebar.tsx index 2b7f5732..0770df11 100644 --- a/packages/web/src/components/layout/ChannelSidebar.tsx +++ b/packages/web/src/components/layout/ChannelSidebar.tsx @@ -21,6 +21,8 @@ import { DmSearchBar } from './DmSearchBar'; import { DmListItem } from './DmListItem'; import { useDragManager, type DropTarget, type LayoutItem } from '../../hooks/useDragManager'; import { useDelayedLoading } from '../../hooks/useDelayedLoading'; +import { useAudioDevices } from '../../hooks/useAudioDevices'; +import { DropdownItem } from '../modals/settingsPanels/_shared/SettingsPickerPrimitives'; export function ChannelSidebar() { const spaces = useSpaceStore((s) => s.spaces); @@ -843,15 +845,20 @@ function UserAreaPanel({ onSettingsClick: (tab?: string) => void; }) { const [openPanel, setOpenPanel] = useState<'input' | 'output' | null>(null); - const [inputDevices, setInputDevices] = useState([]); - const [outputDevices, setOutputDevices] = useState([]); const inputDeviceId = useVoiceStore((s) => s.inputDeviceId); const outputDeviceId = useVoiceStore((s) => s.outputDeviceId); const setInputDevice = useVoiceStore((s) => s.setInputDevice); const setOutputDevice = useVoiceStore((s) => s.setOutputDevice); - - const [selectedInputLabel, setSelectedInputLabel] = useState('Default'); - const [selectedOutputLabel, setSelectedOutputLabel] = useState('Default'); + + // Shared hook drives lists, permission state, and live devicechange refresh. + const { permState, inputs: inputDevices, outputs: outputDevices, inputLabels, outputLabels, requestPermission } = useAudioDevices(); + + const selectedInputLabel = inputDeviceId === 'default' + ? 'System Default' + : inputLabels.get(inputDeviceId) ?? 'System Default'; + const selectedOutputLabel = outputDeviceId === 'default' + ? 'System Default' + : outputLabels.get(outputDeviceId) ?? 'System Default'; const inputVolume = useVoiceStore((s) => s.inputVolume); const storeSetInputVolume = useVoiceStore((s) => s.setInputVolume); @@ -864,38 +871,6 @@ function UserAreaPanel({ const analyserRef = useRef(null); const animFrameRef = useRef(0); - const loadDevices = useCallback(async () => { - try { - // Need to request permission first to get labels - if (!AudioManager.getInstance().getContext()) { - await navigator.mediaDevices.getUserMedia({ audio: true }).then(s => s.getTracks().forEach(t => t.stop())); - } - const devices = await navigator.mediaDevices.enumerateDevices(); - // Deduplicate by deviceId — USB devices sharing the same audio chipset - // (e.g. C-Media 0d8c:0134) appear as multiple entries with identical IDs. - const dedup = (list: MediaDeviceInfo[]): MediaDeviceInfo[] => { - const seen = new Set(); - return list.filter(d => { - if (seen.has(d.deviceId)) return false; - seen.add(d.deviceId); - return true; - }); - }; - const inputs = dedup(devices.filter(d => d.kind === 'audioinput')); - const outputs = dedup(devices.filter(d => d.kind === 'audiooutput')); - setInputDevices(inputs); - setOutputDevices(outputs); - - const currentInput = inputs.find(d => d.deviceId === inputDeviceId); - if (currentInput) setSelectedInputLabel(currentInput.label || 'Default'); - - const currentOutput = outputs.find(d => d.deviceId === outputDeviceId); - if (currentOutput) setSelectedOutputLabel(currentOutput.label || 'Default'); - } catch { - // permission denied - } - }, [inputDeviceId, outputDeviceId]); - // Start mic level monitoring when input panel opens useEffect(() => { if (openPanel !== 'input') { @@ -946,27 +921,24 @@ function UserAreaPanel({ if (openPanel === panel) { setOpenPanel(null); } else { - loadDevices(); setOpenPanel(panel); setShowInputDeviceList(false); setShowOutputDeviceList(false); - // Explicitly resume on interaction + // Resume the AudioContext so the mic-level meter starts measuring on open. AudioManager.getInstance().resumeContext(); } }; - const selectInput = (device: MediaDeviceInfo) => { - setInputDevice(device.deviceId); // Pure state update → triggers syncMic if in voice call - AudioManager.getInstance().setInputDevice(device.deviceId); // Immediate preview for mic level meter - setSelectedInputLabel(device.label || 'Default'); + const selectInput = (deviceId: string) => { + setInputDevice(deviceId); // Pure state update → triggers syncMic if in voice call + AudioManager.getInstance().setInputDevice(deviceId).catch(() => {}); setShowInputDeviceList(false); }; - const selectOutput = (device: MediaDeviceInfo) => { - setOutputDevice(device.deviceId); - setSelectedOutputLabel(device.label || 'Default'); + const selectOutput = (deviceId: string) => { + setOutputDevice(deviceId); + AudioManager.getInstance().setOutputDevice(deviceId).catch(() => {}); setShowOutputDeviceList(false); - AudioManager.getInstance().setOutputDevice(device.deviceId); }; // Generate mic level bars (20 bars like Discord) @@ -993,23 +965,35 @@ function UserAreaPanel({ {showInputDeviceList && ( -
- {inputDevices.map(d => ( - - ))} +
+ {permState !== 'granted' && ( +
+ Microphone permission needed.{' '} + +
+ )} + {permState === 'granted' && ( + <> + selectInput('default')} + /> + {inputDevices.filter(d => d.deviceId !== 'default').map(d => ( + selectInput(d.deviceId)} + /> + ))} + + )}
)}
@@ -1079,23 +1063,35 @@ function UserAreaPanel({ {showOutputDeviceList && ( -
- {outputDevices.map(d => ( - - ))} +
+ {permState !== 'granted' && ( +
+ Audio permission needed.{' '} + +
+ )} + {permState === 'granted' && ( + <> + selectOutput('default')} + /> + {outputDevices.filter(d => d.deviceId !== 'default').map(d => ( + selectOutput(d.deviceId)} + /> + ))} + + )}
)}