/* Copyright 2022-2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { type FC, useCallback, useMemo, useState, type JSX, useEffect, } from "react"; import { useTranslation } from "react-i18next"; import { type MatrixClient } from "matrix-js-sdk"; import { Button } from "@vector-im/compound-web"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/lib/logger"; import { usePreviewTracks } from "@livekit/components-react"; import { type CreateLocalTracksOptions, type LocalVideoTrack, Track, } from "livekit-client"; import { useObservableEagerState } from "observable-hooks"; import { useNavigate } from "react-router-dom"; import inCallStyles from "./InCallView.module.css"; import styles from "./LobbyView.module.css"; import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header"; import { type MatrixInfo, VideoPreview } from "./VideoPreview"; import { type MuteStates } from "./MuteStates"; import { InviteButton } from "../button/InviteButton"; import { EndCallButton, MicButton, SettingsButton, VideoButton, } from "../button/Button"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; import { useMediaQuery } from "../useMediaQuery"; import { E2eeType } from "../e2ee/e2eeType"; import { Link } from "../button/Link"; import { useMediaDevices } from "../MediaDevicesContext"; import { useInitial } from "../useInitial"; import { useTrackProcessor, useTrackProcessorSync, } from "../livekit/TrackProcessorContext"; import { usePageTitle } from "../usePageTitle"; import { useLatest } from "../useLatest"; import { getValue } from "../utils/observable"; interface Props { client: MatrixClient; matrixInfo: MatrixInfo; muteStates: MuteStates; onEnter: () => Promise; enterLabel?: JSX.Element | string; confineToRoom: boolean; hideHeader: boolean; participantCount: number | null; onShareClick: (() => void) | null; waitingForInvite?: boolean; } export const LobbyView: FC = ({ client, matrixInfo, muteStates, onEnter, enterLabel, confineToRoom, hideHeader, participantCount, onShareClick, waitingForInvite, }) => { useEffect(() => { logger.info("[Lifecycle] GroupCallView Component mounted"); return (): void => { logger.info("[Lifecycle] GroupCallView Component unmounted"); }; }, []); const { t } = useTranslation(); usePageTitle(matrixInfo.roomName); const onAudioPress = useCallback( () => muteStates.audio.setEnabled?.((e) => !e), [muteStates], ); const onVideoPress = useCallback( () => muteStates.video.setEnabled?.((e) => !e), [muteStates], ); const [settingsModalOpen, setSettingsModalOpen] = useState(false); const [settingsTab, setSettingsTab] = useState(defaultSettingsTab); const openSettings = useCallback( () => setSettingsModalOpen(true), [setSettingsModalOpen], ); const closeSettings = useCallback( () => setSettingsModalOpen(false), [setSettingsModalOpen], ); const navigate = useNavigate(); const onLeaveClick = useCallback(() => { navigate("/")?.catch((error) => { logger.error("Failed to navigate to /", error); }); }, [navigate]); const recentsButtonInFooter = useMediaQuery("(max-height: 500px)"); const recentsButton = !confineToRoom && ( {t("lobby.leave_button")} ); const devices = useMediaDevices(); const videoInputId = useObservableEagerState( devices.videoInput.selected$, )?.id; // Capture the audio options as they were when we first mounted, because // we're not doing anything with the audio anyway so we don't need to // re-open the devices when they change (see below). const initialAudioOptions = useInitial( () => muteStates.audio.enabled && { deviceId: getValue(devices.audioInput.selected$)?.id, }, ); const { processor } = useTrackProcessor(); const initialProcessor = useInitial(() => processor); const localTrackOptions = useMemo( () => ({ // The only reason we request audio here is to get the audio permission // request over with at the same time. But changing the audio settings // shouldn't cause this hook to recreate the track, which is why we // reference the initial values here. // We also pass in a clone because livekit mutates the object passed in, // which would cause the devices to be re-opened on the next render. audio: Object.assign({}, initialAudioOptions), video: muteStates.video.enabled && { deviceId: videoInputId, processor: initialProcessor, }, }), [ initialAudioOptions, muteStates.video.enabled, videoInputId, initialProcessor, ], ); const latestMuteStates = useLatest(muteStates); const onError = useCallback( (error: Error) => { logger.error("Error while creating preview Tracks:", error); latestMuteStates.current.audio.setEnabled?.(false); latestMuteStates.current.video.setEnabled?.(false); }, [latestMuteStates], ); const tracks = usePreviewTracks(localTrackOptions, onError); const videoTrack = useMemo( () => (tracks?.find((t) => t.kind === Track.Kind.Video) ?? null) as LocalVideoTrack | null, [tracks], ); useEffect(() => { if (videoTrack && videoInputId === undefined) { // If we have a video track but no videoInputId, // we have to update the available devices. So that we select the first // available video input device as the default instead of the `""` id. devices.requestDeviceNames(); } }, [devices, videoInputId, videoTrack]); useTrackProcessorSync(videoTrack); const onEnterCall = useCallback(() => { setWaiting(true); void onEnter().finally(() => setWaiting(false)); }, [onEnter]); const [waiting, setWaiting] = useState(waitingForInvite); // TODO: Unify this component with InCallView, so we can get slick joining // animations and don't have to feel bad about reusing its CSS return ( <>
{!hideHeader && (
{onShareClick !== null && }
)}
{!recentsButtonInFooter && recentsButton}
{recentsButtonInFooter && recentsButton}
{!confineToRoom && }
{client && ( )} ); };