import React, { useEffect, useState } from 'react'; import { Box, Icon, IconButton, Icons, Scroll, Text, Tooltip, TooltipProvider, color } from 'folds'; import { useSetAtom } from 'jotai'; import { useCall } from './useCall'; import { useRoomCallMembers } from './useRoomCallMembers'; import { CallState } from './types'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { pendingUndockDragAtom } from './pendingDragState'; import { useIsCallDockedSidebar } from './useDockedCall'; import { getMemberDisplayName, getMemberAvatarMxc } from '../../utils/room'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { UserAvatar } from '../../components/user-avatar'; import { useSelectedRoom } from '../../hooks/router/useSelectedRoom'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import * as css from './CallOverlay.css'; /** * Formats call duration in MM:SS format */ function formatDuration(seconds: number): string { const mins = Math.floor(seconds / 60); const secs = seconds % 60; const secsStr = secs < 10 ? `0${secs}` : `${secs}`; return `${mins}:${secsStr}`; } /** * Sidebar docked call panel - renders at bottom of room list */ export function SidebarDockedCallPanel() { const isCallDockedSidebar = useIsCallDockedSidebar(); const { activeCall, endCall, toggleMute, toggleDeafen } = useCall(); const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const selectedRoomId = useSelectedRoom(); const { navigateRoom } = useRoomNavigate(); const [duration, setDuration] = useState(0); const [showParticipants, setShowParticipants] = useState(true); const [, setIsDocked] = useSetting(settingsAtom, 'isCallPanelDocked'); const setPendingDrag = useSetAtom(pendingUndockDragAtom); const { callMembers } = useRoomCallMembers(activeCall?.roomId ?? ''); const myUserId = mx.getUserId() ?? ''; useEffect(() => { if (!activeCall || activeCall.state !== CallState.Connected) { setDuration(0); return undefined; } const { startTime } = activeCall; const updateDuration = () => { const elapsed = Math.floor((Date.now() - startTime) / 1000); setDuration(elapsed); }; updateDuration(); const interval = setInterval(updateDuration, 1000); return () => clearInterval(interval); }, [activeCall]); /** * Checks if a participant is currently speaking */ const isParticipantSpeaking = (userId: string): boolean => { if (!activeCall) return false; if (activeCall.activeSpeakers.has(userId)) return true; const member = callMembers.find((m) => m.userId === userId); if (!member) return false; return Array.from(activeCall.activeSpeakers).some((speakerId) => { const underscoreIndex = speakerId.lastIndexOf('_'); const deviceId = underscoreIndex > 0 ? speakerId.substring(0, underscoreIndex) : speakerId; return deviceId === member.deviceId; }); }; /** * Checks if a participant is currently muted */ const isParticipantMuted = (userId: string): boolean => { if (!activeCall) return false; if (userId === myUserId) return activeCall.isMuted; const member = callMembers.find((m) => m.userId === userId); if (!member) return false; return Array.from(activeCall.mutedParticipants).some((mutedId) => { const underscoreIndex = mutedId.lastIndexOf('_'); const deviceId = underscoreIndex > 0 ? mutedId.substring(0, underscoreIndex) : mutedId; return deviceId === member.deviceId; }); }; /** * Handle mousedown on header to start drag-to-undock */ const handleHeaderMouseDown = async (e: React.MouseEvent) => { if ((e.target as HTMLElement).closest('button')) return; if (document.fullscreenElement) { try { await document.exitFullscreen(); } catch { // Ignore } } const header = e.currentTarget as HTMLElement; const rect = header.getBoundingClientRect(); setPendingDrag({ startX: e.clientX, startY: e.clientY, offsetX: e.clientX - rect.left, offsetY: e.clientY - rect.top, timestamp: Date.now(), }); setIsDocked(false); }; if (!isCallDockedSidebar || !activeCall || activeCall.state === CallState.Idle || activeCall.state === CallState.Ended) { return null; } const isConnecting = activeCall.state === CallState.Connecting; const room = mx.getRoom(activeCall.roomId); const allParticipants = callMembers.map((m) => m.userId); const participantCount = allParticipants.length; return (