/* 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";