Clean up room-related components
This commit is contained in:
parent
8be578763d
commit
550c45b69e
9 changed files with 598 additions and 700 deletions
43
src/App.jsx
43
src/App.jsx
|
@ -14,26 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
useLocation,
|
||||
useHistory,
|
||||
} from "react-router-dom";
|
||||
import React from "react";
|
||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { OverlayProvider } from "@react-aria/overlays";
|
||||
import { HomePage } from "./home/HomePage";
|
||||
import { LoginPage } from "./LoginPage";
|
||||
import { RegisterPage } from "./RegisterPage";
|
||||
import { Room } from "./Room";
|
||||
import {
|
||||
ClientProvider,
|
||||
defaultHomeserverHost,
|
||||
} from "./ConferenceCallManagerHooks";
|
||||
import { LoadingView } from "./FullScreenView";
|
||||
import { RoomPage } from "./room/RoomPage";
|
||||
import { RoomRedirect } from "./room/RoomRedirect";
|
||||
import { ClientProvider } from "./ConferenceCallManagerHooks";
|
||||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
export default function App({ history }) {
|
||||
|
@ -54,7 +46,7 @@ export default function App({ history }) {
|
|||
<RegisterPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/room/:roomId?">
|
||||
<Room />
|
||||
<RoomPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="*">
|
||||
<RoomRedirect />
|
||||
|
@ -65,24 +57,3 @@ export default function App({ history }) {
|
|||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
function RoomRedirect() {
|
||||
const { pathname } = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
let roomId = pathname;
|
||||
|
||||
if (pathname.startsWith("/")) {
|
||||
roomId = roomId.substr(1, roomId.length);
|
||||
}
|
||||
|
||||
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {
|
||||
roomId = `#${roomId}:${defaultHomeserverHost}`;
|
||||
}
|
||||
|
||||
history.replace(`/room/${roomId}`);
|
||||
}, [pathname, history]);
|
||||
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
|
447
src/Room.jsx
447
src/Room.jsx
|
@ -1,447 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import styles from "./Room.module.css";
|
||||
import { useLocation, useParams, useHistory, Link } from "react-router-dom";
|
||||
import {
|
||||
Button,
|
||||
CopyButton,
|
||||
HangupButton,
|
||||
MicButton,
|
||||
VideoButton,
|
||||
ScreenshareButton,
|
||||
LinkButton,
|
||||
} from "./button";
|
||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "./Header";
|
||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import VideoGrid, {
|
||||
useVideoGridLayout,
|
||||
} from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
|
||||
import SimpleVideoGrid from "matrix-react-sdk/src/components/views/voip/GroupCallView/SimpleVideoGrid";
|
||||
import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
|
||||
import { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
|
||||
import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed";
|
||||
import { useMediaStream } from "matrix-react-sdk/src/hooks/useMediaStream";
|
||||
import {
|
||||
getAvatarUrl,
|
||||
getRoomUrl,
|
||||
useClient,
|
||||
useLoadGroupCall,
|
||||
useProfile,
|
||||
} from "./ConferenceCallManagerHooks";
|
||||
import { ErrorView, LoadingView, FullScreenView } from "./FullScreenView";
|
||||
import { GroupCallInspector } from "./GroupCallInspector";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { OverflowMenu } from "./OverflowMenu";
|
||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
import classNames from "classnames";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { UserMenuContainer } from "./UserMenuContainer";
|
||||
import { LobbyView } from "./room/LobbyView";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
// or with getUsermedia and getDisplaymedia being used within the same session.
|
||||
// For now we can disable screensharing in Safari.
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
export function Room() {
|
||||
const [registrationError, setRegistrationError] = useState();
|
||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||
useClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !isAuthenticated) {
|
||||
setRegistrationError(new Error("Must be registered"));
|
||||
}
|
||||
}, [loading, isAuthenticated]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
if (registrationError || error) {
|
||||
return <ErrorView error={registrationError || error} />;
|
||||
}
|
||||
|
||||
return <GroupCall client={client} isPasswordlessUser={isPasswordlessUser} />;
|
||||
}
|
||||
|
||||
export function GroupCall({ client, isPasswordlessUser }) {
|
||||
const { roomId: maybeRoomId } = useParams();
|
||||
const { hash, search } = useLocation();
|
||||
const [simpleGrid, viaServers] = useMemo(() => {
|
||||
const params = new URLSearchParams(search);
|
||||
return [params.has("simple"), params.getAll("via")];
|
||||
}, [search]);
|
||||
const roomId = maybeRoomId || hash;
|
||||
const { loading, error, groupCall } = useLoadGroupCall(
|
||||
client,
|
||||
roomId,
|
||||
viaServers
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.groupCall = groupCall;
|
||||
}, [groupCall]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingRoomView />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupCallView
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
client={client}
|
||||
roomId={roomId}
|
||||
groupCall={groupCall}
|
||||
simpleGrid={simpleGrid}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCallView({
|
||||
client,
|
||||
isPasswordlessUser,
|
||||
roomId,
|
||||
groupCall,
|
||||
simpleGrid,
|
||||
}) {
|
||||
const [showInspector, setShowInspector] = useState(false);
|
||||
const {
|
||||
state,
|
||||
error,
|
||||
activeSpeaker,
|
||||
userMediaFeeds,
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
localCallFeed,
|
||||
initLocalCallFeed,
|
||||
enter,
|
||||
leave,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
toggleScreensharing,
|
||||
isScreensharing,
|
||||
localScreenshareFeed,
|
||||
screenshareFeeds,
|
||||
hasLocalParticipant,
|
||||
} = useGroupCall(groupCall);
|
||||
|
||||
useEffect(() => {
|
||||
function onHangup(call) {
|
||||
if (call.hangupReason === "ice_failed") {
|
||||
Sentry.captureException(new Error("Call hangup due to ICE failure."));
|
||||
}
|
||||
}
|
||||
|
||||
function onError(error) {
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
|
||||
if (groupCall) {
|
||||
groupCall.on("hangup", onHangup);
|
||||
groupCall.on("error", onError);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (groupCall) {
|
||||
groupCall.removeListener("hangup", onHangup);
|
||||
groupCall.removeListener("error", onError);
|
||||
}
|
||||
};
|
||||
}, [groupCall]);
|
||||
|
||||
const [left, setLeft] = useState(false);
|
||||
const history = useHistory();
|
||||
|
||||
const onLeave = useCallback(() => {
|
||||
leave();
|
||||
|
||||
if (!isPasswordlessUser) {
|
||||
history.push("/");
|
||||
} else {
|
||||
setLeft(true);
|
||||
}
|
||||
}, [leave, history]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
} else if (state === GroupCallState.Entered) {
|
||||
return (
|
||||
<InRoomView
|
||||
groupCall={groupCall}
|
||||
client={client}
|
||||
roomName={groupCall.room.name}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
userMediaFeeds={userMediaFeeds}
|
||||
activeSpeaker={activeSpeaker}
|
||||
onLeave={onLeave}
|
||||
toggleScreensharing={toggleScreensharing}
|
||||
isScreensharing={isScreensharing}
|
||||
localScreenshareFeed={localScreenshareFeed}
|
||||
screenshareFeeds={screenshareFeeds}
|
||||
simpleGrid={simpleGrid}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
roomId={roomId}
|
||||
/>
|
||||
);
|
||||
} else if (state === GroupCallState.Entering) {
|
||||
return <EnteringRoomView />;
|
||||
} else if (left) {
|
||||
if (isPasswordlessUser) {
|
||||
return <PasswordlessUserCallEndedScreen client={client} />;
|
||||
} else {
|
||||
return <GuestCallEndedScreen />;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<LobbyView
|
||||
client={client}
|
||||
hasLocalParticipant={hasLocalParticipant}
|
||||
roomName={groupCall.room.name}
|
||||
state={state}
|
||||
onInitLocalCallFeed={initLocalCallFeed}
|
||||
localCallFeed={localCallFeed}
|
||||
onEnter={enter}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
roomId={roomId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function LoadingRoomView() {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Loading room...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
export function EnteringRoomView() {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Entering room...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
function InRoomView({
|
||||
client,
|
||||
groupCall,
|
||||
roomName,
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
userMediaFeeds,
|
||||
activeSpeaker,
|
||||
onLeave,
|
||||
toggleScreensharing,
|
||||
isScreensharing,
|
||||
screenshareFeeds,
|
||||
simpleGrid,
|
||||
setShowInspector,
|
||||
showInspector,
|
||||
roomId,
|
||||
}) {
|
||||
const [layout, setLayout] = useVideoGridLayout();
|
||||
|
||||
const items = useMemo(() => {
|
||||
const participants = [];
|
||||
|
||||
for (const callFeed of userMediaFeeds) {
|
||||
participants.push({
|
||||
id: callFeed.stream.id,
|
||||
usermediaCallFeed: callFeed,
|
||||
isActiveSpeaker:
|
||||
screenshareFeeds.length === 0
|
||||
? callFeed.userId === activeSpeaker
|
||||
: false,
|
||||
});
|
||||
}
|
||||
|
||||
for (const callFeed of screenshareFeeds) {
|
||||
const participant = participants.find(
|
||||
(p) => p.usermediaCallFeed.userId === callFeed.userId
|
||||
);
|
||||
|
||||
if (participant) {
|
||||
participant.screenshareCallFeed = callFeed;
|
||||
}
|
||||
}
|
||||
|
||||
return participants;
|
||||
}, [userMediaFeeds, activeSpeaker, screenshareFeeds]);
|
||||
|
||||
const onFocusTile = useCallback(
|
||||
(tiles, focusedTile) => {
|
||||
if (layout === "freedom") {
|
||||
return tiles.map((tile) => {
|
||||
if (tile === focusedTile) {
|
||||
return { ...tile, presenter: !tile.presenter };
|
||||
}
|
||||
|
||||
return tile;
|
||||
});
|
||||
} else {
|
||||
return tiles;
|
||||
}
|
||||
},
|
||||
[layout, setLayout]
|
||||
);
|
||||
|
||||
const renderAvatar = useCallback(
|
||||
(roomMember, width, height) => {
|
||||
const avatarUrl = roomMember.user?.avatarUrl;
|
||||
const size = Math.round(Math.min(width, height) / 2);
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
key={roomMember.userId}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size,
|
||||
fontSize: Math.round(size / 2),
|
||||
}}
|
||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
|
||||
fallback={roomMember.name.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.room, styles.inRoom)}>
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={roomName} />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||
<UserMenuContainer disableLogout />
|
||||
</RightNav>
|
||||
</Header>
|
||||
{items.length === 0 ? (
|
||||
<div className={styles.centerMessage}>
|
||||
<p>Waiting for other participants...</p>
|
||||
</div>
|
||||
) : simpleGrid ? (
|
||||
<SimpleVideoGrid items={items} />
|
||||
) : (
|
||||
<VideoGrid
|
||||
items={items}
|
||||
layout={layout}
|
||||
getAvatar={renderAvatar}
|
||||
onFocusTile={onFocusTile}
|
||||
disableAnimations={isSafari}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.footer}>
|
||||
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
|
||||
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
|
||||
{canScreenshare && !isSafari && (
|
||||
<ScreenshareButton
|
||||
enabled={isScreensharing}
|
||||
onPress={toggleScreensharing}
|
||||
/>
|
||||
)}
|
||||
<OverflowMenu
|
||||
roomId={roomId}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
client={client}
|
||||
/>
|
||||
<HangupButton onPress={onLeave} />
|
||||
</div>
|
||||
<GroupCallInspector
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
show={showInspector}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GuestCallEndedScreen() {
|
||||
return (
|
||||
<FullScreenView className={styles.callEndedScreen}>
|
||||
<h1>Your call is now ended</h1>
|
||||
<div className={styles.callEndedContent}>
|
||||
<p>Why not finish by creating an account?</p>
|
||||
<p>You'll be able to:</p>
|
||||
<ul>
|
||||
<li>Easily access all your previous call links</li>
|
||||
<li>Set a username and avatar</li>
|
||||
</ul>
|
||||
<LinkButton
|
||||
className={styles.callEndedButton}
|
||||
size="lg"
|
||||
variant="default"
|
||||
to="/register"
|
||||
>
|
||||
Create account
|
||||
</LinkButton>
|
||||
</div>
|
||||
<Link to="/">Not now, return to home screen</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
export function PasswordlessUserCallEndedScreen({ client }) {
|
||||
const { displayName } = useProfile(client);
|
||||
|
||||
return (
|
||||
<FullScreenView className={styles.callEndedScreen}>
|
||||
<h1>{displayName}, your call is now ended</h1>
|
||||
<div className={styles.callEndedContent}>
|
||||
<p>Why not finish by setting up a password to keep your account?</p>
|
||||
<p>
|
||||
You'll be able to keep your name and set an avatar for use on future
|
||||
calls
|
||||
</p>
|
||||
<LinkButton
|
||||
className={styles.callEndedButton}
|
||||
size="lg"
|
||||
variant="default"
|
||||
to="/register"
|
||||
>
|
||||
Create account
|
||||
</LinkButton>
|
||||
</div>
|
||||
<Link to="/">Not now, return to home screen</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
|
@ -1,217 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
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%;
|
||||
}
|
||||
|
||||
.inRoom {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.joinRoom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.joinRoomContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.joinRoomContent h1 {
|
||||
display: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.joinRoomFooter {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.homeLink {
|
||||
margin-top: 50px;
|
||||
color: #0dbd8b;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
min-height: 280px;
|
||||
height: 50vh;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
background-color: var(--bgColor3);
|
||||
margin: 40px 20px 20px 20px;
|
||||
}
|
||||
|
||||
.preview video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background-color: black;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.webcamPermissions {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.previewButtons {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 66px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(23, 25, 28, 0.9);
|
||||
}
|
||||
|
||||
.joinCallButton {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: 222px;
|
||||
height: 40px;
|
||||
bottom: 86px;
|
||||
left: 50%;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
width: 320px !important;
|
||||
}
|
||||
|
||||
.previewButtons > * {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.previewButtons > :last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.centerMessage {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.centerMessage p {
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.roomContainer {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.footer > * {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.footer > :last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.callEndedScreen h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.callEndedScreen h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.callEndedScreen p {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.callEndedScreen ul {
|
||||
padding: 0;
|
||||
margin-bottom: 40px;
|
||||
text-align: initial;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.callEndedButton {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.callEndedContent {
|
||||
text-align: center;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.roomContainer {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 118px;
|
||||
}
|
||||
|
||||
.joinRoomContent h1 {
|
||||
display: block;
|
||||
}
|
||||
}
|
50
src/room/CallEndedView.jsx
Normal file
50
src/room/CallEndedView.jsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import React from "react";
|
||||
import styles from "./CallEndedView.module.css";
|
||||
import { LinkButton } from "../button";
|
||||
import { useProfile } from "../ConferenceCallManagerHooks";
|
||||
import { Subtitle, Body, Link, Headline } from "../typography/Typography";
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||
|
||||
export function CallEndedView({ client }) {
|
||||
const { displayName } = useProfile(client);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav />
|
||||
</Header>
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<Headline className={styles.headline}>
|
||||
{displayName}, your call is now ended
|
||||
</Headline>
|
||||
<div className={styles.callEndedContent}>
|
||||
<Subtitle>
|
||||
Why not finish by setting up a password to keep your account?
|
||||
</Subtitle>
|
||||
<Subtitle>
|
||||
You'll be able to keep your name and set an avatar for use on
|
||||
future calls
|
||||
</Subtitle>
|
||||
<LinkButton
|
||||
className={styles.callEndedButton}
|
||||
size="lg"
|
||||
variant="default"
|
||||
to="/register"
|
||||
>
|
||||
Create account
|
||||
</LinkButton>
|
||||
</div>
|
||||
</main>
|
||||
<Body className={styles.footer}>
|
||||
<Link color="primary" to="/">
|
||||
Not now, return to home screen
|
||||
</Link>
|
||||
</Body>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
73
src/room/CallEndedView.module.css
Normal file
73
src/room/CallEndedView.module.css
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.headline {
|
||||
text-align: center;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.callEndedContent {
|
||||
text-align: center;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.callEndedContent h3 {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.callEndedButton {
|
||||
width: 100%;
|
||||
margin-top: 54px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: calc(100% - 64px);
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.headline {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: calc(100% - 76px);
|
||||
}
|
||||
}
|
166
src/room/InCallView.jsx
Normal file
166
src/room/InCallView.jsx
Normal file
|
@ -0,0 +1,166 @@
|
|||
import React, { useCallback, useMemo } from "react";
|
||||
import styles from "./InCallView.module.css";
|
||||
import {
|
||||
HangupButton,
|
||||
MicButton,
|
||||
VideoButton,
|
||||
ScreenshareButton,
|
||||
} from "../button";
|
||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||
import VideoGrid, {
|
||||
useVideoGridLayout,
|
||||
} from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
|
||||
import SimpleVideoGrid from "matrix-react-sdk/src/components/views/voip/GroupCallView/SimpleVideoGrid";
|
||||
import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
|
||||
import { getAvatarUrl } from "../ConferenceCallManagerHooks";
|
||||
import { GroupCallInspector } from "../GroupCallInspector";
|
||||
import { OverflowMenu } from "../OverflowMenu";
|
||||
import { GridLayoutMenu } from "../GridLayoutMenu";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
// or with getUsermedia and getDisplaymedia being used within the same session.
|
||||
// For now we can disable screensharing in Safari.
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
export function InCallView({
|
||||
client,
|
||||
groupCall,
|
||||
roomName,
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
userMediaFeeds,
|
||||
activeSpeaker,
|
||||
onLeave,
|
||||
toggleScreensharing,
|
||||
isScreensharing,
|
||||
screenshareFeeds,
|
||||
simpleGrid,
|
||||
setShowInspector,
|
||||
showInspector,
|
||||
roomId,
|
||||
}) {
|
||||
const [layout, setLayout] = useVideoGridLayout();
|
||||
|
||||
const items = useMemo(() => {
|
||||
const participants = [];
|
||||
|
||||
for (const callFeed of userMediaFeeds) {
|
||||
participants.push({
|
||||
id: callFeed.stream.id,
|
||||
usermediaCallFeed: callFeed,
|
||||
isActiveSpeaker:
|
||||
screenshareFeeds.length === 0
|
||||
? callFeed.userId === activeSpeaker
|
||||
: false,
|
||||
});
|
||||
}
|
||||
|
||||
for (const callFeed of screenshareFeeds) {
|
||||
const participant = participants.find(
|
||||
(p) => p.usermediaCallFeed.userId === callFeed.userId
|
||||
);
|
||||
|
||||
if (participant) {
|
||||
participant.screenshareCallFeed = callFeed;
|
||||
}
|
||||
}
|
||||
|
||||
return participants;
|
||||
}, [userMediaFeeds, activeSpeaker, screenshareFeeds]);
|
||||
|
||||
const onFocusTile = useCallback(
|
||||
(tiles, focusedTile) => {
|
||||
if (layout === "freedom") {
|
||||
return tiles.map((tile) => {
|
||||
if (tile === focusedTile) {
|
||||
return { ...tile, presenter: !tile.presenter };
|
||||
}
|
||||
|
||||
return tile;
|
||||
});
|
||||
} else {
|
||||
return tiles;
|
||||
}
|
||||
},
|
||||
[layout, setLayout]
|
||||
);
|
||||
|
||||
const renderAvatar = useCallback(
|
||||
(roomMember, width, height) => {
|
||||
const avatarUrl = roomMember.user?.avatarUrl;
|
||||
const size = Math.round(Math.min(width, height) / 2);
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
key={roomMember.userId}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size,
|
||||
fontSize: Math.round(size / 2),
|
||||
}}
|
||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
|
||||
fallback={roomMember.name.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.inRoom}>
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={roomName} />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||
<UserMenuContainer disableLogout />
|
||||
</RightNav>
|
||||
</Header>
|
||||
{items.length === 0 ? (
|
||||
<div className={styles.centerMessage}>
|
||||
<p>Waiting for other participants...</p>
|
||||
</div>
|
||||
) : simpleGrid ? (
|
||||
<SimpleVideoGrid items={items} />
|
||||
) : (
|
||||
<VideoGrid
|
||||
items={items}
|
||||
layout={layout}
|
||||
getAvatar={renderAvatar}
|
||||
onFocusTile={onFocusTile}
|
||||
disableAnimations={isSafari}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.footer}>
|
||||
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
|
||||
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
|
||||
{canScreenshare && !isSafari && (
|
||||
<ScreenshareButton
|
||||
enabled={isScreensharing}
|
||||
onPress={toggleScreensharing}
|
||||
/>
|
||||
)}
|
||||
<OverflowMenu
|
||||
roomId={roomId}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
client={client}
|
||||
/>
|
||||
<HangupButton onPress={onLeave} />
|
||||
</div>
|
||||
<GroupCallInspector
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
show={showInspector}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
68
src/room/InCallView.module.css
Normal file
68
src/room/InCallView.module.css
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.inRoom {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 100%;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.centerMessage {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.centerMessage p {
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.footer > * {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.footer > :last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.footer {
|
||||
height: 118px;
|
||||
}
|
||||
}
|
209
src/room/RoomPage.jsx
Normal file
209
src/room/RoomPage.jsx
Normal file
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useLocation, useParams, useHistory } from "react-router-dom";
|
||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
|
||||
import { useClient, useLoadGroupCall } from "../ConferenceCallManagerHooks";
|
||||
import { ErrorView, LoadingView, FullScreenView } from "../FullScreenView";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { LobbyView } from "./LobbyView";
|
||||
import { InCallView } from "./InCallView";
|
||||
import { CallEndedView } from "./CallEndedView";
|
||||
|
||||
export function RoomPage() {
|
||||
const [registrationError, setRegistrationError] = useState();
|
||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||
useClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !isAuthenticated) {
|
||||
setRegistrationError(new Error("Must be registered"));
|
||||
}
|
||||
}, [loading, isAuthenticated]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
if (registrationError || error) {
|
||||
return <ErrorView error={registrationError || error} />;
|
||||
}
|
||||
|
||||
return <GroupCall client={client} isPasswordlessUser={isPasswordlessUser} />;
|
||||
}
|
||||
|
||||
export function GroupCall({ client, isPasswordlessUser }) {
|
||||
const { roomId: maybeRoomId } = useParams();
|
||||
const { hash, search } = useLocation();
|
||||
const [simpleGrid, viaServers] = useMemo(() => {
|
||||
const params = new URLSearchParams(search);
|
||||
return [params.has("simple"), params.getAll("via")];
|
||||
}, [search]);
|
||||
const roomId = maybeRoomId || hash;
|
||||
const { loading, error, groupCall } = useLoadGroupCall(
|
||||
client,
|
||||
roomId,
|
||||
viaServers
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.groupCall = groupCall;
|
||||
}, [groupCall]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Loading room...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupCallView
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
client={client}
|
||||
roomId={roomId}
|
||||
groupCall={groupCall}
|
||||
simpleGrid={simpleGrid}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCallView({
|
||||
client,
|
||||
isPasswordlessUser,
|
||||
roomId,
|
||||
groupCall,
|
||||
simpleGrid,
|
||||
}) {
|
||||
const [showInspector, setShowInspector] = useState(false);
|
||||
const {
|
||||
state,
|
||||
error,
|
||||
activeSpeaker,
|
||||
userMediaFeeds,
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
localCallFeed,
|
||||
initLocalCallFeed,
|
||||
enter,
|
||||
leave,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
toggleScreensharing,
|
||||
isScreensharing,
|
||||
localScreenshareFeed,
|
||||
screenshareFeeds,
|
||||
hasLocalParticipant,
|
||||
} = useGroupCall(groupCall);
|
||||
|
||||
useEffect(() => {
|
||||
function onHangup(call) {
|
||||
if (call.hangupReason === "ice_failed") {
|
||||
Sentry.captureException(new Error("Call hangup due to ICE failure."));
|
||||
}
|
||||
}
|
||||
|
||||
function onError(error) {
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
|
||||
if (groupCall) {
|
||||
groupCall.on("hangup", onHangup);
|
||||
groupCall.on("error", onError);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (groupCall) {
|
||||
groupCall.removeListener("hangup", onHangup);
|
||||
groupCall.removeListener("error", onError);
|
||||
}
|
||||
};
|
||||
}, [groupCall]);
|
||||
|
||||
const [left, setLeft] = useState(false);
|
||||
const history = useHistory();
|
||||
|
||||
const onLeave = useCallback(() => {
|
||||
leave();
|
||||
|
||||
if (!isPasswordlessUser) {
|
||||
history.push("/");
|
||||
} else {
|
||||
setLeft(true);
|
||||
}
|
||||
}, [leave, history]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
} else if (state === GroupCallState.Entered) {
|
||||
return (
|
||||
<InCallView
|
||||
groupCall={groupCall}
|
||||
client={client}
|
||||
roomName={groupCall.room.name}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
userMediaFeeds={userMediaFeeds}
|
||||
activeSpeaker={activeSpeaker}
|
||||
onLeave={onLeave}
|
||||
toggleScreensharing={toggleScreensharing}
|
||||
isScreensharing={isScreensharing}
|
||||
localScreenshareFeed={localScreenshareFeed}
|
||||
screenshareFeeds={screenshareFeeds}
|
||||
simpleGrid={simpleGrid}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
roomId={roomId}
|
||||
/>
|
||||
);
|
||||
} else if (state === GroupCallState.Entering) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Entering room...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else if (left) {
|
||||
return <CallEndedView client={client} />;
|
||||
} else {
|
||||
return (
|
||||
<LobbyView
|
||||
client={client}
|
||||
hasLocalParticipant={hasLocalParticipant}
|
||||
roomName={groupCall.room.name}
|
||||
state={state}
|
||||
onInitLocalCallFeed={initLocalCallFeed}
|
||||
localCallFeed={localCallFeed}
|
||||
onEnter={enter}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
roomId={roomId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
25
src/room/RoomRedirect.jsx
Normal file
25
src/room/RoomRedirect.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { useLocation, useHistory } from "react-router-dom";
|
||||
import { defaultHomeserverHost } from "../ConferenceCallManagerHooks";
|
||||
import { LoadingView } from "../FullScreenView";
|
||||
|
||||
export function RoomRedirect() {
|
||||
const { pathname } = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
let roomId = pathname;
|
||||
|
||||
if (pathname.startsWith("/")) {
|
||||
roomId = roomId.substr(1, roomId.length);
|
||||
}
|
||||
|
||||
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {
|
||||
roomId = `#${roomId}:${defaultHomeserverHost}`;
|
||||
}
|
||||
|
||||
history.replace(`/room/${roomId}`);
|
||||
}, [pathname, history]);
|
||||
|
||||
return <LoadingView />;
|
||||
}
|
Loading…
Reference in a new issue