/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
ComputerIcon,
UserProfileSolidIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { type TrackReference } from "@livekit/components-core";
import { VideoTrack } from "@livekit/components-react";
import classNames from "classnames";
import { type FC, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { type MediaViewModel } from "../state/media/MediaViewModel";
import { type ScreenShareViewModel } from "../state/media/ScreenShareViewModel";
import { type LocalScreenShareViewModel } from "../state/media/LocalScreenShareViewModel";
import { type RemoteScreenShareViewModel } from "../state/media/RemoteScreenShareViewModel";
import { useBehavior } from "../useBehavior";
import styles from "./SpotlightIndicator.module.css";
interface SpotlightIndicatorProps {
vm: MediaViewModel;
visible: boolean;
focusable: boolean;
/**
* Whether to attach the screen share preview. The indicator row is kept
* mounted even while hidden so that it can fade, but LiveKit's visibility
* detection ignores opacity, so an attached preview would keep streaming
* while invisible.
*/
showPreview: boolean;
onClick: (id: string) => void;
}
interface ScreenShareIndicatorPreviewProps {
vm: ScreenShareViewModel;
displayName: string;
showPreview: boolean;
}
interface ScreenShareIndicatorPreviewContentProps {
video: TrackReference | undefined;
videoEnabled: boolean;
displayName: string;
}
const screenShareAspectRatio = (video: TrackReference | undefined): number => {
const { width, height } = video?.publication.dimensions ?? {};
return width && height ? width / height : 16 / 9;
};
const ScreenShareIndicatorPreviewContent: FC<
ScreenShareIndicatorPreviewContentProps
> = ({ video, videoEnabled, displayName }) => {
const [aspectRatio, setAspectRatio] = useState(() =>
screenShareAspectRatio(video),
);
useEffect(() => setAspectRatio(screenShareAspectRatio(video)), [video]);
return (
<>
{video !== undefined && videoEnabled ? (
{
const { videoWidth, videoHeight } = event.currentTarget;
if (videoWidth > 0 && videoHeight > 0)
setAspectRatio(videoWidth / videoHeight);
}}
/>
) : (
)}
{displayName}
>
);
};
interface LocalScreenShareIndicatorPreviewProps {
vm: LocalScreenShareViewModel;
displayName: string;
showPreview: boolean;
}
const LocalScreenShareIndicatorPreview: FC<
LocalScreenShareIndicatorPreviewProps
> = ({ vm, displayName, showPreview }) => {
const video = useBehavior(vm.video$);
return (
);
};
interface RemoteScreenShareIndicatorPreviewProps {
vm: RemoteScreenShareViewModel;
displayName: string;
showPreview: boolean;
}
const RemoteScreenShareIndicatorPreview: FC<
RemoteScreenShareIndicatorPreviewProps
> = ({ vm, displayName, showPreview }) => {
const video = useBehavior(vm.video$);
const videoEnabled = useBehavior(vm.videoEnabled$);
return (
);
};
const ScreenShareIndicatorPreview: FC = ({
vm,
displayName,
showPreview,
}) =>
vm.local ? (
) : (
);
export const SpotlightIndicator: FC = ({
vm,
visible,
focusable,
showPreview,
onClick,
}) => {
const { t } = useTranslation();
const displayName = useBehavior(vm.displayName$);
const screenShare = vm.type === "screen share";
const label = screenShare
? t("video_tile.screen_share_name", { displayName })
: displayName;
const onPreviewIndicatorClick = useCallback(
() => onClick(vm.id),
[onClick, vm.id],
);
return (
);
};
SpotlightIndicator.displayName = "SpotlightIndicator";