/** * PiPOverlay — floating voice-call status widget shown when the user * is connected to a voice channel but has navigated to a different * channel / DM / route. Presents the channel name, a "Return to * channel" button, and a disconnect button. The widget is draggable * via pointer events. * * The new UI's screen-share video PiP required live LiveKit track * binding from a MobX store; our Convex/React voice context doesn't * expose that the same way, so this is a simpler status widget * focused on the common case: "I'm in a call, I want to get back." */ import { useCallback, useEffect, useRef, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { PhoneDisconnect, ArrowRight } from '@phosphor-icons/react'; import { useVoice } from '../../contexts/VoiceContext'; import styles from './PiPOverlay.module.css'; interface Box { x: number; y: number; } const WIDTH = 260; const HEIGHT = 96; function clamp(v: number, min: number, max: number): number { return Math.max(min, Math.min(max, v)); } export function PiPOverlay() { const navigate = useNavigate(); const location = useLocation(); const voice = useVoice() as any; const [box, setBox] = useState(() => ({ x: Math.max(16, window.innerWidth - WIDTH - 24), y: Math.max(16, window.innerHeight - HEIGHT - 24), })); const dragRef = useRef<{ startX: number; startY: number; startBox: Box } | null>(null); useEffect(() => { const handleMove = (e: PointerEvent) => { const drag = dragRef.current; if (!drag) return; const dx = e.clientX - drag.startX; const dy = e.clientY - drag.startY; setBox({ x: clamp(drag.startBox.x + dx, 0, window.innerWidth - WIDTH), y: clamp(drag.startBox.y + dy, 0, window.innerHeight - HEIGHT), }); }; const handleUp = () => { dragRef.current = null; }; window.addEventListener('pointermove', handleMove); window.addEventListener('pointerup', handleUp); return () => { window.removeEventListener('pointermove', handleMove); window.removeEventListener('pointerup', handleUp); }; }, []); const startDrag = useCallback( (e: React.PointerEvent) => { // Don't initiate a drag when the pointer lands on a button. const target = e.target as HTMLElement; if (target.closest('button')) return; e.preventDefault(); dragRef.current = { startX: e.clientX, startY: e.clientY, startBox: { ...box }, }; }, [box], ); const activeChannelId: string | null = voice?.activeChannelId ?? null; const activeChannelName: string | null = voice?.activeChannelName ?? null; // Hide the overlay when not in a call or when the user is already // looking at the active voice channel. if (!activeChannelId) return null; const onActiveChannelRoute = location.pathname.includes(`/${activeChannelId}`); if (onActiveChannelRoute) return null; const handleReturn = () => { navigate(`/channels/home/${activeChannelId}`); }; const handleDisconnect = () => { voice?.disconnect?.(); }; return (
In voice
#{activeChannelName || 'channel'}
); }