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

@@ -9,7 +9,6 @@
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>",
"<0>Oops, something's gone wrong.</0>": "<0>Oops, something's gone wrong.</0>",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Submitting debug logs will help us track down the problem.</0>",
"<0>Thanks for your feedback!</0>": "<0>Thanks for your feedback!</0>",
@@ -18,10 +17,10 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.",
"Audio": "Audio",
"Avatar": "Avatar",
"Back to recents": "Back to recents",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.",
"Call link copied": "Call link copied",
"Camera": "Camera",
"Close": "Close",
"Confirm password": "Confirm password",
@@ -107,7 +106,6 @@
"Submit": "Submit",
"Submit feedback": "Submit feedback",
"Submitting…": "Submitting…",
"Take me Home": "Take me Home",
"Thanks, we received your feedback!": "Thanks, we received your feedback!",
"Thanks!": "Thanks!",
"This call already exists, would you like to join?": "This call already exists, would you like to join?",

View File

@@ -21,6 +21,7 @@ limitations under the License.
align-items: center;
user-select: none;
flex-shrink: 0;
padding-inline: var(--inline-content-inset);
}
.nav {
@@ -28,7 +29,6 @@ limitations under the License.
flex: 1;
align-items: center;
white-space: nowrap;
padding-inline: var(--inline-content-inset);
height: 80px;
}

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>
);
};

View File

@@ -35,7 +35,7 @@ limitations under the License.
width: 100%;
height: 100%;
object-fit: cover;
background-color: #444;
background-color: var(--cpd-color-bg-subtle-primary);
}
.videoTile.isLocal:not(.screenshare) video {