Merge pull request #1485 from robintown/lobby-updates
Implement new lobby design
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user