Implement new lobby design

This commit is contained in:
Robin
2023-09-18 15:45:48 -04:00
parent f3e8ee6913
commit 771ab41833
8 changed files with 188 additions and 207 deletions

View File

@@ -21,7 +21,7 @@ limitations under the License.
min-height: 100%;
height: 100%;
width: 100%;
--footerPadding: 8px;
--footerPadding: var(--cpd-space-4x);
--footerHeight: calc(50px + 2 * var(--footerPadding));
}
@@ -83,17 +83,19 @@ limitations under the License.
justify-self: end;
}
@media (min-height: 300px) {
@media (min-height: 400px) {
.inRoom {
--footerPadding: 40px;
--footerPadding: var(--cpd-space-10x);
}
}
@media (min-height: 800px) {
.inRoom {
--footerPadding: var(--cpd-space-15x);
}
}
@media (min-width: 800px) {
.inRoom {
--footerPadding: 60px;
}
.buttons {
gap: var(--cpd-space-4x);
}

View File

@@ -14,61 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.room {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 100%;
}
.joinRoom {
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--cpd-space-6x);
flex: 1;
overflow: hidden;
height: 100%;
padding-block-end: var(--footerHeight);
}
.joinRoomContent {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
@media (max-width: 500px) {
.join {
width: 100%;
}
}
.joinRoomFooter {
margin: 20px 0;
}
.homeLink {
margin-top: 50px;
}
.joinCallButton {
position: absolute;
width: 100%;
max-width: 222px;
height: 40px;
bottom: 86px;
left: 50%;
font-weight: 600;
font-size: var(--font-size-body);
transform: translateX(-50%);
}
.copyButton {
width: 320px !important;
margin-bottom: 15px;
}
.copyButton:last-child {
margin-bottom: 0;
}
.passwordField {
width: 320px !important;
margin-bottom: 20px;
flex: 0;
@media (min-height: 650px) {
.content {
gap: var(--cpd-space-10x);
}
}

View File

@@ -14,20 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useRef, useEffect, FC } from "react";
import { Trans, useTranslation } from "react-i18next";
import { FC, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
import { Button, Link } from "@vector-im/compound-web";
import classNames from "classnames";
import { useHistory } from "react-router-dom";
import styles from "./LobbyView.module.css";
import { Button, CopyButton } from "../button";
import inCallStyles from "./InCallView.module.css";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { getRoomUrl } from "../matrix-utils";
import { Body, Link } from "../typography/Typography";
import { useLocationNavigation } from "../useLocationNavigation";
import { MatrixInfo, VideoPreview } from "./VideoPreview";
import { MuteStates } from "./MuteStates";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { ShareButton } from "../button/ShareButton";
import {
HangupButton,
MicButton,
SettingsButton,
VideoButton,
} from "../button/Button";
import { SettingsModal } from "../settings/SettingsModal";
import { useMediaQuery } from "../useMediaQuery";
interface Props {
client: MatrixClient;
@@ -51,68 +59,98 @@ export const LobbyView: FC<Props> = ({
onShareClick,
}) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(matrixInfo.roomId);
useLocationNavigation();
const joinCallButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (joinCallButtonRef.current) {
joinCallButtonRef.current.focus();
}
}, [joinCallButtonRef]);
const onAudioPress = useCallback(
() => muteStates.audio.setEnabled?.((e) => !e),
[muteStates]
);
const onVideoPress = useCallback(
() => muteStates.video.setEnabled?.((e) => !e),
[muteStates]
);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const openSettings = useCallback(
() => setSettingsModalOpen(true),
[setSettingsModalOpen]
);
const closeSettings = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen]
);
const history = useHistory();
const onLeaveClick = useCallback(() => history.push("/"), [history]);
const recentsButtonInFooter = useMediaQuery("(max-height: 500px)");
const recentsButton = !isEmbedded && (
<Link className={styles.recents} href="#" onClick={onLeaveClick}>
{t("Back to recents")}
</Link>
);
// 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={styles.room}>
{!hideHeader && (
<Header>
<LeftNav>
<RoomHeaderInfo
id={matrixInfo.roomId}
name={matrixInfo.roomName}
avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.roomEncrypted}
participants={participatingMembers}
client={client}
/>
</LeftNav>
<RightNav>
{onShareClick !== null && <ShareButton onClick={onShareClick} />}
</RightNav>
</Header>
)}
<div className={styles.joinRoom}>
<div className={styles.joinRoomContent}>
<VideoPreview matrixInfo={matrixInfo} muteStates={muteStates} />
<Trans>
<>
<div className={classNames(styles.room, inCallStyles.inRoom)}>
{!hideHeader && (
<Header>
<LeftNav>
<RoomHeaderInfo
id={matrixInfo.roomId}
name={matrixInfo.roomName}
avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.roomEncrypted}
participants={participatingMembers}
client={client}
/>
</LeftNav>
<RightNav>
{onShareClick !== null && <ShareButton onClick={onShareClick} />}
</RightNav>
</Header>
)}
<div className={styles.content}>
<VideoPreview matrixInfo={matrixInfo} muteStates={muteStates}>
<Button
ref={joinCallButtonRef}
className={styles.copyButton}
className={styles.join}
size="lg"
onPress={() => onEnter()}
onClick={onEnter}
data-testid="lobby_joinCall"
>
Join call now
{t("Join call")}
</Button>
<Body>Or</Body>
<CopyButton
variant="secondaryCopy"
value={getRoomUrl(matrixInfo.roomId, roomSharedKey ?? undefined)}
className={styles.copyButton}
copiedMessage={t("Call link copied")}
data-testid="lobby_inviteLink"
>
Copy call link and join later
</CopyButton>
</Trans>
</VideoPreview>
{!recentsButtonInFooter && recentsButton}
</div>
<div className={inCallStyles.footer}>
{recentsButtonInFooter && recentsButton}
<div className={inCallStyles.buttons}>
<VideoButton
muted={!muteStates.video.enabled}
onPress={onVideoPress}
disabled={muteStates.video.setEnabled === null}
/>
<MicButton
muted={!muteStates.audio.enabled}
onPress={onAudioPress}
disabled={muteStates.audio.setEnabled === null}
/>
<SettingsButton onPress={openSettings} />
{!isEmbedded && <HangupButton onPress={onLeaveClick} />}
</div>
</div>
{!isEmbedded && (
<Body className={styles.joinRoomFooter}>
<Link color="primary" to="/">
{t("Take me Home")}
</Link>
</Body>
)}
</div>
</div>
{client && (
<SettingsModal
client={client}
open={settingsModalOpen}
onDismiss={closeSettings}
/>
)}
</>
);
};

