import React from 'react'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { getMemberDisplayName } from '../../utils/room'; import { getMxIdLocalPart } from '../../utils/matrix'; import { useRoomCallMembers } from './useRoomCallMembers'; import { useCall } from './useCall'; import { useSendCursor, useReceiveCursors, RemoteCursorState } from './useRemoteCursor'; import colorMXID from '../../../util/colorMXID'; import * as css from './CallOverlay.css'; /** Props for RemoteCursorOverlay */ interface RemoteCursorOverlayProps { /** The screen share ID to track cursors for */ shareId: string; /** The room ID of the active call */ roomId: string; } /** * Resolves a LiveKit participant identity to a Matrix user ID * LiveKit identities are typically in the format "deviceId_timestamp" */ function resolveParticipantUserId( livekitIdentity: string, callMembers: { userId: string; deviceId: string }[] ): string { const underscoreIndex = livekitIdentity.lastIndexOf('_'); const possibleDeviceId = underscoreIndex > 0 ? livekitIdentity.substring(0, underscoreIndex) : livekitIdentity; const match = callMembers.find((m) => m.deviceId === possibleDeviceId); return match?.userId ?? livekitIdentity; } /** * Single remote cursor dot with label */ function CursorDot({ cursor, displayName, cursorColor, }: { cursor: RemoteCursorState; displayName: string; cursorColor: string; }) { if (!cursor.visible) return null; const left = `${cursor.x * 100}%`; const top = `${cursor.y * 100}%`; return ( <>
{displayName}
); } /** * Overlay that renders remote cursor positions on top of a screen share * Also provides mouse event handlers for sending local cursor position */ export function RemoteCursorOverlay({ shareId, roomId }: RemoteCursorOverlayProps) { const mx = useMatrixClient(); const { callMembers } = useRoomCallMembers(roomId); const { activeCall } = useCall(); const cursors = useReceiveCursors(shareId); const room = mx.getRoom(roomId); const myUserId = mx.getUserId() ?? ''; /** * Gets a display name for a LiveKit participant */ const getDisplayName = (livekitIdentity: string): string => { const userId = resolveParticipantUserId(livekitIdentity, callMembers); if (!room) return getMxIdLocalPart(userId) ?? userId; return getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId; }; // Filter out our own cursor and only show visible cursors const myIdentity = activeCall?.participants.find((p) => { const underscoreIndex = p.lastIndexOf('_'); const deviceId = underscoreIndex > 0 ? p.substring(0, underscoreIndex) : p; const member = callMembers.find((m) => m.deviceId === deviceId); return member?.userId === myUserId; }); const remoteCursors = Array.from(cursors.entries()).filter( ([participantId]) => participantId !== myIdentity ); if (remoteCursors.length === 0) return null; return (
{remoteCursors.map(([participantId, cursor]) => { const userId = resolveParticipantUserId(participantId, callMembers); return ( ); })}
); } /** * Wrapper that provides mouse event handlers for cursor sending on a screenshare * @param shareId - The screen share ID * @returns Mouse event handlers to attach to the container */ export function useScreenShareCursor(shareId: string) { return useSendCursor(shareId); }