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.
This commit is contained in:
132
src/app/features/call/RemoteCursorOverlay.tsx
Normal file
132
src/app/features/call/RemoteCursorOverlay.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user