Files
cinny/src/app/features/call/RemoteCursorOverlay.tsx
Max Litruv Boonzaayer 3f6f2134ad feat(call): add docked call panel and remote cursor overlay
- Implemented DockedCallPanel component to render the docked call interface when an active call is present.
- Created RemoteCursorOverlay component to display remote cursor positions during screen sharing.
- Added hooks for managing docked call state and remote cursor functionality.
- Introduced pending drag state management for seamless transitions between docked and floating states.
- Developed embed filter management components for personal and room-wide settings, allowing users to customize URL embed visibility.
- Implemented auto-joining functionality for rooms within spaces, enhancing user experience during space navigation.
- Added state management for tracking joining progress of rooms in spaces.
2026-02-19 17:49:48 +11:00

133 lines
3.9 KiB
TypeScript

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 (
<>
<div
className={css.RemoteCursorDot}
style={{
left,
top,
backgroundColor: cursorColor,
opacity: cursor.visible ? 1 : 0,
}}
/>
<div
className={css.RemoteCursorLabel}
style={{ left, top, color: 'white' }}
>
{displayName}
</div>
</>
);
}
/**
* 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 (
<div className={css.RemoteCursorOverlay}>
{remoteCursors.map(([participantId, cursor]) => {
const userId = resolveParticipantUserId(participantId, callMembers);
return (
<CursorDot
key={participantId}
cursor={cursor}
displayName={getDisplayName(participantId)}
cursorColor={colorMXID(userId)}
/>
);
})}
</div>
);
}
/**
* 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);
}