mirror of
https://github.com/vector-im/element-call.git
synced 2026-02-26 05:17:04 +00:00
276 lines
8.3 KiB
TypeScript
276 lines
8.3 KiB
TypeScript
/*
|
|
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<void>;
|
|
enterLabel?: JSX.Element | string;
|
|
confineToRoom: boolean;
|
|
hideHeader: boolean;
|
|
participantCount: number | null;
|
|
onShareClick: (() => void) | null;
|
|
waitingForInvite?: boolean;
|
|
}
|
|
|
|
export const LobbyView: FC<Props> = ({
|
|
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 && (
|
|
<Link className={styles.recents} to="/">
|
|
{t("lobby.leave_button")}
|
|
</Link>
|
|
);
|
|
|
|
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<CreateLocalTracksOptions>(
|
|
() => ({
|
|
// 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 (
|
|
<>
|
|
<div className={classNames(styles.room, inCallStyles.inRoom)}>
|
|
{!hideHeader && (
|
|
<Header>
|
|
<LeftNav>
|
|
<RoomHeaderInfo
|
|
id={matrixInfo.roomId}
|
|
name={matrixInfo.roomName}
|
|
avatarUrl={matrixInfo.roomAvatar}
|
|
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
|
|
participantCount={participantCount}
|
|
/>
|
|
</LeftNav>
|
|
<RightNav>
|
|
{onShareClick !== null && <InviteButton onClick={onShareClick} />}
|
|
</RightNav>
|
|
</Header>
|
|
)}
|
|
<div className={styles.content}>
|
|
<VideoPreview
|
|
matrixInfo={matrixInfo}
|
|
muteStates={muteStates}
|
|
videoTrack={videoTrack}
|
|
>
|
|
<Button
|
|
className={classNames(styles.join, {
|
|
[styles.wait]: waiting,
|
|
})}
|
|
size={waiting ? "sm" : "lg"}
|
|
disabled={waiting}
|
|
onClick={() => {
|
|
if (!waiting) onEnterCall();
|
|
}}
|
|
data-testid="lobby_joinCall"
|
|
>
|
|
{enterLabel ?? t("lobby.join_button")}
|
|
</Button>
|
|
</VideoPreview>
|
|
{!recentsButtonInFooter && recentsButton}
|
|
</div>
|
|
<div className={inCallStyles.footer}>
|
|
{recentsButtonInFooter && recentsButton}
|
|
<div className={inCallStyles.buttons}>
|
|
<MicButton
|
|
muted={!muteStates.audio.enabled}
|
|
onClick={onAudioPress}
|
|
disabled={muteStates.audio.setEnabled === null}
|
|
/>
|
|
<VideoButton
|
|
muted={!muteStates.video.enabled}
|
|
onClick={onVideoPress}
|
|
disabled={muteStates.video.setEnabled === null}
|
|
/>
|
|
<SettingsButton onClick={openSettings} />
|
|
{!confineToRoom && <EndCallButton onClick={onLeaveClick} />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{client && (
|
|
<SettingsModal
|
|
client={client}
|
|
open={settingsModalOpen}
|
|
onDismiss={closeSettings}
|
|
tab={settingsTab}
|
|
onTabChange={setSettingsTab}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|