import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { getParticipantVolume, setParticipantVolume, subscribeParticipantVolumes, } from '../lib/participantVolumes'; interface Props { userId: string; displayName: string; x: number; y: number; onClose: () => void; } const MENU_W = 240; const MENU_H = 96; export function ParticipantVolumeMenu({ userId, displayName, x, y, onClose, }: Props) { const [volume, setVolume] = useState(() => getParticipantVolume(userId)); // Re-sync from store in case another menu instance changed the same user. useEffect(() => subscribeParticipantVolumes(() => { setVolume(getParticipantVolume(userId)); }), [userId]); // Outside click + Esc to close. useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; const onDown = (e: MouseEvent) => { const target = e.target as HTMLElement | null; if (target?.closest('[data-volume-menu]')) return; onClose(); }; window.addEventListener('keydown', onKey); window.addEventListener('mousedown', onDown); return () => { window.removeEventListener('keydown', onKey); window.removeEventListener('mousedown', onDown); }; }, [onClose]); const left = Math.min(Math.max(8, x), window.innerWidth - MENU_W - 8); const top = Math.min(Math.max(8, y), window.innerHeight - MENU_H - 8); return createPortal(
{displayName} 1 ? 'text-amber-500' : 'text-fg-muted') } > {Math.round(volume * 100)}%
{/* Up to 200% via WebAudio gain. Values above 100% can clip on loud mics — the amber count-up hints at that without a verbose warning. */} { const v = Number(e.target.value); setVolume(v); setParticipantVolume(userId, v); }} aria-label={'Lautstärke ' + displayName} className="w-full accent-accent" />
0% 100% 200%
, document.body, ); }