import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { useCall } from '../context/CallContext'; import { getScreenShareVolume, setScreenShareVolume, subscribeScreenShareVolumes, } from '../lib/screenShareVolumes'; import { HeadphonesIcon, HeadphonesOffIcon, MonitorShareIcon, PhoneOffIcon } from './icons'; interface Props { /** Participant whose screen share the user right-clicked. */ userId: string; displayName: string; /** Whether the share has an audio track published. Controls whether the * volume / mute rows render — without audio those would be no-ops. */ hasAudio: boolean; x: number; y: number; onClose: () => void; } const MENU_W = 260; const MENU_H_WITH_AUDIO = 200; const MENU_H_NO_AUDIO = 96; // Context menu surfaced on right-click of a remote screen share. Mirrors // Discord's stream menu: volume slider, audio mute toggle (independent of // volume — matches HTMLMediaElement's `muted` field), and "stop watching" // which both un-subscribes locally and dismisses the tile from the grid. export function ScreenShareContextMenu({ userId, displayName, hasAudio, x, y, onClose, }: Props) { const { t } = useTranslation(['app']); const { dismissShare, screenShareAudioMutedIds, setScreenShareAudioMuted, } = useCall(); const [volume, setVolume] = useState(() => getScreenShareVolume(userId)); useEffect( () => subscribeScreenShareVolumes(() => { setVolume(getScreenShareVolume(userId)); }), [userId], ); 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-share-menu]')) return; onClose(); }; window.addEventListener('keydown', onKey); window.addEventListener('mousedown', onDown); return () => { window.removeEventListener('keydown', onKey); window.removeEventListener('mousedown', onDown); }; }, [onClose]); const muted = screenShareAudioMutedIds.has(userId); const height = hasAudio ? MENU_H_WITH_AUDIO : MENU_H_NO_AUDIO; const left = Math.min(Math.max(8, x), window.innerWidth - MENU_W - 8); const top = Math.min(Math.max(8, y), window.innerHeight - height - 8); return createPortal(
{t('app:call.share_menu_owner', { defaultValue: 'Bildschirmfreigabe · {{name}}', name: displayName, })}
{hasAudio && ( <>
{t('app:call.share_volume', { defaultValue: 'Lautstärke' })} 1 ? 'text-amber-500' : 'text-fg-muted') } > {Math.round(volume * 100)}%
{ const v = Number(e.target.value); setVolume(v); setScreenShareVolume(userId, v); }} aria-label={t('app:call.share_volume', { defaultValue: 'Lautstärke' })} className="w-full accent-accent" />
0% 100% 200%
)}
, document.body, ); }