/* Copyright 2022-2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ import { ComponentProps, ReactNode, forwardRef, useCallback, useRef, useState, } from "react"; import { animated } from "@react-spring/web"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; import { MicOnSolidIcon, MicOffSolidIcon, MicOffIcon, OverflowHorizontalIcon, VolumeOnIcon, VolumeOffIcon, VisibilityOnIcon, UserProfileIcon, ExpandIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { ContextMenu, MenuItem, ToggleMenuItem, Menu, } from "@vector-im/compound-web"; import { useObservableEagerState } from "observable-hooks"; import styles from "./GridTile.module.css"; import { UserMediaViewModel, useDisplayName, LocalUserMediaViewModel, RemoteUserMediaViewModel, } from "../state/MediaViewModel"; import { Slider } from "../Slider"; import { MediaView } from "./MediaView"; import { useLatest } from "../useLatest"; import { GridTileViewModel } from "../state/TileViewModel"; import { useMergedRefs } from "../useMergedRefs"; import { useReactions } from "../useReactions"; interface TileProps { className?: string; style?: ComponentProps["style"]; targetWidth: number; targetHeight: number; displayName: string; showSpeakingIndicators: boolean; } interface UserMediaTileProps extends TileProps { vm: UserMediaViewModel; mirror: boolean; menuStart?: ReactNode; menuEnd?: ReactNode; } const UserMediaTile = forwardRef( ( { vm, showSpeakingIndicators, menuStart, menuEnd, className, displayName, ...props }, ref, ) => { const { t } = useTranslation(); const video = useObservableEagerState(vm.video); const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning); const audioEnabled = useObservableEagerState(vm.audioEnabled); const videoEnabled = useObservableEagerState(vm.videoEnabled); const speaking = useObservableEagerState(vm.speaking); const cropVideo = useObservableEagerState(vm.cropVideo); const onSelectFitContain = useCallback( (e: Event) => { e.preventDefault(); vm.toggleFitContain(); }, [vm], ); const { raisedHands } = useReactions(); const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon; const [menuOpen, setMenuOpen] = useState(false); const menu = ( <> {menuStart} {menuEnd} ); const handRaised: Date | undefined = raisedHands[vm.member?.userId ?? ""]; const showSpeaking = showSpeakingIndicators && speaking; const tile = ( } displayName={displayName} primaryButton={ } side="left" align="start" > {menu} } raisedHandTime={handRaised} {...props} /> ); return ( {menu} ); }, ); UserMediaTile.displayName = "UserMediaTile"; interface LocalUserMediaTileProps extends TileProps { vm: LocalUserMediaViewModel; onOpenProfile: (() => void) | null; } const LocalUserMediaTile = forwardRef( ({ vm, onOpenProfile, ...props }, ref) => { const { t } = useTranslation(); const mirror = useObservableEagerState(vm.mirror); const alwaysShow = useObservableEagerState(vm.alwaysShow); const latestAlwaysShow = useLatest(alwaysShow); const onSelectAlwaysShow = useCallback( (e: Event) => { e.preventDefault(); vm.setAlwaysShow(!latestAlwaysShow.current); }, [vm, latestAlwaysShow], ); return ( } menuEnd={ onOpenProfile && ( ) } {...props} /> ); }, ); LocalUserMediaTile.displayName = "LocalUserMediaTile"; interface RemoteUserMediaTileProps extends TileProps { vm: RemoteUserMediaViewModel; } const RemoteUserMediaTile = forwardRef< HTMLDivElement, RemoteUserMediaTileProps >(({ vm, ...props }, ref) => { const { t } = useTranslation(); const locallyMuted = useObservableEagerState(vm.locallyMuted); const localVolume = useObservableEagerState(vm.localVolume); const onSelectMute = useCallback( (e: Event) => { e.preventDefault(); vm.toggleLocallyMuted(); }, [vm], ); const onChangeLocalVolume = useCallback( (v: number) => vm.setLocalVolume(v), [vm], ); const onCommitLocalVolume = useCallback(() => vm.commitLocalVolume(), [vm]); const VolumeIcon = locallyMuted ? VolumeOffIcon : VolumeOnIcon; return ( {/* TODO: Figure out how to make this slider keyboard accessible */} } {...props} /> ); }); RemoteUserMediaTile.displayName = "RemoteUserMediaTile"; interface GridTileProps { vm: GridTileViewModel; onOpenProfile: (() => void) | null; targetWidth: number; targetHeight: number; className?: string; style?: ComponentProps["style"]; showSpeakingIndicators: boolean; } export const GridTile = forwardRef( ({ vm, onOpenProfile, ...props }, theirRef) => { const ourRef = useRef(null); const ref = useMergedRefs(ourRef, theirRef); const media = useObservableEagerState(vm.media); const displayName = useDisplayName(media); if (media instanceof LocalUserMediaViewModel) { return ( ); } else { return ( ); } }, ); GridTile.displayName = "GridTile";