Cleanup error/loading screens

This commit is contained in:
Robert Long 2021-12-10 16:42:18 -08:00
parent aa988e9f4a
commit 940706bed0
12 changed files with 77 additions and 530 deletions

View file

@ -31,7 +31,7 @@ import { Room } from "./Room";
import { ClientProvider } from "./ConferenceCallManagerHooks";
import { useFocusVisible } from "@react-aria/interactions";
import styles from "./App.module.css";
import { ErrorModal } from "./ErrorModal";
import { ErrorView, LoadingView } from "./FullScreenView";
const SentryRoute = Sentry.withSentryRouting(Route);
@ -129,8 +129,8 @@ function RoomRedirect() {
}, [history, pathname]);
if (error) {
return <ErrorModal error={error} />;
return <ErrorView error={error} />;
}
return <div>Loading...</div>;
return <LoadingView />;
}

View file

@ -1,245 +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, useRef, useState } from "react";
import { createPortal } from "react-dom";
import ColorHash from "color-hash";
import classNames from "classnames";
import styles from "./DevTools.module.css";
import { Resizable } from "re-resizable";
const colorHash = new ColorHash({ lightness: 0.8 });
function UserId({ userId, ...rest }) {
const shortUserId = userId.split(":")[0];
const color = colorHash.hex(shortUserId);
return (
<span style={{ color }} {...rest}>
{shortUserId}
</span>
);
}
function CallId({ callId, ...rest }) {
const shortId = callId.substr(callId.length - 16);
const color = colorHash.hex(shortId);
return (
<span style={{ color }} {...rest}>
{shortId}
</span>
);
}
function sortEntries(a, b) {
const aInactive = a[1].state === "inactive";
const bInactive = b[1].state === "inactive";
if (aInactive && !bInactive) {
return 1;
} else if (bInactive && !aInactive) {
return -1;
} else {
return a[0] < b[0] ? -1 : 1;
}
}
export function DevTools({ callDebugger }) {
const [debugState, setDebugState] = useState(callDebugger.debugState);
const [selectedEvent, setSelectedEvent] = useState();
const [activeTab, setActiveTab] = useState("users");
useEffect(() => {
function onRoomDebug() {
setDebugState({ ...callDebugger.debugState });
}
callDebugger.on("debug", onRoomDebug);
return () => {
callDebugger.removeListener("debug", onRoomDebug);
};
}, [callDebugger]);
if (!callDebugger.groupCall.entered) {
return <div className={styles.devTools} />;
}
return (
<Resizable
enable={{
top: true,
right: false,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
className={styles.devTools}
>
<div className={styles.toolbar}>
<div
className={classNames(styles.tab, {
[styles.activeTab]: activeTab === "users",
})}
onClick={() => setActiveTab("users")}
>
Users
</div>
<div
className={classNames(styles.tab, {
[styles.activeTab]: activeTab === "calls",
})}
onClick={() => setActiveTab("calls")}
>
Calls
</div>
</div>
<div className={styles.devToolsContainer}>
{activeTab === "users" &&
Array.from(debugState.users.entries())
.sort(sortEntries)
.map(([userId, props]) => (
<EventContainer
key={userId}
showCallId
title={<UserId userId={userId} />}
{...props}
onSelect={setSelectedEvent}
/>
))}
{activeTab === "calls" &&
Array.from(debugState.calls.entries())
.sort(sortEntries)
.map(([callId, props]) => (
<EventContainer
key={callId}
showSender
title={<CallId callId={callId} />}
{...props}
onSelect={setSelectedEvent}
/>
))}
</div>
{selectedEvent && (
<EventViewer
event={selectedEvent}
onClose={() => setSelectedEvent(null)}
/>
)}
</Resizable>
);
}
function EventContainer({ title, state, events, ...rest }) {
const eventsRef = useRef();
const [autoScroll, setAutoScroll] = useState(true);
useEffect(() => {
if (autoScroll) {
const el = eventsRef.current;
el.scrollTop = el.scrollHeight - el.clientHeight;
}
});
const onScroll = useCallback(() => {
const el = eventsRef.current;
if (el.scrollHeight - el.scrollTop === el.clientHeight) {
setAutoScroll(true);
} else {
setAutoScroll(false);
}
}, []);
return (
<div className={styles.user}>
<div className={styles.userId}>
<span>{title}</span>
<span>{`(${state})`}</span>
</div>
<div ref={eventsRef} className={styles.events} onScroll={onScroll}>
{events.map((event, idx) => (
<EventItem key={idx} event={event} {...rest} />
))}
</div>
</div>
);
}
function EventItem({ event, showCallId, showSender, onSelect }) {
const type = event.getType();
const sender = event.getSender();
const { call_id, invitee, reason, eventType, ...rest } = event.getContent();
let eventValue;
if (eventType === "icegatheringstatechange") {
eventValue = rest.iceGatheringState;
} else if (eventType === "iceconnectionstatechange") {
eventValue = rest.iceConnectionState;
} else if (eventType === "signalingstatechange") {
eventValue = rest.signalingState;
}
return (
<div className={styles.event} onClick={() => onSelect(event)}>
{showSender && sender && (
<UserId className={styles.eventDetails} userId={sender} />
)}
<span className={styles.eventType}>
{type.replace("me.robertlong.", "x.")}
</span>
{showCallId && call_id && (
<CallId className={styles.eventDetails} callId={call_id} />
)}
{invitee && <UserId className={styles.eventDetails} userId={invitee} />}
{reason && <span className={styles.eventDetails}>{reason}</span>}
{eventType && <span className={styles.eventDetails}>{eventType}</span>}
{eventValue && <span className={styles.eventDetails}>{eventValue}</span>}
</div>
);
}
function EventViewer({ event, onClose }) {
const type = event.getType();
const sender = event.getSender();
const { call_id, invitee } = event.getContent();
const json = event.toJSON();
return createPortal(
<div className={styles.eventViewer}>
<p>Event Type: {type}</p>
<p>Sender: {sender}</p>
{call_id && (
<p>
Call Id: <CallId callId={call_id} />
</p>
)}
{invitee && (
<p>
Invitee: <UserId userId={invitee} />
</p>
)}
<p>Raw Event:</p>
<pre className={styles.content}>{JSON.stringify(json, undefined, 2)}</pre>
<button onClick={onClose}>Close</button>
</div>,
document.body
);
}

View file

@ -1,132 +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.
*/
.devTools {
display: flex;
flex-direction: column;
border-top: 2px solid #111;
background-color: #111;
}
.toolbar {
display: flex;
background-color: #222;
}
.tab {
vertical-align: middle;
padding: 4px 8px;
background-color: #444;
margin: 0 2px;
cursor: pointer;
}
.activeTab {
background-color: #666;
}
.devToolsContainer {
display: flex;
height: 250px;
gap: 2px;
overflow-x: auto;
}
.user {
display: flex;
flex-direction: column;
flex: 1;
background-color: #555;
min-width: 512px;
}
.userId {
font-size: 14px;
font-weight: bold;
padding: 12px;
}
.userId > * {
margin-right: 4px;
}
.userId > :last-child {
margin-right: 0;
}
.events {
display: flex;
flex-direction: column;
flex: 1;
overflow: auto;
background-color: #222;
}
.event {
display: flex;
font-family: monospace;
padding: 4px 12px;
background-color: #333;
cursor: pointer;
}
.event:nth-child(even) {
background-color: #444;
}
.event:hover {
background-color: #555;
}
.event > * {
margin-right: 4px;
}
.event > :last-child {
margin-right: 0;
}
.eventType, .eventDetails {
font-size: 12px;
}
.eventType {
font-weight: bold;
}
.eventDetails {
font-weight: 200;
word-break: break-all;
}
.eventViewer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 320px;
max-width: 80%;
border-radius: 8px;
border: 1px solid black;
background-color: #222;
padding: 20px;
margin: 20px;
}
.eventViewer .content {
max-height: 200px;
overflow: auto;
}

View file

@ -1,33 +0,0 @@
import React, { useEffect } from "react";
import { Link, useLocation } from "react-router-dom";
import { ErrorMessage } from "./Input";
import styles from "./ErrorModal.module.css";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
export function ErrorModal({ error }) {
const location = useLocation();
useEffect(() => {
console.error(error);
}, [error]);
return (
<>
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav />
</Header>
<div className={styles.container}>
<div className={styles.content}>
<h1>Error</h1>
<ErrorMessage>{error.message}</ErrorMessage>
<Link className={styles.homeLink} to="/">
Return to home screen
</Link>
</div>
</div>
</>
);
}

49
src/FullScreenView.jsx Normal file
View file

@ -0,0 +1,49 @@
import React, { useEffect } from "react";
import { Link, useLocation } from "react-router-dom";
import { ErrorMessage } from "./Input";
import styles from "./FullScreenView.module.css";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
export function FullScreenView({ children }) {
return (
<div className={styles.page}>
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav />
</Header>
<div className={styles.container}>
<div className={styles.content}>{children}</div>
</div>
</div>
);
}
export function ErrorView({ error }) {
const location = useLocation();
useEffect(() => {
console.error(error);
}, [error]);
return (
<FullScreenView>
<h1>Error</h1>
<ErrorMessage>{error.message}</ErrorMessage>
{location.pathname !== "/" && (
<Link className={styles.homeLink} to="/">
Return to home screen
</Link>
)}
</FullScreenView>
);
}
export function LoadingView() {
return (
<FullScreenView>
<h1>Loading...</h1>
</FullScreenView>
);
}

View file

@ -1,3 +1,11 @@
.page {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 100%;
}
.header {
padding: 76px 65px;
}

View file

@ -29,7 +29,7 @@ import { UserMenu } from "./UserMenu";
import { Button } from "./button";
import { CallList } from "./CallList";
import classNames from "classnames";
import { ErrorModal } from "./ErrorModal";
import { ErrorView, LoadingView } from "./FullScreenView";
export function Home() {
const { isAuthenticated, isGuest, loading, error, client } = useClient();
@ -64,9 +64,9 @@ export function Home() {
);
if (loading) {
return <div>Loading...</div>;
} else if (error) {
return <ErrorModal error={error} />;
return <LoadingView />;
} else if (error || createRoomError) {
return <ErrorView error={error || createRoomError} />;
} else if (!isAuthenticated || isGuest) {
return (
<UnregisteredView

View file

@ -1,35 +0,0 @@
import React from "react";
import classNames from "classnames";
import styles from "./Layout.module.css";
export function Content({ children, className, ...rest }) {
return (
<div className={classNames(styles.content, className)} {...rest}>
{children}
</div>
);
}
export function Center({ children, className, ...rest }) {
return (
<div className={classNames(styles.center, className)} {...rest}>
{children}
</div>
);
}
export function Modal({ children, className, ...rest }) {
return (
<div className={classNames(styles.modal, className)} {...rest}>
{children}
</div>
);
}
export function Info({ children, className, ...rest }) {
return (
<p className={classNames(styles.info, className)} {...rest}>
{children}
</p>
);
}

View file

@ -1,32 +0,0 @@
.content {
display: flex;
flex-direction: column;
margin: 0 20px;
flex: 1;
}
.center {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
}
.modal {
color: var(--textColor1);
border-radius: 8px;
padding: 25px 60px;
max-width: 400px;
background-color: var(--bgColor2);
flex: 1;
margin-bottom: 20px;
}
.modal h2 {
margin: 0 0 20px 0;
}
.info {
font-size: 13px;
text-align: center;
}

View file

@ -1,7 +0,0 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M30 60C46.5686 60 60 46.5686 60 30C60 13.4315 46.5686 0 30 0C13.4315 0 0 13.4315 0 30C0 46.5686 13.4315 60 30 60Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.5148 13.9784C24.5148 12.7674 25.4985 11.7858 26.712 11.7858C34.9366 11.7858 41.6039 18.4393 41.6039 26.6468C41.6039 27.8577 40.6202 28.8394 39.4067 28.8394C38.1932 28.8394 37.2095 27.8577 37.2095 26.6468C37.2095 20.8612 32.5096 16.171 26.712 16.171C25.4985 16.171 24.5148 15.1893 24.5148 13.9784Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M46.017 24.4541C47.2304 24.4541 48.2142 25.4358 48.2142 26.6467C48.2142 34.8542 41.5468 41.5077 33.3223 41.5077C32.1088 41.5077 31.1251 40.5261 31.1251 39.3151C31.1251 38.1042 32.1088 37.1225 33.3223 37.1225C39.1199 37.1225 43.8198 32.4323 43.8198 26.6467C43.8198 25.4358 44.8035 24.4541 46.017 24.4541Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.5193 46.0216C35.5193 47.2326 34.5355 48.2142 33.3221 48.2142C25.0975 48.2142 18.4302 41.5607 18.4302 33.3532C18.4302 32.1423 19.4139 31.1606 20.6274 31.1606C21.8408 31.1606 22.8245 32.1423 22.8245 33.3532C22.8245 39.1388 27.5244 43.829 33.3221 43.829C34.5355 43.829 35.5193 44.8107 35.5193 46.0216Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9825 35.5459C12.7691 35.5459 11.7854 34.5642 11.7854 33.3533C11.7854 25.1458 18.4527 18.4923 26.6773 18.4923C27.8907 18.4923 28.8744 19.4739 28.8744 20.6849C28.8744 21.8958 27.8907 22.8775 26.6773 22.8775C20.8796 22.8775 16.1797 27.5677 16.1797 33.3533C16.1797 34.5642 15.196 35.5459 13.9825 35.5459Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -31,7 +31,6 @@ import {
RightNav,
RoomHeaderInfo,
RoomSetupHeaderInfo,
HeaderLogo,
} from "./Header";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import VideoGrid, {
@ -43,7 +42,7 @@ 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 { useClient, useLoadGroupCall } from "./ConferenceCallManagerHooks";
import { ErrorModal } from "./ErrorModal";
import { ErrorView, LoadingView, FullScreenView } from "./FullScreenView";
import { GroupCallInspector } from "./GroupCallInspector";
import * as Sentry from "@sentry/react";
import { OverflowMenu } from "./OverflowMenu";
@ -79,15 +78,11 @@ export function Room() {
}, [loading, isAuthenticated]);
if (loading || registeringGuest) {
return <div>Loading...</div>;
return <LoadingView />;
}
if (registrationError || error) {
return (
<div className={styles.room}>
<ErrorModal error={registrationError || error} />
</div>
);
return <ErrorView error={registrationError || error} />;
}
return <GroupCall client={client} />;
@ -116,16 +111,7 @@ export function GroupCall({ client }) {
}
if (error) {
return (
<div className={styles.room}>
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
</Header>
<ErrorModal error={error} />
</div>
);
return <ErrorView error={error} />;
}
return (
@ -185,16 +171,7 @@ export function GroupCallView({ client, roomId, groupCall, simpleGrid }) {
}, [groupCall]);
if (error) {
return (
<div className={styles.room}>
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
</Header>
<ErrorModal error={error} />
</div>
);
return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered) {
return (
<InRoomView
@ -244,21 +221,17 @@ export function GroupCallView({ client, roomId, groupCall, simpleGrid }) {
export function LoadingRoomView() {
return (
<div className={styles.room}>
<div className={styles.centerMessage}>
<p>Loading room...</p>
</div>
</div>
<FullScreenView>
<h1>Loading room...</h1>
</FullScreenView>
);
}
export function EnteringRoomView() {
return (
<div className={styles.room}>
<div className={styles.centerMessage}>
<p>Entering room...</p>
</div>
</div>
<FullScreenView>
<h1>Entering room...</h1>
</FullScreenView>
);
}

View file

@ -21,6 +21,7 @@ import "./index.css";
import App from "./App";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import { ErrorView } from "./FullScreenView";
if (import.meta.env.VITE_CUSTOM_THEME) {
const style = document.documentElement.style;
@ -56,7 +57,7 @@ Sentry.init({
ReactDOM.render(
<React.StrictMode>
<Sentry.ErrorBoundary fallback={<p>An error has occurred</p>}>
<Sentry.ErrorBoundary fallback={ErrorView}>
<App history={history} />
</Sentry.ErrorBoundary>
</React.StrictMode>,