View File

@@ -15,21 +15,29 @@ limitations under the License.
*/
.preview {
position: relative;
min-height: 280px;
height: 50vh;
border-radius: 24px;
overflow: hidden;
background-color: var(--stopgap-bgColor3);
margin: 20px;
margin-inline: var(--inline-content-inset);
min-block-size: 0;
block-size: 50vh;
}
.preview video {
.preview.content {
margin-inline: 0;
}
.content {
position: relative;
block-size: 100%;
inline-size: 100%;
overflow: hidden;
}
.content video {
width: 100%;
height: 100%;
object-fit: contain;
object-fit: cover;
background-color: black;
transform: scaleX(-1);
background-color: var(--cpd-color-bg-subtle-primary);
}
.avatarContainer {
@@ -41,40 +49,32 @@ limitations under the License.
display: flex;
justify-content: center;
align-items: center;
background-color: var(--stopgap-bgColor3);
background-color: var(--cpd-color-bg-subtle-secondary);
}
.cameraPermissions {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
text-align: center;
}
.previewButtons {
.buttonBar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 66px;
height: calc(30 * var(--cpd-space-1x));
display: flex;
justify-content: center;
align-items: center;
background-color: var(--stopgap-background-85);
gap: var(--cpd-space-4x);
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
var(--cpd-color-bg-canvas-default) 100%
);
}
.previewButtons > * {
margin-right: 30px;
.preview.content .buttonBar {
padding-inline: var(--inline-content-inset);
}
.previewButtons > :last-child {
margin-right: 0px;
}
@media (min-width: 800px) {
.preview {
margin-top: 40px;
@media (min-aspect-ratio: 1 / 1) {
.preview video {
aspect-ratio: 16 / 9;
}
}

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useEffect, useCallback, useMemo, useRef, FC, useState } from "react";
import { useEffect, useMemo, useRef, FC, ReactNode } from "react";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { usePreviewTracks } from "@livekit/components-react";
@@ -23,14 +23,14 @@ import {
LocalVideoTrack,
Track,
} from "livekit-client";
import classNames from "classnames";
import { MicButton, SettingsButton, VideoButton } from "../button";
import { Avatar } from "../Avatar";
import styles from "./VideoPreview.module.css";
import { SettingsModal } from "../settings/SettingsModal";
import { useClient } from "../ClientContext";
import { useMediaDevices } from "../livekit/MediaDevicesContext";
import { MuteStates } from "./MuteStates";
import { Glass } from "../Glass";
import { useMediaQuery } from "../useMediaQuery";
export type MatrixInfo = {
userId: string;
@@ -46,23 +46,16 @@ export type MatrixInfo = {
interface Props {
matrixInfo: MatrixInfo;
muteStates: MuteStates;
children: ReactNode;
}
export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
const { client } = useClient();
export const VideoPreview: FC<Props> = ({
matrixInfo,
muteStates,
children,
}) => {
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const openSettings = useCallback(
() => setSettingsModalOpen(true),
[setSettingsModalOpen]
);
const closeSettings = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen]
);
const devices = useMediaDevices();
// Capture the audio options as they were when we first mounted, because
@@ -110,50 +103,35 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
};
}, [videoTrack]);
const onAudioPress = useCallback(
() => muteStates.audio.setEnabled?.((e) => !e),
[muteStates]
);
const onVideoPress = useCallback(
() => muteStates.video.setEnabled?.((e) => !e),
[muteStates]
const content = (
<>
<video ref={videoEl} muted playsInline disablePictureInPicture />
{!muteStates.video.enabled && (
<div className={styles.avatarContainer}>
<Avatar
id={matrixInfo.userId}
name={matrixInfo.displayName}
size={Math.min(previewBounds.width, previewBounds.height) / 2}
src={matrixInfo.avatarUrl}
/>
</div>
)}
<div className={styles.buttonBar}>{children}</div>
</>
);
return (
<div className={styles.preview} ref={previewRef}>
<video ref={videoEl} muted playsInline disablePictureInPicture />
<>
{!muteStates.video.enabled && (
<div className={styles.avatarContainer}>
<Avatar
id={matrixInfo.userId}
name={matrixInfo.displayName}
size={(previewBounds.height - 66) / 2}
src={matrixInfo.avatarUrl}
/>
</div>
)}
<div className={styles.previewButtons}>
<VideoButton
muted={!muteStates.video.enabled}
onPress={onVideoPress}
disabled={muteStates.video.setEnabled === null}
/>
<MicButton
muted={!muteStates.audio.enabled}
onPress={onAudioPress}
disabled={muteStates.audio.setEnabled === null}
/>
<SettingsButton onPress={openSettings} />
</div>
</>
{client && (
<SettingsModal
client={client}
open={settingsModalOpen}
onDismiss={closeSettings}
/>
)}
return useMediaQuery("(max-width: 550px)") ? (
<div
className={classNames(styles.preview, styles.content)}
ref={previewRef}
>
{content}
</div>
) : (
<Glass className={styles.preview}>
<div className={styles.content} ref={previewRef}>
{content}
</div>
</Glass>
);
};