Compare commits
72 Commits
perf-impro
...
always-aut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34f8023a4e | ||
|
|
38eb5e7c2e | ||
|
|
881a8a1d1f | ||
|
|
269d8d4729 | ||
|
|
d43e85ecc2 | ||
|
|
8eefc0ce50 | ||
|
|
3cc7892198 | ||
|
|
284a4ef794 | ||
|
|
637776dc5e | ||
|
|
c3f4f32107 | ||
|
|
48d011fb69 | ||
|
|
4d6c4bc70e | ||
|
|
2466f25d9f | ||
|
|
1d35c45d5a | ||
|
|
f8fbd50ecd | ||
|
|
1ad4ad5bb0 | ||
|
|
d5407270ad | ||
|
|
2e97c488e2 | ||
|
|
e08a24ade7 | ||
|
|
e5e57387c4 | ||
|
|
7558a61066 | ||
|
|
0c1ca27333 | ||
|
|
c051c96587 | ||
|
|
9d029e9847 | ||
|
|
d007dfda0c | ||
|
|
211e732a2c | ||
|
|
940706bed0 | ||
|
|
aa988e9f4a | ||
|
|
c5654e4398 | ||
|
|
11eb137184 | ||
|
|
77aff4f829 | ||
|
|
95e348f842 | ||
|
|
93be86c42e | ||
|
|
10f1446415 | ||
|
|
c5728b2f66 | ||
|
|
8c287ffcb0 | ||
|
|
12cc844758 | ||
|
|
412d9258e9 | ||
|
|
9174369fb5 | ||
|
|
fc3960ce63 | ||
|
|
20350e66a2 | ||
|
|
9c7006f239 | ||
|
|
4cb144809f | ||
|
|
db1fb064ca | ||
|
|
94f42019df | ||
|
|
4e2d1c5dcd | ||
|
|
f57b520622 | ||
|
|
742fdab56d | ||
|
|
8425a177e2 | ||
|
|
f09454ec09 | ||
|
|
dbaf467a20 | ||
|
|
3cccb86902 | ||
|
|
9a5f40baa0 | ||
|
|
4588cca2b8 | ||
|
|
baa7d4869c | ||
|
|
f0d5977e88 | ||
|
|
e6c93c6b80 | ||
|
|
7610226975 | ||
|
|
e438d85018 | ||
|
|
950e713cb8 | ||
|
|
10a8433ccf | ||
|
|
3df1f89f06 | ||
|
|
99374bbe00 | ||
|
|
26dad81e06 | ||
|
|
442086f31b | ||
|
|
56ba87f25d | ||
|
|
123a68e3b0 | ||
|
|
719ede126a | ||
|
|
225f0487d9 | ||
|
|
4c7c3e2cd3 | ||
|
|
dc57166229 | ||
|
|
27f155e4a1 |
13
.env
@@ -8,4 +8,15 @@
|
||||
# VITE_PUBLIC_SPACE_ROOM_ID=!hjdfshkdskjdsk:myhomeserver.com
|
||||
|
||||
# The Sentry DSN to use for error reporting. Leave undefined to disable.
|
||||
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
||||
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
||||
|
||||
# VITE_CUSTOM_THEME=true
|
||||
# VITE_PRIMARY_COLOR=#0dbd8b
|
||||
# VITE_BG_COLOR_1=#ffffff
|
||||
# VITE_BG_COLOR_2=#f0f1f4
|
||||
# VITE_BG_COLOR_3=#dbdfe4
|
||||
# VITE_BG_COLOR_4=#d1d3d7
|
||||
# VITE_INPUT_BORDER_COLOR=#e7e7e7
|
||||
# VITE_INPUT_BORDER_COLOR_FOCUSED=#238cf5
|
||||
# VITE_TEXT_COLOR_1=#17191c
|
||||
# VITE_TEXT_COLOR_2=#61708b
|
||||
|
||||
16
package.json
@@ -6,6 +6,18 @@
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-aria/button": "^3.3.4",
|
||||
"@react-aria/dialog": "^3.1.4",
|
||||
"@react-aria/focus": "^3.5.0",
|
||||
"@react-aria/menu": "^3.3.0",
|
||||
"@react-aria/overlays": "^3.7.3",
|
||||
"@react-aria/select": "^3.6.0",
|
||||
"@react-aria/tabs": "^3.1.0",
|
||||
"@react-aria/utils": "^3.10.0",
|
||||
"@react-stately/collections": "^3.3.4",
|
||||
"@react-stately/overlays": "^3.1.3",
|
||||
"@react-stately/select": "^3.1.3",
|
||||
"@react-stately/tree": "^3.2.0",
|
||||
"@sentry/react": "^6.13.3",
|
||||
"@sentry/tracing": "^6.13.3",
|
||||
"classnames": "^2.3.1",
|
||||
@@ -18,7 +30,9 @@
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-router-dom": "^5.2.0"
|
||||
"react-router": "6",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-use-clipboard": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.42.1",
|
||||
|
||||
147
src/App.jsx
@@ -14,86 +14,123 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
HashRouter as Router,
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
useLocation,
|
||||
useHistory,
|
||||
} from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { useClient } from "./ConferenceCallManagerHooks";
|
||||
import { OverlayProvider } from "@react-aria/overlays";
|
||||
import { Home } from "./Home";
|
||||
import { Room } from "./Room";
|
||||
import { RegisterPage } from "./RegisterPage";
|
||||
import { LoginPage } from "./LoginPage";
|
||||
import { Center } from "./Layout";
|
||||
import { GuestAuthPage } from "./GuestAuthPage";
|
||||
import { RegisterPage } from "./RegisterPage";
|
||||
import { Room } from "./Room";
|
||||
import { ClientProvider } from "./ConferenceCallManagerHooks";
|
||||
import { useFocusVisible } from "@react-aria/interactions";
|
||||
import styles from "./App.module.css";
|
||||
import { ErrorView, LoadingView } from "./FullScreenView";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
const { protocol, host } = window.location;
|
||||
// Assume homeserver is hosted on same domain (proxied in development by vite)
|
||||
const homeserverUrl = `${protocol}//${host}`;
|
||||
|
||||
export default function App() {
|
||||
const { protocol, host } = window.location;
|
||||
// Assume homeserver is hosted on same domain (proxied in development by vite)
|
||||
const homeserverUrl = `${protocol}//${host}`;
|
||||
const {
|
||||
loading,
|
||||
authenticated,
|
||||
client,
|
||||
login,
|
||||
logout,
|
||||
registerGuest,
|
||||
register,
|
||||
} = useClient(homeserverUrl);
|
||||
const { isFocusVisible } = useFocusVisible();
|
||||
|
||||
useEffect(() => {
|
||||
const classList = document.body.classList;
|
||||
const hasClass = classList.contains(styles.hideFocus);
|
||||
|
||||
if (isFocusVisible && hasClass) {
|
||||
classList.remove(styles.hideFocus);
|
||||
} else if (!isFocusVisible && !hasClass) {
|
||||
classList.add(styles.hideFocus);
|
||||
}
|
||||
|
||||
return () => {
|
||||
classList.remove(styles.hideFocus);
|
||||
};
|
||||
}, [isFocusVisible]);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<>
|
||||
{loading ? (
|
||||
<Center>
|
||||
<p>Loading...</p>
|
||||
</Center>
|
||||
) : (
|
||||
<ClientProvider homeserverUrl={homeserverUrl}>
|
||||
<OverlayProvider>
|
||||
<Switch>
|
||||
<AuthenticatedRoute authenticated={authenticated} exact path="/">
|
||||
<Home client={client} onLogout={logout} />
|
||||
</AuthenticatedRoute>
|
||||
<SentryRoute exact path="/">
|
||||
<Home />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/login">
|
||||
<LoginPage onLogin={login} />
|
||||
<LoginPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/register">
|
||||
<RegisterPage onRegister={register} />
|
||||
<RegisterPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/room/:roomId">
|
||||
{authenticated ? (
|
||||
<Room client={client} />
|
||||
) : (
|
||||
<GuestAuthPage onLoginAsGuest={registerGuest} />
|
||||
)}
|
||||
<SentryRoute path="/room/:roomId?">
|
||||
<Room />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="*">
|
||||
<RoomRedirect />
|
||||
</SentryRoute>
|
||||
</Switch>
|
||||
)}
|
||||
</>
|
||||
</OverlayProvider>
|
||||
</ClientProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthenticatedRoute({ authenticated, children, ...rest }) {
|
||||
const location = useLocation();
|
||||
function RoomRedirect() {
|
||||
const { pathname } = useLocation();
|
||||
const history = useHistory();
|
||||
const [error, setError] = useState();
|
||||
|
||||
return (
|
||||
<SentryRoute {...rest}>
|
||||
{authenticated ? (
|
||||
children
|
||||
) : (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: "/login",
|
||||
state: { from: location },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SentryRoute>
|
||||
);
|
||||
useEffect(() => {
|
||||
async function redirect() {
|
||||
let roomId = pathname;
|
||||
|
||||
if (pathname.startsWith("/")) {
|
||||
roomId = roomId.substr(1, roomId.length);
|
||||
}
|
||||
|
||||
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {
|
||||
let loginHomeserverUrl = homeserverUrl.trim();
|
||||
|
||||
if (!loginHomeserverUrl.includes("://")) {
|
||||
loginHomeserverUrl = "https://" + loginHomeserverUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
const wellKnownUrl = new URL(
|
||||
"/.well-known/matrix/client",
|
||||
window.location
|
||||
);
|
||||
const response = await fetch(wellKnownUrl);
|
||||
const config = await response.json();
|
||||
|
||||
if (config["m.homeserver"]) {
|
||||
loginHomeserverUrl = config["m.homeserver"];
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
const { host } = new URL(loginHomeserverUrl);
|
||||
|
||||
roomId = `#${roomId}:${host}`;
|
||||
}
|
||||
|
||||
history.replace(`/room/${roomId}`);
|
||||
}
|
||||
|
||||
redirect().catch(setError);
|
||||
}, [history, pathname]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
3
src/App.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.hideFocus * {
|
||||
outline: none;
|
||||
}
|
||||
58
src/Avatar.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useMemo } from "react";
|
||||
import classNames from "classnames";
|
||||
import styles from "./Avatar.module.css";
|
||||
|
||||
const backgroundColors = [
|
||||
"#5C56F5",
|
||||
"#03B381",
|
||||
"#368BD6",
|
||||
"#AC3BA8",
|
||||
"#E64F7A",
|
||||
"#FF812D",
|
||||
"#2DC2C5",
|
||||
"#74D12C",
|
||||
];
|
||||
|
||||
function hashStringToArrIndex(str, arrLength) {
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
sum += str.charCodeAt(i);
|
||||
}
|
||||
|
||||
return sum % arrLength;
|
||||
}
|
||||
|
||||
export function Avatar({
|
||||
bgKey,
|
||||
src,
|
||||
fallback,
|
||||
size,
|
||||
className,
|
||||
style,
|
||||
...rest
|
||||
}) {
|
||||
const backgroundColor = useMemo(() => {
|
||||
const index = hashStringToArrIndex(
|
||||
bgKey || fallback || src,
|
||||
backgroundColors.length
|
||||
);
|
||||
return backgroundColors[index];
|
||||
}, [bgKey, src, fallback]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.avatar, styles[size || "md"], className)}
|
||||
style={{ backgroundColor, ...style }}
|
||||
{...rest}
|
||||
>
|
||||
{src ? (
|
||||
<img src={src} />
|
||||
) : typeof fallback === "string" ? (
|
||||
<span>{fallback}</span>
|
||||
) : (
|
||||
fallback
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
src/Avatar.module.css
Normal file
@@ -0,0 +1,46 @@
|
||||
.avatar {
|
||||
position: relative;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar svg * {
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
.sm {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 22px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.md {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 36px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.lg {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 42px;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.xl {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 90px;
|
||||
}
|
||||
54
src/CallList.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { CopyButton } from "./button";
|
||||
import { Facepile } from "./Facepile";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||
import styles from "./CallList.module.css";
|
||||
import { getRoomUrl } from "./ConferenceCallManagerHooks";
|
||||
|
||||
export function CallList({ title, rooms }) {
|
||||
return (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<div className={styles.callList}>
|
||||
{rooms.map(({ roomId, roomName, avatarUrl, participants }) => (
|
||||
<CallTile
|
||||
key={roomId}
|
||||
name={roomName}
|
||||
avatarUrl={avatarUrl}
|
||||
roomId={roomId}
|
||||
participants={participants}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CallTile({ name, avatarUrl, roomId, participants }) {
|
||||
return (
|
||||
<div className={styles.callTile}>
|
||||
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
||||
<Avatar
|
||||
size="md"
|
||||
bgKey={name}
|
||||
src={avatarUrl}
|
||||
fallback={<VideoIcon width={16} height={16} />}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
<div className={styles.callInfo}>
|
||||
<h5>{name}</h5>
|
||||
<p>{getRoomUrl(roomId)}</p>
|
||||
{participants && <Facepile participants={participants} />}
|
||||
</div>
|
||||
<div className={styles.copyButtonSpacer} />
|
||||
</Link>
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
variant="icon"
|
||||
value={getRoomUrl(roomId)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
src/CallList.module.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.callTile {
|
||||
min-width: 240px;
|
||||
height: 94px;
|
||||
padding: 12px;
|
||||
background-color: var(--bgColor2);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.callTileLink {
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.copyButtonSpacer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.callInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 0 16px;
|
||||
color: var(--textColor1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.callInfo > * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.callInfo > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.callInfo h5 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.callInfo p {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.copyButtonSpacer,
|
||||
.copyButton {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.callList {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
@@ -1,359 +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 { useCallback, useEffect, useState } from "react";
|
||||
import matrix from "matrix-js-sdk/src/browser-index";
|
||||
|
||||
function waitForSync(client) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onSync = (state) => {
|
||||
if (state === "PREPARED") {
|
||||
resolve();
|
||||
client.removeListener("sync", onSync);
|
||||
}
|
||||
};
|
||||
client.on("sync", onSync);
|
||||
});
|
||||
}
|
||||
|
||||
async function initClient(clientOptions, guest) {
|
||||
const client = matrix.createClient(clientOptions);
|
||||
|
||||
if (guest) {
|
||||
client.setGuest(true);
|
||||
}
|
||||
|
||||
await client.startClient({
|
||||
// dirty hack to reduce chance of gappy syncs
|
||||
// should be fixed by spotting gaps and backpaginating
|
||||
initialSyncLimit: 50,
|
||||
});
|
||||
|
||||
await waitForSync(client);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export async function fetchGroupCall(client, roomIdOrAlias, timeout = 5000) {
|
||||
const { roomId } = await client.joinRoom(roomIdOrAlias);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId;
|
||||
|
||||
function onGroupCallIncoming(groupCall) {
|
||||
if (groupCall && groupCall.room.roomId === roomId) {
|
||||
clearTimeout(timeoutId);
|
||||
client.removeListener("GroupCall.incoming", onGroupCallIncoming);
|
||||
resolve(groupCall);
|
||||
}
|
||||
}
|
||||
|
||||
const groupCall = client.getGroupCallForRoom(roomId);
|
||||
|
||||
if (groupCall) {
|
||||
resolve(groupCall);
|
||||
}
|
||||
|
||||
client.on("GroupCall.incoming", onGroupCallIncoming);
|
||||
|
||||
if (timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
client.removeListener("GroupCall.incoming", onGroupCallIncoming);
|
||||
reject(new Error("Fetching group call timed out."));
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useClient(homeserverUrl) {
|
||||
const [{ loading, authenticated, client }, setState] = useState({
|
||||
loading: true,
|
||||
authenticated: false,
|
||||
client: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function restore() {
|
||||
try {
|
||||
const authStore = localStorage.getItem("matrix-auth-store");
|
||||
|
||||
if (authStore) {
|
||||
const { user_id, device_id, access_token, guest } =
|
||||
JSON.parse(authStore);
|
||||
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
guest
|
||||
);
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token, guest })
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
restore()
|
||||
.then((client) => {
|
||||
if (client) {
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
} else {
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const login = useCallback(async (homeserver, username, password) => {
|
||||
try {
|
||||
let loginHomeserverUrl = homeserver.trim();
|
||||
|
||||
if (!loginHomeserverUrl.includes("://")) {
|
||||
loginHomeserverUrl = "https://" + loginHomeserverUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
const wellKnownUrl = new URL(
|
||||
"/.well-known/matrix/client",
|
||||
window.location
|
||||
);
|
||||
const response = await fetch(wellKnownUrl);
|
||||
const config = await response.json();
|
||||
|
||||
if (config["m.homeserver"]) {
|
||||
loginHomeserverUrl = config["m.homeserver"];
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
const registrationClient = matrix.createClient(loginHomeserverUrl);
|
||||
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.loginWithPassword(username, password);
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: loginHomeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const registerGuest = useCallback(async () => {
|
||||
try {
|
||||
const registrationClient = matrix.createClient(homeserverUrl);
|
||||
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.registerGuest({});
|
||||
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
await client.setProfileInfo("displayname", {
|
||||
displayname: `Guest ${client.getUserIdLocalpart()}`,
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token, guest: true })
|
||||
);
|
||||
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (username, password) => {
|
||||
try {
|
||||
const registrationClient = matrix.createClient(homeserverUrl);
|
||||
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.register(username, password, null, {
|
||||
type: "m.login.dummy",
|
||||
});
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
}, []);
|
||||
|
||||
return {
|
||||
loading,
|
||||
authenticated,
|
||||
client,
|
||||
login,
|
||||
registerGuest,
|
||||
register,
|
||||
logout,
|
||||
};
|
||||
}
|
||||
|
||||
const tsCache = {};
|
||||
|
||||
function getLastTs(client, r) {
|
||||
if (tsCache[r.roomId]) {
|
||||
return tsCache[r.roomId];
|
||||
}
|
||||
|
||||
if (!r || !r.timeline) {
|
||||
const ts = Number.MAX_SAFE_INTEGER;
|
||||
tsCache[r.roomId] = ts;
|
||||
return ts;
|
||||
}
|
||||
|
||||
const myUserId = client.getUserId();
|
||||
|
||||
if (r.getMyMembership() !== "join") {
|
||||
const membershipEvent = r.currentState.getStateEvents(
|
||||
"m.room.member",
|
||||
myUserId
|
||||
);
|
||||
|
||||
if (membershipEvent && !Array.isArray(membershipEvent)) {
|
||||
const ts = membershipEvent.getTs();
|
||||
tsCache[r.roomId] = ts;
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = r.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = r.timeline[i];
|
||||
const ts = ev.getTs();
|
||||
|
||||
if (ts) {
|
||||
tsCache[r.roomId] = ts;
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
|
||||
const ts = Number.MAX_SAFE_INTEGER;
|
||||
tsCache[r.roomId] = ts;
|
||||
return ts;
|
||||
}
|
||||
|
||||
function sortRooms(client, rooms) {
|
||||
return rooms.sort((a, b) => {
|
||||
return getLastTs(client, b) - getLastTs(client, a);
|
||||
});
|
||||
}
|
||||
|
||||
export function useGroupCallRooms(client) {
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
function updateRooms() {
|
||||
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
||||
const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
|
||||
const sortedRooms = sortRooms(client, rooms);
|
||||
const items = sortedRooms.map((room) => {
|
||||
const groupCall = client.getGroupCallForRoom(room.roomId);
|
||||
|
||||
return {
|
||||
room,
|
||||
groupCall,
|
||||
participants: [...groupCall.participants],
|
||||
};
|
||||
});
|
||||
setRooms(items);
|
||||
}
|
||||
|
||||
updateRooms();
|
||||
|
||||
client.on("GroupCall.incoming", updateRooms);
|
||||
client.on("GroupCall.participants", updateRooms);
|
||||
|
||||
return () => {
|
||||
client.removeListener("GroupCall.incoming", updateRooms);
|
||||
client.removeListener("GroupCall.participants", updateRooms);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
export function usePublicRooms(client, publicSpaceRoomId, maxRooms = 50) {
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (publicSpaceRoomId) {
|
||||
client.getRoomHierarchy(publicSpaceRoomId, maxRooms).then(({ rooms }) => {
|
||||
const filteredRooms = rooms.filter(
|
||||
(room) => room.room_type !== "m.space"
|
||||
);
|
||||
|
||||
setRooms(filteredRooms);
|
||||
});
|
||||
} else {
|
||||
setRooms([]);
|
||||
}
|
||||
}, [publicSpaceRoomId]);
|
||||
|
||||
return rooms;
|
||||
}
|
||||
620
src/ConferenceCallManagerHooks.jsx
Normal file
@@ -0,0 +1,620 @@
|
||||
/*
|
||||
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,
|
||||
useState,
|
||||
createContext,
|
||||
useMemo,
|
||||
useContext,
|
||||
} from "react";
|
||||
import matrix from "matrix-js-sdk/src/browser-index";
|
||||
import {
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
} from "matrix-js-sdk/src/browser-index";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
const ClientContext = createContext();
|
||||
|
||||
function waitForSync(client) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onSync = (state, _old, data) => {
|
||||
if (state === "PREPARED") {
|
||||
resolve();
|
||||
client.removeListener("sync", onSync);
|
||||
} else if (state === "ERROR") {
|
||||
reject(data?.error);
|
||||
client.removeListener("sync", onSync);
|
||||
}
|
||||
};
|
||||
client.on("sync", onSync);
|
||||
});
|
||||
}
|
||||
|
||||
async function initClient(clientOptions, guest) {
|
||||
const client = matrix.createClient(clientOptions);
|
||||
|
||||
if (guest) {
|
||||
client.setGuest(true);
|
||||
}
|
||||
|
||||
await client.startClient({
|
||||
// dirty hack to reduce chance of gappy syncs
|
||||
// should be fixed by spotting gaps and backpaginating
|
||||
initialSyncLimit: 50,
|
||||
});
|
||||
|
||||
await waitForSync(client);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
async function registerGuest(homeserverUrl) {
|
||||
const registrationClient = matrix.createClient(homeserverUrl);
|
||||
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.registerGuest({});
|
||||
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
await client.setDisplayName(`Guest ${client.getUserIdLocalpart()}`);
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token, guest: true })
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export async function fetchGroupCall(
|
||||
client,
|
||||
roomIdOrAlias,
|
||||
viaServers = undefined,
|
||||
timeout = 5000
|
||||
) {
|
||||
const { roomId } = await client.joinRoom(roomIdOrAlias, { viaServers });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId;
|
||||
|
||||
function onGroupCallIncoming(groupCall) {
|
||||
if (groupCall && groupCall.room.roomId === roomId) {
|
||||
clearTimeout(timeoutId);
|
||||
client.removeListener("GroupCall.incoming", onGroupCallIncoming);
|
||||
resolve(groupCall);
|
||||
}
|
||||
}
|
||||
|
||||
const groupCall = client.getGroupCallForRoom(roomId);
|
||||
|
||||
if (groupCall) {
|
||||
resolve(groupCall);
|
||||
}
|
||||
|
||||
client.on("GroupCall.incoming", onGroupCallIncoming);
|
||||
|
||||
if (timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
client.removeListener("GroupCall.incoming", onGroupCallIncoming);
|
||||
reject(new Error("Fetching group call timed out."));
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function ClientProvider({ homeserverUrl, children }) {
|
||||
const history = useHistory();
|
||||
const [{ loading, isPasswordlessUser, isGuest, client, userName }, setState] =
|
||||
useState({
|
||||
loading: true,
|
||||
isPasswordlessUser: false,
|
||||
isGuest: false,
|
||||
client: undefined,
|
||||
userName: null,
|
||||
displayName: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function restore() {
|
||||
try {
|
||||
const authStore = localStorage.getItem("matrix-auth-store");
|
||||
|
||||
if (authStore) {
|
||||
const { user_id, device_id, access_token, guest, passwordlessUser } =
|
||||
JSON.parse(authStore);
|
||||
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
guest
|
||||
);
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({
|
||||
user_id,
|
||||
device_id,
|
||||
access_token,
|
||||
guest,
|
||||
passwordlessUser,
|
||||
})
|
||||
);
|
||||
|
||||
return { client, guest, passwordlessUser };
|
||||
}
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await registerGuest(homeserverUrl);
|
||||
return { client, guest: true, passwordlessUser: false };
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
restore()
|
||||
.then(({ client, guest, passwordlessUser }) => {
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isPasswordlessUser: !!passwordlessUser,
|
||||
isGuest: guest,
|
||||
userName: client?.getUserIdLocalpart(),
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setState({
|
||||
error,
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isPasswordlessUser: false,
|
||||
isGuest: false,
|
||||
userName: null,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const login = useCallback(async (homeserver, username, password) => {
|
||||
let loginHomeserverUrl = homeserver.trim();
|
||||
|
||||
if (!loginHomeserverUrl.includes("://")) {
|
||||
loginHomeserverUrl = "https://" + loginHomeserverUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
const wellKnownUrl = new URL(
|
||||
"/.well-known/matrix/client",
|
||||
window.location
|
||||
);
|
||||
const response = await fetch(wellKnownUrl);
|
||||
const config = await response.json();
|
||||
|
||||
if (config["m.homeserver"]) {
|
||||
loginHomeserverUrl = config["m.homeserver"];
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
const registrationClient = matrix.createClient(loginHomeserverUrl);
|
||||
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.loginWithPassword(username, password);
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: loginHomeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isPasswordlessUser: false,
|
||||
isGuest: false,
|
||||
userName: client.getUserIdLocalpart(),
|
||||
});
|
||||
|
||||
return client;
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (username, password, passwordlessUser) => {
|
||||
const registrationClient = matrix.createClient(homeserverUrl);
|
||||
|
||||
const { user_id, device_id, access_token } =
|
||||
await registrationClient.register(username, password, null, {
|
||||
type: "m.login.dummy",
|
||||
});
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: homeserverUrl,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({ user_id, device_id, access_token, passwordlessUser })
|
||||
);
|
||||
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isGuest: false,
|
||||
isPasswordlessUser: passwordlessUser,
|
||||
userName: client.getUserIdLocalpart(),
|
||||
});
|
||||
|
||||
return client;
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
window.location = "/";
|
||||
}, [history]);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
loading,
|
||||
isPasswordlessUser,
|
||||
isGuest,
|
||||
client,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
userName,
|
||||
}),
|
||||
[
|
||||
loading,
|
||||
isPasswordlessUser,
|
||||
isGuest,
|
||||
client,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
userName,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useClient() {
|
||||
return useContext(ClientContext);
|
||||
}
|
||||
|
||||
function roomAliasFromRoomName(roomName) {
|
||||
return roomName
|
||||
.trim()
|
||||
.replace(/\s/g, "-")
|
||||
.replace(/[^\w-]/g, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export async function createRoom(client, name) {
|
||||
const { room_id, room_alias } = await client.createRoom({
|
||||
visibility: "private",
|
||||
preset: "public_chat",
|
||||
name,
|
||||
room_alias_name: roomAliasFromRoomName(name),
|
||||
power_level_content_override: {
|
||||
invite: 100,
|
||||
kick: 100,
|
||||
ban: 100,
|
||||
redact: 50,
|
||||
state_default: 0,
|
||||
events_default: 0,
|
||||
users_default: 0,
|
||||
events: {
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.tombstone": 100,
|
||||
"m.room.encryption": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.message": 0,
|
||||
"m.room.encrypted": 50,
|
||||
"m.sticker": 50,
|
||||
"org.matrix.msc3401.call.member": 0,
|
||||
},
|
||||
users: {
|
||||
[client.getUserId()]: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await client.setGuestAccess(room_id, {
|
||||
allowJoin: true,
|
||||
allowRead: true,
|
||||
});
|
||||
|
||||
await client.createGroupCall(
|
||||
room_id,
|
||||
GroupCallType.Video,
|
||||
GroupCallIntent.Prompt
|
||||
);
|
||||
|
||||
return room_alias || room_id;
|
||||
}
|
||||
|
||||
export function useCreateRoom() {
|
||||
const { register, client } = useClient();
|
||||
const [creatingRoom, setCreatingRoom] = useState(false);
|
||||
const [createRoomError, setCreateRoomError] = useState();
|
||||
|
||||
const onCreateRoom = useCallback(
|
||||
(roomName, userName) => {
|
||||
async function onCreateRoom(roomName, userName) {
|
||||
let _client = client;
|
||||
|
||||
if (!_client) {
|
||||
_client = await register(userName, randomString(16), true);
|
||||
}
|
||||
|
||||
return await createRoom(_client, roomName);
|
||||
}
|
||||
|
||||
setCreateRoomError(undefined);
|
||||
setCreatingRoom(true);
|
||||
|
||||
return onCreateRoom(roomName, userName).catch((error) => {
|
||||
setCreateRoomError(error);
|
||||
setCreatingRoom(false);
|
||||
});
|
||||
},
|
||||
[register, client]
|
||||
);
|
||||
|
||||
return {
|
||||
creatingRoom,
|
||||
createRoomError,
|
||||
createRoom: onCreateRoom,
|
||||
};
|
||||
}
|
||||
|
||||
export function useLoadGroupCall(client, roomId, viaServers) {
|
||||
const [state, setState] = useState({
|
||||
loading: true,
|
||||
error: undefined,
|
||||
groupCall: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setState({ loading: true });
|
||||
fetchGroupCall(client, roomId, viaServers, 30000)
|
||||
.then((groupCall) => setState({ loading: false, groupCall }))
|
||||
.catch((error) => setState({ loading: false, error }));
|
||||
}, [client, roomId]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
const tsCache = {};
|
||||
|
||||
function getLastTs(client, r) {
|
||||
if (tsCache[r.roomId]) {
|
||||
return tsCache[r.roomId];
|
||||
}
|
||||
|
||||
if (!r || !r.timeline) {
|
||||
const ts = Number.MAX_SAFE_INTEGER;
|
||||
tsCache[r.roomId] = ts;
|
||||
return ts;
|
||||
}
|
||||
|
||||
const myUserId = client.getUserId();
|
||||
|
||||
if (r.getMyMembership() !== "join") {
|
||||
const membershipEvent = r.currentState.getStateEvents(
|
||||
"m.room.member",
|
||||
myUserId
|
||||
);
|
||||
|
||||
if (membershipEvent && !Array.isArray(membershipEvent)) {
|
||||
const ts = membershipEvent.getTs();
|
||||
tsCache[r.roomId] = ts;
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = r.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = r.timeline[i];
|
||||
const ts = ev.getTs();
|
||||
|
||||
if (ts) {
|
||||
tsCache[r.roomId] = ts;
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
|
||||
const ts = Number.MAX_SAFE_INTEGER;
|
||||
tsCache[r.roomId] = ts;
|
||||
return ts;
|
||||
}
|
||||
|
||||
function sortRooms(client, rooms) {
|
||||
return rooms.sort((a, b) => {
|
||||
return getLastTs(client, b) - getLastTs(client, a);
|
||||
});
|
||||
}
|
||||
|
||||
export function useGroupCallRooms(client) {
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
function updateRooms() {
|
||||
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
||||
const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
|
||||
const sortedRooms = sortRooms(client, rooms);
|
||||
const items = sortedRooms.map((room) => {
|
||||
const groupCall = client.getGroupCallForRoom(room.roomId);
|
||||
|
||||
return {
|
||||
roomId: room.getCanonicalAlias() || room.roomId,
|
||||
roomName: room.name,
|
||||
avatarUrl: null,
|
||||
room,
|
||||
groupCall,
|
||||
participants: [...groupCall.participants],
|
||||
};
|
||||
});
|
||||
setRooms(items);
|
||||
}
|
||||
|
||||
updateRooms();
|
||||
|
||||
client.on("GroupCall.incoming", updateRooms);
|
||||
client.on("GroupCall.participants", updateRooms);
|
||||
|
||||
return () => {
|
||||
client.removeListener("GroupCall.incoming", updateRooms);
|
||||
client.removeListener("GroupCall.participants", updateRooms);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
export function usePublicRooms(client, publicSpaceRoomId, maxRooms = 50) {
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (publicSpaceRoomId) {
|
||||
client.getRoomHierarchy(publicSpaceRoomId, maxRooms).then(({ rooms }) => {
|
||||
const filteredRooms = rooms
|
||||
.filter((room) => room.room_type !== "m.space")
|
||||
.map((room) => ({
|
||||
roomId: room.room_alias || room.room_id,
|
||||
roomName: room.name,
|
||||
avatarUrl: null,
|
||||
room,
|
||||
participants: [],
|
||||
}));
|
||||
|
||||
setRooms(filteredRooms);
|
||||
});
|
||||
} else {
|
||||
setRooms([]);
|
||||
}
|
||||
}, [publicSpaceRoomId]);
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
export function getRoomUrl(roomId) {
|
||||
if (roomId.startsWith("#")) {
|
||||
const [localPart, host] = roomId.replace("#", "").split(":");
|
||||
|
||||
if (host !== window.location.host) {
|
||||
return `${window.location.host}/room/${roomId}`;
|
||||
} else {
|
||||
return `${window.location.host}/${localPart}`;
|
||||
}
|
||||
} else {
|
||||
return `${window.location.host}/room/${roomId}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function useDisplayName(client) {
|
||||
const [{ loading, displayName, error, success }, setState] = useState(() => ({
|
||||
success: false,
|
||||
loading: false,
|
||||
displayName: client?.getUser(client.getUserId())?.displayName,
|
||||
error: null,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const onChangeDisplayName = (_event, { displayName }) => {
|
||||
setState({ success: false, loading: false, displayName, error: null });
|
||||
};
|
||||
|
||||
let user;
|
||||
|
||||
if (client) {
|
||||
const userId = client.getUserId();
|
||||
user = client.getUser(userId);
|
||||
user.on("User.displayName", onChangeDisplayName);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (user) {
|
||||
user.removeListener("User.displayName", onChangeDisplayName);
|
||||
}
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
const setDisplayName = useCallback(
|
||||
(displayName) => {
|
||||
if (client) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: true,
|
||||
error: null,
|
||||
success: false,
|
||||
}));
|
||||
|
||||
client
|
||||
.setDisplayName(displayName)
|
||||
.then(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
displayName,
|
||||
loading: false,
|
||||
success: true,
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error,
|
||||
success: false,
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
console.error("Client not initialized before calling setDisplayName");
|
||||
}
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return { loading, error, displayName, setDisplayName, success };
|
||||
}
|
||||
245
src/DevTools.jsx
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Center, Content, Modal } from "./Layout";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ErrorMessage } from "./Input";
|
||||
import styles from "./ErrorModal.module.css";
|
||||
|
||||
export function ErrorModal({ error }) {
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<Center>
|
||||
<Modal>
|
||||
<h2>Error</h2>
|
||||
<div className={styles.errorModalContent}>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
<p>
|
||||
<Link to="/login">Login</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Link to="/register">Register</Link>
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</Center>
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.errorModalContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.errorModalContent p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -1,24 +1,33 @@
|
||||
import React from "react";
|
||||
import styles from "./Facepile.module.css";
|
||||
import ColorHash from "color-hash";
|
||||
import classNames from "classnames";
|
||||
import { Avatar } from "./Avatar";
|
||||
|
||||
const colorHash = new ColorHash({ lightness: 0.3 });
|
||||
|
||||
export function Facepile({ participants }) {
|
||||
export function Facepile({ className, participants, ...rest }) {
|
||||
return (
|
||||
<div
|
||||
className={styles.facepile}
|
||||
className={classNames(styles.facepile, className)}
|
||||
title={participants.map((member) => member.name).join(", ")}
|
||||
{...rest}
|
||||
>
|
||||
{participants.map((member) => (
|
||||
<div
|
||||
{participants.slice(0, 3).map((member, i) => (
|
||||
<Avatar
|
||||
key={member.userId}
|
||||
size="sm"
|
||||
fallback={member.name.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
style={{ backgroundColor: colorHash.hex(member.name) }}
|
||||
>
|
||||
<span>{member.name.slice(0, 1).toUpperCase()}</span>
|
||||
</div>
|
||||
style={{ left: i * 22 }}
|
||||
/>
|
||||
))}
|
||||
{participants.length > 3 && (
|
||||
<Avatar
|
||||
key="additional"
|
||||
size="sm"
|
||||
fallback={`+${participants.length - 3}`}
|
||||
className={styles.avatar}
|
||||
style={{ left: 3 * 22 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
.facepile {
|
||||
display: flex;
|
||||
margin: 0 16px;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.facepile .avatar {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.facepile .avatar > * {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.facepile .avatar span {
|
||||
font-size: 14px;
|
||||
width: 20px;
|
||||
line-height: 20px;
|
||||
top: 0;
|
||||
border: 1px solid var(--bgColor2);
|
||||
}
|
||||
|
||||
68
src/FullScreenView.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import styles from "./FullScreenView.module.css";
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||
import classNames from "classnames";
|
||||
import { LinkButton, Button } from "./button";
|
||||
|
||||
export function FullScreenView({ className, children }) {
|
||||
return (
|
||||
<div className={classNames(styles.page, className)}>
|
||||
<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]);
|
||||
|
||||
const onReload = useCallback(() => {
|
||||
window.location = "/";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Error</h1>
|
||||
<p>{error.message}</p>
|
||||
{location.pathname === "/" ? (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
onPress={onReload}
|
||||
>
|
||||
Return to home screen
|
||||
</Button>
|
||||
) : (
|
||||
<LinkButton
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
to="/"
|
||||
>
|
||||
Return to home screen
|
||||
</LinkButton>
|
||||
)}
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingView() {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Loading...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
41
src/FullScreenView.module.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 76px 65px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 660px;
|
||||
padding: 0 65px;
|
||||
}
|
||||
|
||||
.content > * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.content > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.homeLink {
|
||||
width: 291px;
|
||||
}
|
||||
36
src/GridLayoutMenu.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import { ButtonTooltip, Button } from "./button";
|
||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
||||
import { ReactComponent as SpotlightIcon } from "./icons/Spotlight.svg";
|
||||
import { ReactComponent as FreedomIcon } from "./icons/Freedom.svg";
|
||||
import { ReactComponent as CheckIcon } from "./icons/Check.svg";
|
||||
import styles from "./GridLayoutMenu.module.css";
|
||||
import { Menu } from "./Menu";
|
||||
import { Item } from "@react-stately/collections";
|
||||
|
||||
export function GridLayoutMenu({ layout, setLayout }) {
|
||||
return (
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
<Button variant="icon">
|
||||
<ButtonTooltip>Layout Type</ButtonTooltip>
|
||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
||||
<Item key="freedom" textValue="Freedom">
|
||||
<FreedomIcon />
|
||||
<span>Freedom</span>
|
||||
{layout === "freedom" && <CheckIcon className={styles.checkIcon} />}
|
||||
</Item>
|
||||
<Item key="spotlight" textValue="Spotlight">
|
||||
<SpotlightIcon />
|
||||
<span>Spotlight</span>
|
||||
{layout === "spotlight" && (
|
||||
<CheckIcon className={styles.checkIcon} />
|
||||
)}
|
||||
</Item>
|
||||
</Menu>
|
||||
)}
|
||||
</PopoverMenuTrigger>
|
||||
);
|
||||
}
|
||||
8
src/GridLayoutMenu.module.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.checkIcon {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.checkIcon * {
|
||||
stroke: var(--textColor1);
|
||||
}
|
||||
@@ -1,47 +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, { useState, useEffect } from "react";
|
||||
import styles from "./GuestAuthPage.module.css";
|
||||
import { useLocation, useHistory } from "react-router-dom";
|
||||
import { Header, LeftNav } from "./Header";
|
||||
import { Center, Content, Modal } from "./Layout";
|
||||
import { ErrorModal } from "./ErrorModal";
|
||||
|
||||
export function GuestAuthPage({ onLoginAsGuest }) {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [error, setError] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
onLoginAsGuest("Guest " + Math.round(Math.random() * 999)).catch(setError);
|
||||
}, [onLoginAsGuest, location, history]);
|
||||
|
||||
return (
|
||||
<div className={styles.guestAuthPage}>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
</Header>
|
||||
<Content>
|
||||
<Center>
|
||||
<Modal>
|
||||
{error ? <ErrorModal error={error} /> : <div>Loading...</div>}
|
||||
</Modal>
|
||||
</Center>
|
||||
</Content>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
.guestAuthPage {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styles from "./Header.module.css";
|
||||
import { ReactComponent as Logo } from "./Logo.svg";
|
||||
import { ReactComponent as Logo } from "./icons/Logo.svg";
|
||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
||||
import { useButton } from "@react-aria/button";
|
||||
|
||||
export function Header({ children, className, ...rest }) {
|
||||
return (
|
||||
@@ -14,18 +17,10 @@ export function Header({ children, className, ...rest }) {
|
||||
|
||||
export function LeftNav({ children, className, ...rest }) {
|
||||
return (
|
||||
<div className={classNames(styles.leftNav, className)} {...rest}>
|
||||
<Link className={styles.logo} to="/">
|
||||
<Logo width={32} height={32} />
|
||||
</Link>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CenterNav({ children, className, ...rest }) {
|
||||
return (
|
||||
<div className={classNames(styles.centerNav, className)} {...rest}>
|
||||
<div
|
||||
className={classNames(styles.nav, styles.leftNav, className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -33,23 +28,41 @@ export function CenterNav({ children, className, ...rest }) {
|
||||
|
||||
export function RightNav({ children, className, ...rest }) {
|
||||
return (
|
||||
<div className={classNames(styles.rightNav, className)} {...rest}>
|
||||
<div
|
||||
className={classNames(styles.nav, styles.rightNav, className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserNav({ signedIn, userName, onLogout }) {
|
||||
if (!signedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function HeaderLogo() {
|
||||
return (
|
||||
<RightNav>
|
||||
<span className={styles.userName}>{userName}</span>
|
||||
<button className={styles.signOutButton} type="button" onClick={onLogout}>
|
||||
Sign Out
|
||||
</button>
|
||||
</RightNav>
|
||||
<Link className={styles.logo} to="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoomHeaderInfo({ roomName }) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.roomAvatar}>
|
||||
<VideoIcon width={16} height={16} />
|
||||
</div>
|
||||
<h3>{roomName}</h3>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoomSetupHeaderInfo({ roomName, ...rest }) {
|
||||
const ref = useRef();
|
||||
const { buttonProps } = useButton(rest, ref);
|
||||
return (
|
||||
<button className={styles.backButton} ref={ref} {...buttonProps}>
|
||||
<ArrowLeftIcon width={16} height={16} />
|
||||
<RoomHeaderInfo roomName={roomName} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
.header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.leftNav {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
.nav {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
padding: 0 20px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@@ -19,12 +22,57 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.leftNav > * {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.rightNav {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
max-width: 40%;
|
||||
white-space: nowrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.rightNav > * {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.nav > :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.roomAvatar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 36px;
|
||||
background-color: #5c56f5;
|
||||
}
|
||||
|
||||
.roomAvatar > * {
|
||||
fill: white;
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: flex;
|
||||
color: var(--textColor1);
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backButton h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.backButton > * {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.backButton > :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.userName {
|
||||
@@ -44,8 +92,8 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media(min-width: 800px) {
|
||||
.header {
|
||||
@media (min-width: 800px) {
|
||||
.nav {
|
||||
height: 98px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
382
src/Home.jsx
@@ -14,108 +14,137 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { useHistory, Link } from "react-router-dom";
|
||||
import {
|
||||
useClient,
|
||||
useGroupCallRooms,
|
||||
usePublicRooms,
|
||||
useCreateRoom,
|
||||
} from "./ConferenceCallManagerHooks";
|
||||
import { Header, LeftNav, UserNav } from "./Header";
|
||||
import ColorHash from "color-hash";
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||
import styles from "./Home.module.css";
|
||||
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
|
||||
import { Center, Content, Modal } from "./Layout";
|
||||
import {
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
} from "matrix-js-sdk/src/browser-index";
|
||||
import { Facepile } from "./Facepile";
|
||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
import { Button } from "./button";
|
||||
import { CallList } from "./CallList";
|
||||
import classNames from "classnames";
|
||||
import { ErrorView, LoadingView } from "./FullScreenView";
|
||||
|
||||
const colorHash = new ColorHash({ lightness: 0.3 });
|
||||
export function Home() {
|
||||
const { isGuest, isPasswordlessUser, loading, error, client } = useClient();
|
||||
|
||||
export function Home({ client, onLogout }) {
|
||||
const history = useHistory();
|
||||
const roomNameRef = useRef();
|
||||
const guestAccessRef = useRef();
|
||||
const [createRoomError, setCreateRoomError] = useState();
|
||||
const rooms = useGroupCallRooms(client);
|
||||
const publicRooms = usePublicRooms(
|
||||
client,
|
||||
import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
|
||||
);
|
||||
const { createRoomError, creatingRoom, createRoom } = useCreateRoom();
|
||||
|
||||
const onCreateRoom = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
setCreateRoomError(undefined);
|
||||
const data = new FormData(e.target);
|
||||
const roomName = data.get("roomName");
|
||||
const userName = data.get("userName");
|
||||
|
||||
async function createRoom(name, guestAccess) {
|
||||
const { room_id } = await client.createRoom({
|
||||
visibility: "private",
|
||||
preset: "public_chat",
|
||||
name,
|
||||
power_level_content_override: {
|
||||
invite: 100,
|
||||
kick: 100,
|
||||
ban: 100,
|
||||
redact: 50,
|
||||
state_default: 0,
|
||||
events_default: 0,
|
||||
users_default: 0,
|
||||
events: {
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.tombstone": 100,
|
||||
"m.room.encryption": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.message": 0,
|
||||
"m.room.encrypted": 50,
|
||||
"m.sticker": 50,
|
||||
"org.matrix.msc3401.call.member": 0,
|
||||
},
|
||||
users: {
|
||||
[client.getUserId()]: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (guestAccess) {
|
||||
await client.setGuestAccess(room_id, {
|
||||
allowJoin: true,
|
||||
allowRead: true,
|
||||
});
|
||||
createRoom(roomName, userName).then((roomIdOrAlias) => {
|
||||
if (roomIdOrAlias) {
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
}
|
||||
|
||||
await client.createGroupCall(
|
||||
room_id,
|
||||
GroupCallType.Video,
|
||||
GroupCallIntent.Prompt
|
||||
);
|
||||
|
||||
history.push(`/room/${room_id}`);
|
||||
}
|
||||
|
||||
createRoom(
|
||||
roomNameRef.current.value,
|
||||
guestAccessRef.current.checked
|
||||
).catch(setCreateRoomError);
|
||||
});
|
||||
},
|
||||
[client]
|
||||
[history]
|
||||
);
|
||||
|
||||
const onJoinRoom = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const roomId = data.get("roomId");
|
||||
history.push(`/${roomId}`);
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
} else if (error || createRoomError) {
|
||||
return <ErrorView error={error || createRoomError} />;
|
||||
} else if (isGuest) {
|
||||
return (
|
||||
<UnregisteredView
|
||||
onCreateRoom={onCreateRoom}
|
||||
createRoomError={createRoomError}
|
||||
creatingRoom={creatingRoom}
|
||||
onJoinRoom={onJoinRoom}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RegisteredView
|
||||
client={client}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
isGuest={isGuest}
|
||||
onCreateRoom={onCreateRoom}
|
||||
createRoomError={createRoomError}
|
||||
creatingRoom={creatingRoom}
|
||||
onJoinRoom={onJoinRoom}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function UnregisteredView({
|
||||
onCreateRoom,
|
||||
createRoomError,
|
||||
creatingRoom,
|
||||
onJoinRoom,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
<UserNav signedIn userName={client.getUserId()} onLogout={onLogout} />
|
||||
<div className={classNames(styles.home, styles.fullWidth)}>
|
||||
<Header className={styles.header}>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<Content>
|
||||
<Center>
|
||||
<Modal>
|
||||
<section>
|
||||
<div className={styles.splitContainer}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.centered}>
|
||||
<form onSubmit={onJoinRoom}>
|
||||
<h1>Join a call</h1>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="roomId"
|
||||
name="roomId"
|
||||
label="Call ID"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Call ID"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button className={styles.button} type="submit">
|
||||
Join call
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
<hr />
|
||||
<form onSubmit={onCreateRoom}>
|
||||
<h2>Create New Room</h2>
|
||||
<FieldRow>
|
||||
<h1>Create a call</h1>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="userName"
|
||||
name="userName"
|
||||
label="Username"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Username"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="roomName"
|
||||
name="roomName"
|
||||
@@ -124,74 +153,145 @@ export function Home({ client, onLogout }) {
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Room Name"
|
||||
ref={roomNameRef}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="guestAccess"
|
||||
name="guestAccess"
|
||||
label="Allow Guest Access"
|
||||
type="checkbox"
|
||||
ref={guestAccessRef}
|
||||
/>
|
||||
</FieldRow>
|
||||
{createRoomError && (
|
||||
<FieldRow>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<ErrorMessage>{createRoomError.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow rightAlign>
|
||||
<Button type="submit">Create Room</Button>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
type="submit"
|
||||
disabled={creatingRoom}
|
||||
>
|
||||
{creatingRoom ? "Creating call..." : "Create call"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
</section>
|
||||
{publicRooms.length > 0 && (
|
||||
<section>
|
||||
<h3>Public Rooms</h3>
|
||||
<div className={styles.roomList}>
|
||||
{publicRooms.map((room) => (
|
||||
<Link
|
||||
className={styles.roomListItem}
|
||||
key={room.room_id}
|
||||
to={`/room/${room.room_id}`}
|
||||
>
|
||||
<div
|
||||
className={styles.roomAvatar}
|
||||
style={{ backgroundColor: colorHash.hex(room.name) }}
|
||||
>
|
||||
<span>{room.name.slice(0, 1)}</span>
|
||||
</div>
|
||||
<div className={styles.roomName}>{room.name}</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<section>
|
||||
<h3>Recent Rooms</h3>
|
||||
<div className={styles.roomList}>
|
||||
{rooms.map(({ room, participants }) => (
|
||||
<Link
|
||||
className={styles.roomListItem}
|
||||
key={room.roomId}
|
||||
to={`/room/${room.roomId}`}
|
||||
>
|
||||
<div
|
||||
className={styles.roomAvatar}
|
||||
style={{ backgroundColor: colorHash.hex(room.name) }}
|
||||
>
|
||||
<span>{room.name.slice(0, 1)}</span>
|
||||
</div>
|
||||
<div className={styles.roomName}>{room.name}</div>
|
||||
<Facepile participants={participants} />
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div className={styles.authLinks}>
|
||||
<p>
|
||||
Not registered yet?{" "}
|
||||
<Link to="/register">Create an account</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Modal>
|
||||
</Center>
|
||||
</Content>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RegisteredView({
|
||||
client,
|
||||
isPasswordlessUser,
|
||||
isGuest,
|
||||
onCreateRoom,
|
||||
createRoomError,
|
||||
creatingRoom,
|
||||
onJoinRoom,
|
||||
}) {
|
||||
const publicRooms = usePublicRooms(
|
||||
client,
|
||||
import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
|
||||
);
|
||||
const recentRooms = useGroupCallRooms(client);
|
||||
|
||||
const hideCallList = publicRooms.length === 0 && recentRooms.length === 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.home, {
|
||||
[styles.fullWidth]: hideCallList,
|
||||
})}
|
||||
>
|
||||
<Header className={styles.header}>
|
||||
<LeftNav className={styles.leftNav}>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.splitContainer}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.centered}>
|
||||
<form onSubmit={onJoinRoom}>
|
||||
<h1>Join a call</h1>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="roomId"
|
||||
name="roomId"
|
||||
label="Call ID"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Call ID"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button className={styles.button} type="submit">
|
||||
Join call
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
<hr />
|
||||
<form onSubmit={onCreateRoom}>
|
||||
<h1>Create a call</h1>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<InputField
|
||||
id="roomName"
|
||||
name="roomName"
|
||||
label="Room Name"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Room Name"
|
||||
/>
|
||||
</FieldRow>
|
||||
{createRoomError && (
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<ErrorMessage>{createRoomError.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
type="submit"
|
||||
disabled={creatingRoom}
|
||||
>
|
||||
{creatingRoom ? "Creating call..." : "Create call"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
{(isPasswordlessUser || isGuest) && (
|
||||
<div className={styles.authLinks}>
|
||||
<p>
|
||||
Not registered yet?{" "}
|
||||
<Link to="/register">Create an account</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!hideCallList && (
|
||||
<div className={styles.right}>
|
||||
<div className={styles.content}>
|
||||
{publicRooms.length > 0 && (
|
||||
<CallList title="Public Calls" rooms={publicRooms} />
|
||||
)}
|
||||
{recentRooms.length > 0 && (
|
||||
<CallList title="Recent Calls" rooms={recentRooms} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,47 +1,140 @@
|
||||
.roomList {
|
||||
|
||||
.home {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.roomListItem {
|
||||
margin-bottom: 4px;
|
||||
padding: 4px;
|
||||
.splitContainer {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
background-color: var(--bgColor1);
|
||||
}
|
||||
|
||||
.fullWidth .header {
|
||||
background-color: var(--bgColor1);
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-width: 512px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.left .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 40px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.roomListItem:hover {
|
||||
background-color: rgba(141, 151, 165, 0.2);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
.left .content form > * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.roomAvatar {
|
||||
.left .content form > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.left .content hr:after {
|
||||
background-color: var(--bgColor1);
|
||||
content: "OR";
|
||||
padding: 0 12px;
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
top: -12px;
|
||||
}
|
||||
|
||||
.roomAvatar > * {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
font-weight: 400;
|
||||
.left .content form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 92px;
|
||||
}
|
||||
|
||||
.roomAvatar span {
|
||||
font-size: 20.8px;
|
||||
width: 32px;
|
||||
line-height: 32px;
|
||||
.fieldRow {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roomName {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
.button {
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.left .content form:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.left .content form:last-child {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.right .content {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.right .content h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.authLinks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.authLinks {
|
||||
margin-bottom: 100px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.authLinks a {
|
||||
color: #0dbd8b;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.left {
|
||||
background-color: var(--bgColor2);
|
||||
}
|
||||
|
||||
.home:not(.fullWidth) .left {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.home:not(.fullWidth) .leftNav {
|
||||
background-color: var(--bgColor2);
|
||||
}
|
||||
|
||||
.splitContainer {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.fullWidth .content hr:after,
|
||||
.left .content hr:after,
|
||||
.fullWidth .header {
|
||||
background-color: var(--bgColor2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,18 +44,6 @@ export const InputField = forwardRef(
|
||||
}
|
||||
);
|
||||
|
||||
export const Button = forwardRef(({ className, children, ...rest }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={classNames(styles.button, className)}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export function ErrorMessage({ children }) {
|
||||
return <p className={styles.errorMessage}>{children}</p>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.fieldRow {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -8,39 +8,34 @@
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.fieldRow.rightAlign {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.fieldRow > .field {
|
||||
margin: 0 5px;
|
||||
.fieldRow > * {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.fieldRow > .field:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.fieldRow > .field:last-child {
|
||||
.fieldRow > :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
border-radius: 4px;
|
||||
transition: border-color 0.25s;
|
||||
border: 1px solid #394049;
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
}
|
||||
|
||||
.inputField input {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 9px;
|
||||
color: white;
|
||||
background-color: #21262c;
|
||||
padding: 11px 9px;
|
||||
color: var(--textColor1);
|
||||
background-color: var(--bgColor1);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -52,19 +47,19 @@
|
||||
|
||||
.inputField input:placeholder-shown:focus::placeholder {
|
||||
transition: color 0.25s ease-in 0.1s;
|
||||
color: #6f7882;
|
||||
color: var(--textColor2);
|
||||
}
|
||||
|
||||
.inputField label {
|
||||
transition: font-size 0.25s ease-out 0.1s, color 0.25s ease-out 0.1s,
|
||||
top 0.25s ease-out 0.1s, background-color 0.25s ease-out 0.1s;
|
||||
color: white;
|
||||
color: var(--textColor3);
|
||||
background-color: transparent;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin: 7px 8px;
|
||||
margin: 9px 8px;
|
||||
padding: 2px;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
@@ -74,7 +69,7 @@
|
||||
}
|
||||
|
||||
.inputField:focus-within {
|
||||
border-color: #0086e6;
|
||||
border-color: var(--inputBorderColorFocused);
|
||||
}
|
||||
|
||||
.inputField input:focus {
|
||||
@@ -83,7 +78,7 @@
|
||||
|
||||
.inputField input:focus + label,
|
||||
.inputField input:not(:placeholder-shown) + label {
|
||||
background-color: #21262c;
|
||||
background-color: var(--bgColor2);
|
||||
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s,
|
||||
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s;
|
||||
font-size: 10px;
|
||||
@@ -93,7 +88,7 @@
|
||||
}
|
||||
|
||||
.inputField input:focus + label {
|
||||
color: #0086e6;
|
||||
color: var(--inputBorderColorFocused);
|
||||
}
|
||||
|
||||
.checkboxField {
|
||||
@@ -109,6 +104,7 @@
|
||||
}
|
||||
|
||||
.checkboxField input {
|
||||
outline: 0;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
@@ -138,36 +134,16 @@
|
||||
}
|
||||
|
||||
.checkboxField input[type="checkbox"]:checked + label > .checkbox {
|
||||
background: #0dbd8b;
|
||||
border-color: #0dbd8b;
|
||||
background: var(--primaryColor);
|
||||
border-color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.checkboxField input[type="checkbox"]:checked + label > .checkbox svg {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.button {
|
||||
vertical-align: middle;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
background-color: #0dbd8b;
|
||||
width: auto;
|
||||
padding: 7px 15px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
}
|
||||
|
||||
.button:active {
|
||||
.checkboxField:focus-within .checkbox {
|
||||
border: 1.5px solid var(--inputBorderColorFocused) !important;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
|
||||
21
src/InviteModal.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { Modal, ModalContent } from "./Modal";
|
||||
import { CopyButton } from "./button";
|
||||
import { getRoomUrl } from "./ConferenceCallManagerHooks";
|
||||
import styles from "./InviteModal.module.css";
|
||||
|
||||
export function InviteModal({ roomId, ...rest }) {
|
||||
return (
|
||||
<Modal
|
||||
title="Invite People"
|
||||
isDismissable
|
||||
className={styles.inviteModal}
|
||||
{...rest}
|
||||
>
|
||||
<ModalContent>
|
||||
<p>Copy and share this meeting link</p>
|
||||
<CopyButton className={styles.copyButton} value={getRoomUrl(roomId)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
7
src/InviteModal.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.inviteModal {
|
||||
max-width: 413px;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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: white;
|
||||
border-radius: 8px;
|
||||
padding: 25px 60px;
|
||||
max-width: 400px;
|
||||
background-color: #21262c;
|
||||
flex: 1;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
50
src/ListBox.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useListBox, useOption } from "@react-aria/listbox";
|
||||
import styles from "./ListBox.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function ListBox(props) {
|
||||
const ref = useRef();
|
||||
let { listBoxRef = ref, state } = props;
|
||||
const { listBoxProps } = useListBox(props, state, listBoxRef);
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...listBoxProps}
|
||||
ref={listBoxRef}
|
||||
className={classNames(styles.listBox, props.className)}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
<Option
|
||||
key={item.key}
|
||||
item={item}
|
||||
state={state}
|
||||
className={props.optionClassName}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function Option({ item, state, className }) {
|
||||
const ref = useRef();
|
||||
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
|
||||
{ key: item.key },
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
{...optionProps}
|
||||
ref={ref}
|
||||
className={classNames(styles.option, className, {
|
||||
[styles.selected]: isSelected,
|
||||
[styles.focused]: isFocused,
|
||||
[styles.disables]: isDisabled,
|
||||
})}
|
||||
>
|
||||
{item.rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
37
src/ListBox.module.css
Normal file
@@ -0,0 +1,37 @@
|
||||
.listBox {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
background-color: var(--bgColor1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: transparent;
|
||||
color: var(--textColor1);
|
||||
padding: 8px 16px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
color: #0dbd8b;
|
||||
}
|
||||
|
||||
.option.focused {
|
||||
background-color: rgba(111, 120, 130, 0.2);
|
||||
}
|
||||
|
||||
.option.disabled {
|
||||
color: var(--textColor2);
|
||||
background-color: var(--bgColor3);
|
||||
}
|
||||
@@ -16,11 +16,14 @@ limitations under the License.
|
||||
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||
import { Header, LeftNav } from "./Header";
|
||||
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
|
||||
import { Center, Content, Info, Modal } from "./Layout";
|
||||
import { ReactComponent as Logo } from "./icons/LogoLarge.svg";
|
||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||
import { Button } from "./button";
|
||||
import { useClient } from "./ConferenceCallManagerHooks";
|
||||
import styles from "./LoginPage.module.css";
|
||||
|
||||
export function LoginPage({ onLogin }) {
|
||||
export function LoginPage() {
|
||||
const { login } = useClient();
|
||||
const [homeserver, setHomeServer] = useState(
|
||||
`${window.location.protocol}//${window.location.host}`
|
||||
);
|
||||
@@ -28,33 +31,41 @@ export function LoginPage({ onLogin }) {
|
||||
const passwordRef = useRef();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
|
||||
// TODO: Handle hitting login page with authenticated client
|
||||
|
||||
const onSubmitLoginForm = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
onLogin(homeserver, usernameRef.current.value, passwordRef.current.value)
|
||||
setLoading(true);
|
||||
|
||||
login(homeserver, usernameRef.current.value, passwordRef.current.value)
|
||||
.then(() => {
|
||||
if (location.state && location.state.from) {
|
||||
history.replace(location.state.from);
|
||||
history.push(location.state.from);
|
||||
} else {
|
||||
history.replace("/");
|
||||
history.push("/");
|
||||
}
|
||||
})
|
||||
.catch(setError);
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[onLogin, location, history, homeserver]
|
||||
[login, location, history, homeserver]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
</Header>
|
||||
<Content>
|
||||
<Center>
|
||||
<Modal>
|
||||
<h2>Login</h2>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.formContainer}>
|
||||
<Logo width="auto" height="auto" className={styles.logo} />
|
||||
|
||||
<h2>Log In</h2>
|
||||
<h4>To continue to Element</h4>
|
||||
<form onSubmit={onSubmitLoginForm}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
@@ -90,24 +101,23 @@ export function LoginPage({ onLogin }) {
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow rightAlign>
|
||||
<Button type="submit">Login</Button>
|
||||
<FieldRow>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Logging in..." : "Login"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
<Info>
|
||||
New?{" "}
|
||||
<Link
|
||||
to={{
|
||||
pathname: "/register",
|
||||
state: location.state,
|
||||
}}
|
||||
>
|
||||
Create account
|
||||
</Link>
|
||||
</Info>
|
||||
</Modal>
|
||||
</Center>
|
||||
</Content>
|
||||
</div>
|
||||
<div className={styles.authLinks}>
|
||||
<p>Not registered yet?</p>
|
||||
<p>
|
||||
<Link to="/register">Create an account</Link>
|
||||
{" Or "}
|
||||
<Link to="/">Access as a guest</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
70
src/LoginPage.module.css
Normal file
@@ -0,0 +1,70 @@
|
||||
.logo {
|
||||
max-width: 300px;
|
||||
margin: 80px 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.formContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.formContainer h2,
|
||||
.formContainer h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.formContainer h4 {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.formContainer form {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.formContainer button {
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.authLinks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.authLinks {
|
||||
margin-bottom: 100px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.authLinks a {
|
||||
color: #0dbd8b;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
@@ -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 |
60
src/Menu.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import styles from "./Menu.module.css";
|
||||
import { useMenu, useMenuItem } from "@react-aria/menu";
|
||||
import { useTreeState } from "@react-stately/tree";
|
||||
import { mergeProps } from "@react-aria/utils";
|
||||
import { useFocus } from "@react-aria/interactions";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function Menu({ className, onAction, ...rest }) {
|
||||
const state = useTreeState({ ...rest, selectionMode: "none" });
|
||||
const menuRef = useRef();
|
||||
const { menuProps } = useMenu(rest, state, menuRef);
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...mergeProps(menuProps, rest)}
|
||||
ref={menuRef}
|
||||
className={classNames(styles.menu, className)}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
<MenuItem
|
||||
key={item.key}
|
||||
item={item}
|
||||
state={state}
|
||||
onAction={onAction}
|
||||
onClose={rest.onClose}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItem({ item, state, onAction, onClose }) {
|
||||
const ref = useRef();
|
||||
const { menuItemProps } = useMenuItem(
|
||||
{
|
||||
key: item.key,
|
||||
isDisabled: item.isDisabled,
|
||||
onAction,
|
||||
onClose,
|
||||
},
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
const { focusProps } = useFocus({ onFocusChange: setFocused });
|
||||
|
||||
return (
|
||||
<li
|
||||
{...mergeProps(menuItemProps, focusProps)}
|
||||
ref={ref}
|
||||
className={classNames(styles.menuItem, {
|
||||
[styles.focused]: isFocused,
|
||||
})}
|
||||
>
|
||||
{item.rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
39
src/Menu.module.css
Normal file
@@ -0,0 +1,39 @@
|
||||
.menu {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
cursor: pointer;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
color: var(--textColor1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menuItem > * {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.menuItem > :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.menuItem.focused,
|
||||
.menuItem:hover {
|
||||
background-color: var(--bgColor4);
|
||||
}
|
||||
|
||||
.menuItem.focused:first-child,
|
||||
.menuItem:hover:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.menuItem.focused:last-child,
|
||||
.menuItem:hover:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
132
src/Modal.jsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React, { useRef, useMemo } from "react";
|
||||
import {
|
||||
useOverlay,
|
||||
usePreventScroll,
|
||||
useModal,
|
||||
OverlayContainer,
|
||||
} from "@react-aria/overlays";
|
||||
import { useOverlayTriggerState } from "@react-stately/overlays";
|
||||
import { useDialog } from "@react-aria/dialog";
|
||||
import { FocusScope } from "@react-aria/focus";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
||||
import styles from "./Modal.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function Modal(props) {
|
||||
const { title, children, className, mobileFullScreen } = props;
|
||||
const modalRef = useRef();
|
||||
const { overlayProps, underlayProps } = useOverlay(props, modalRef);
|
||||
usePreventScroll();
|
||||
const { modalProps } = useModal();
|
||||
const { dialogProps, titleProps } = useDialog(props, modalRef);
|
||||
const closeButtonRef = useRef();
|
||||
const { buttonProps: closeButtonProps } = useButton({
|
||||
onPress: () => props.onClose(),
|
||||
});
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<div className={styles.modalOverlay} {...underlayProps}>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div
|
||||
{...overlayProps}
|
||||
{...dialogProps}
|
||||
{...modalProps}
|
||||
ref={modalRef}
|
||||
className={classNames(
|
||||
styles.modal,
|
||||
{ [styles.mobileFullScreen]: mobileFullScreen },
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={styles.modalHeader}>
|
||||
<h3 {...titleProps}>{title}</h3>
|
||||
<button
|
||||
{...closeButtonProps}
|
||||
ref={closeButtonRef}
|
||||
className={styles.closeButton}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</FocusScope>
|
||||
</div>
|
||||
</OverlayContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalContent({ children, className, ...rest }) {
|
||||
return (
|
||||
<div className={classNames(styles.content, className)} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useModalTriggerState() {
|
||||
const modalState = useOverlayTriggerState({});
|
||||
const modalProps = useMemo(
|
||||
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
||||
[modalState]
|
||||
);
|
||||
return { modalState, modalProps };
|
||||
}
|
||||
|
||||
export function useToggleModalButton(modalState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.toggle(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useOpenModalButton(modalState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.open(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useCloseModalButton(modalState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.close(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalTrigger({ children }) {
|
||||
const { modalState, modalProps } = useModalState();
|
||||
const buttonRef = useRef();
|
||||
const { buttonProps } = useToggleModalButton(modalState, buttonRef);
|
||||
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
typeof children[1] !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"ModalTrigger must have two props. The first being a button and the second being a render prop."
|
||||
);
|
||||
}
|
||||
|
||||
const [modalTrigger, modal] = children;
|
||||
|
||||
return (
|
||||
<>
|
||||
<modalTrigger.type
|
||||
{...modalTrigger.props}
|
||||
{...buttonProps}
|
||||
ref={buttonRef}
|
||||
/>
|
||||
{modalState.isOpen && modal(modalProps)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
67
src/Modal.module.css
Normal file
@@ -0,0 +1,67 @@
|
||||
.modalOverlay {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(23, 25, 28, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #21262c;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
max-width: 90vw;
|
||||
width: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 34px 34px 0 34px;
|
||||
}
|
||||
|
||||
.modalHeader h3 {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 32px;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 799px) {
|
||||
.modal.mobileFullScreen {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
70
src/OverflowMenu.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { ButtonTooltip, Button } from "./button";
|
||||
import { Menu } from "./Menu";
|
||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { ReactComponent as SettingsIcon } from "./icons/Settings.svg";
|
||||
import { ReactComponent as AddUserIcon } from "./icons/AddUser.svg";
|
||||
import { ReactComponent as OverflowIcon } from "./icons/Overflow.svg";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { SettingsModal } from "./SettingsModal";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
|
||||
export function OverflowMenu({
|
||||
roomId,
|
||||
setShowInspector,
|
||||
showInspector,
|
||||
client,
|
||||
}) {
|
||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||
useModalTriggerState();
|
||||
const { modalState: settingsModalState, modalProps: settingsModalProps } =
|
||||
useModalTriggerState();
|
||||
|
||||
// TODO: On closing modal, focus should be restored to the trigger button
|
||||
// https://github.com/adobe/react-spectrum/issues/2444
|
||||
const onAction = useCallback((key) => {
|
||||
switch (key) {
|
||||
case "invite":
|
||||
inviteModalState.open();
|
||||
break;
|
||||
case "settings":
|
||||
settingsModalState.open();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopoverMenuTrigger disableOnState>
|
||||
<Button variant="toolbar">
|
||||
<ButtonTooltip>More</ButtonTooltip>
|
||||
<OverflowIcon />
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Menu {...props} label="More menu" onAction={onAction}>
|
||||
<Item key="invite" textValue="Invite people">
|
||||
<AddUserIcon />
|
||||
<span>Invite people</span>
|
||||
</Item>
|
||||
<Item key="settings" textValue="Settings">
|
||||
<SettingsIcon />
|
||||
<span>Settings</span>
|
||||
</Item>
|
||||
</Menu>
|
||||
)}
|
||||
</PopoverMenuTrigger>
|
||||
{settingsModalState.isOpen && (
|
||||
<SettingsModal
|
||||
{...settingsModalProps}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
client={client}
|
||||
/>
|
||||
)}
|
||||
{inviteModalState.isOpen && (
|
||||
<InviteModal roomId={roomId} {...inviteModalProps} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
36
src/Popover.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { forwardRef, useRef } from "react";
|
||||
import { DismissButton, useOverlay } from "@react-aria/overlays";
|
||||
import { FocusScope } from "@react-aria/focus";
|
||||
import classNames from "classnames";
|
||||
import styles from "./Popover.module.css";
|
||||
|
||||
export const Popover = forwardRef(
|
||||
({ isOpen = true, onClose, className, children, ...rest }, ref) => {
|
||||
const fallbackRef = useRef();
|
||||
const popoverRef = ref || fallbackRef;
|
||||
|
||||
const { overlayProps } = useOverlay(
|
||||
{
|
||||
isOpen,
|
||||
onClose,
|
||||
shouldCloseOnBlur: true,
|
||||
isDismissable: true,
|
||||
},
|
||||
popoverRef
|
||||
);
|
||||
|
||||
return (
|
||||
<FocusScope restoreFocus>
|
||||
<div
|
||||
{...overlayProps}
|
||||
{...rest}
|
||||
className={classNames(styles.popover, className)}
|
||||
ref={popoverRef}
|
||||
>
|
||||
{children}
|
||||
<DismissButton onDismiss={onClose} />
|
||||
</div>
|
||||
</FocusScope>
|
||||
);
|
||||
}
|
||||
);
|
||||
8
src/Popover.module.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 194px;
|
||||
background: var(--bgColor2);
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 8px;
|
||||
}
|
||||
72
src/PopoverMenu.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { useRef } from "react";
|
||||
import styles from "./PopoverMenu.module.css";
|
||||
import { useMenuTriggerState } from "@react-stately/menu";
|
||||
import { useMenuTrigger } from "@react-aria/menu";
|
||||
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
||||
import classNames from "classnames";
|
||||
import { Popover } from "./Popover";
|
||||
|
||||
export function PopoverMenuTrigger({
|
||||
children,
|
||||
placement,
|
||||
className,
|
||||
disableOnState,
|
||||
...rest
|
||||
}) {
|
||||
const popoverMenuState = useMenuTriggerState(rest);
|
||||
const buttonRef = useRef();
|
||||
const { menuTriggerProps, menuProps } = useMenuTrigger(
|
||||
{},
|
||||
popoverMenuState,
|
||||
buttonRef
|
||||
);
|
||||
|
||||
const popoverRef = useRef();
|
||||
|
||||
const { overlayProps } = useOverlayPosition({
|
||||
targetRef: buttonRef,
|
||||
overlayRef: popoverRef,
|
||||
placement: placement || "top",
|
||||
offset: 5,
|
||||
isOpen: popoverMenuState.isOpen,
|
||||
});
|
||||
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
typeof children[1] !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"PopoverMenu must have two props. The first being a button and the second being a render prop."
|
||||
);
|
||||
}
|
||||
|
||||
const [popoverTrigger, popoverMenu] = children;
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.popoverMenuTrigger, className)}>
|
||||
<popoverTrigger.type
|
||||
{...popoverTrigger.props}
|
||||
{...menuTriggerProps}
|
||||
on={!disableOnState && popoverMenuState.isOpen}
|
||||
ref={buttonRef}
|
||||
/>
|
||||
{popoverMenuState.isOpen && (
|
||||
<OverlayContainer>
|
||||
<Popover
|
||||
{...overlayProps}
|
||||
isOpen={popoverMenuState.isOpen}
|
||||
onClose={popoverMenuState.close}
|
||||
ref={popoverRef}
|
||||
>
|
||||
{popoverMenu({
|
||||
...menuProps,
|
||||
autoFocus: popoverMenuState.focusStrategy,
|
||||
onClose: popoverMenuState.close,
|
||||
})}
|
||||
</Popover>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
4
src/PopoverMenu.module.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.popoverMenuTrigger {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
76
src/ProfileModal.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Button } from "./button";
|
||||
import { useDisplayName } from "./ConferenceCallManagerHooks";
|
||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||
import { Modal, ModalContent } from "./Modal";
|
||||
|
||||
export function ProfileModal({ client, ...rest }) {
|
||||
const { onClose } = rest;
|
||||
const {
|
||||
success,
|
||||
error,
|
||||
loading,
|
||||
displayName: initialDisplayName,
|
||||
setDisplayName: submitDisplayName,
|
||||
} = useDisplayName(client);
|
||||
const [displayName, setDisplayName] = useState(initialDisplayName || "");
|
||||
|
||||
const onChangeDisplayName = useCallback(
|
||||
(e) => {
|
||||
setDisplayName(e.target.value);
|
||||
},
|
||||
[setDisplayName]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const displayName = data.get("displayName");
|
||||
console.log(displayName);
|
||||
submitDisplayName(displayName);
|
||||
},
|
||||
[setDisplayName]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
}, [success, onClose]);
|
||||
|
||||
return (
|
||||
<Modal title="Profile" isDismissable {...rest}>
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="displayName"
|
||||
name="displayName"
|
||||
label="Display Name"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Display Name"
|
||||
value={displayName}
|
||||
onChange={onChangeDisplayName}
|
||||
/>
|
||||
</FieldRow>
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow rightAlign>
|
||||
<Button type="button" variant="secondary" onPress={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
0
src/ProfileModal.module.css
Normal file
@@ -16,45 +16,52 @@ limitations under the License.
|
||||
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||
import { Header, LeftNav } from "./Header";
|
||||
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
|
||||
import { Center, Content, Info, Modal } from "./Layout";
|
||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||
import { Button } from "./button";
|
||||
import { useClient } from "./ConferenceCallManagerHooks";
|
||||
import styles from "./LoginPage.module.css";
|
||||
import { ReactComponent as Logo } from "./icons/LogoLarge.svg";
|
||||
|
||||
export function RegisterPage({ onRegister }) {
|
||||
export function RegisterPage() {
|
||||
// TODO: Handle hitting login page with authenticated client
|
||||
const { register } = useClient();
|
||||
const registerUsernameRef = useRef();
|
||||
const registerPasswordRef = useRef();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
|
||||
const onSubmitRegisterForm = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
onRegister(
|
||||
setLoading(true);
|
||||
register(
|
||||
registerUsernameRef.current.value,
|
||||
registerPasswordRef.current.value
|
||||
)
|
||||
.then(() => {
|
||||
if (location.state && location.state.from) {
|
||||
history.replace(location.state.from);
|
||||
history.push(location.state.from);
|
||||
} else {
|
||||
history.replace("/");
|
||||
history.push("/");
|
||||
}
|
||||
})
|
||||
.catch(setError);
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[onRegister, location, history]
|
||||
[register, location, history]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
</Header>
|
||||
<Content>
|
||||
<Center>
|
||||
<Modal>
|
||||
<h2>Register</h2>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.formContainer}>
|
||||
<Logo width="auto" height="auto" className={styles.logo} />
|
||||
<h2>Create your account</h2>
|
||||
<form onSubmit={onSubmitRegisterForm}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
@@ -79,24 +86,23 @@ export function RegisterPage({ onRegister }) {
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow rightAlign>
|
||||
<Button type="submit">Register</Button>
|
||||
<FieldRow>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Registering..." : "Register"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
<Info>
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
to={{
|
||||
pathname: "/login",
|
||||
state: location.state,
|
||||
}}
|
||||
>
|
||||
Sign in here
|
||||
</Link>
|
||||
</Info>
|
||||
</Modal>
|
||||
</Center>
|
||||
</Content>
|
||||
</div>
|
||||
<div className={styles.authLinks}>
|
||||
<p>Already have an account?</p>
|
||||
<p>
|
||||
<Link to="/login">Log in</Link>
|
||||
{" Or "}
|
||||
<Link to="/">Access as a guest</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
459
src/Room.jsx
@@ -16,18 +16,23 @@ limitations under the License.
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import styles from "./Room.module.css";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { useLocation, useParams, useHistory, Link } from "react-router-dom";
|
||||
import {
|
||||
Button,
|
||||
CopyButton,
|
||||
HangupButton,
|
||||
MicButton,
|
||||
VideoButton,
|
||||
LayoutToggleButton,
|
||||
ScreenshareButton,
|
||||
DropdownButton,
|
||||
SettingsButton,
|
||||
} from "./RoomButton";
|
||||
import { Header, LeftNav, RightNav, CenterNav } from "./Header";
|
||||
import { Button } from "./Input";
|
||||
LinkButton,
|
||||
} from "./button";
|
||||
import {
|
||||
Header,
|
||||
LeftNav,
|
||||
RightNav,
|
||||
RoomHeaderInfo,
|
||||
RoomSetupHeaderInfo,
|
||||
} from "./Header";
|
||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import VideoGrid, {
|
||||
useVideoGridLayout,
|
||||
@@ -37,10 +42,14 @@ 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 { fetchGroupCall } from "./ConferenceCallManagerHooks";
|
||||
import { ErrorModal } from "./ErrorModal";
|
||||
import { useClient, useLoadGroupCall } 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";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
@@ -48,31 +57,32 @@ const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||
// For now we can disable screensharing in Safari.
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
function useLoadGroupCall(client, roomId) {
|
||||
const [state, setState] = useState({
|
||||
loading: true,
|
||||
error: undefined,
|
||||
groupCall: undefined,
|
||||
});
|
||||
export function Room() {
|
||||
const { loading, error, client, isGuest } = useClient();
|
||||
|
||||
useEffect(() => {
|
||||
setState({ loading: true });
|
||||
fetchGroupCall(client, roomId, 30000)
|
||||
.then((groupCall) => setState({ loading: false, groupCall }))
|
||||
.catch((error) => setState({ loading: false, error }));
|
||||
}, [roomId]);
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return state;
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return <GroupCall client={client} isGuest={isGuest} />;
|
||||
}
|
||||
|
||||
export function Room({ client }) {
|
||||
export function GroupCall({ client, isGuest }) {
|
||||
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);
|
||||
const simpleGrid = useMemo(
|
||||
() => new URLSearchParams(search).has("simple"),
|
||||
[search]
|
||||
const { loading, error, groupCall } = useLoadGroupCall(
|
||||
client,
|
||||
roomId,
|
||||
viaServers
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -80,33 +90,32 @@ export function Room({ client }) {
|
||||
}, [groupCall]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.room}>
|
||||
<LoadingRoomView />
|
||||
</div>
|
||||
);
|
||||
return <LoadingRoomView />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.room}>
|
||||
<ErrorModal error={error} />
|
||||
</div>
|
||||
);
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.room}>
|
||||
<GroupCallView
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
simpleGrid={simpleGrid}
|
||||
/>
|
||||
</div>
|
||||
<GroupCallView
|
||||
isGuest={isGuest}
|
||||
client={client}
|
||||
roomId={roomId}
|
||||
groupCall={groupCall}
|
||||
simpleGrid={simpleGrid}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCallView({ client, groupCall, simpleGrid }) {
|
||||
export function GroupCallView({
|
||||
client,
|
||||
isGuest,
|
||||
roomId,
|
||||
groupCall,
|
||||
simpleGrid,
|
||||
}) {
|
||||
const [showInspector, setShowInspector] = useState(false);
|
||||
const {
|
||||
state,
|
||||
error,
|
||||
@@ -134,19 +143,38 @@ export function GroupCallView({ client, groupCall, simpleGrid }) {
|
||||
}
|
||||
}
|
||||
|
||||
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 (!isGuest) {
|
||||
history.push("/");
|
||||
} else {
|
||||
setLeft(true);
|
||||
}
|
||||
}, [leave, history, isGuest]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorModal error={error} />;
|
||||
return <ErrorView error={error} />;
|
||||
} else if (state === GroupCallState.Entered) {
|
||||
return (
|
||||
<InRoomView
|
||||
@@ -159,19 +187,26 @@ export function GroupCallView({ client, groupCall, simpleGrid }) {
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
userMediaFeeds={userMediaFeeds}
|
||||
activeSpeaker={activeSpeaker}
|
||||
onLeave={leave}
|
||||
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) {
|
||||
return <CallEndedScreen />;
|
||||
} else {
|
||||
return (
|
||||
<RoomSetupView
|
||||
isGuest={isGuest}
|
||||
client={client}
|
||||
hasLocalParticipant={hasLocalParticipant}
|
||||
roomName={groupCall.room.name}
|
||||
state={state}
|
||||
@@ -182,6 +217,9 @@ export function GroupCallView({ client, groupCall, simpleGrid }) {
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
roomId={roomId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -189,25 +227,23 @@ export function GroupCallView({ client, groupCall, simpleGrid }) {
|
||||
|
||||
export function LoadingRoomView() {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.centerMessage}>
|
||||
<p>Loading room...</p>
|
||||
</div>
|
||||
</>
|
||||
<FullScreenView>
|
||||
<h1>Loading room...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
export function EnteringRoomView() {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.centerMessage}>
|
||||
<p>Entering room...</p>
|
||||
</div>
|
||||
</>
|
||||
<FullScreenView>
|
||||
<h1>Entering room...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
function RoomSetupView({
|
||||
client,
|
||||
isGuest,
|
||||
roomName,
|
||||
state,
|
||||
onInitLocalCallFeed,
|
||||
@@ -217,125 +253,96 @@ function RoomSetupView({
|
||||
localVideoMuted,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
hasLocalParticipant,
|
||||
setShowInspector,
|
||||
showInspector,
|
||||
roomId,
|
||||
}) {
|
||||
const { stream } = useCallFeed(localCallFeed);
|
||||
const videoRef = useMediaStream(stream, true);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
onInitLocalCallFeed();
|
||||
}, [onInitLocalCallFeed]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.room}>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
<CenterNav>
|
||||
<h3>{roomName}</h3>
|
||||
</CenterNav>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={roomName} />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
{isGuest ? (
|
||||
<LinkButton to={{ pathname: "/login", state: { from: location } }}>
|
||||
Log in
|
||||
</LinkButton>
|
||||
) : (
|
||||
<UserMenu />
|
||||
)}
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.joinRoom}>
|
||||
{hasLocalParticipant && (
|
||||
<p>Warning, you are signed into this call on another device.</p>
|
||||
)}
|
||||
<div className={styles.preview}>
|
||||
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||
<p className={styles.webcamPermissions}>
|
||||
Webcam/microphone permissions needed to join the call.
|
||||
</p>
|
||||
)}
|
||||
{state === GroupCallState.InitializingLocalCallFeed && (
|
||||
<p className={styles.webcamPermissions}>
|
||||
Accept webcam/microphone permissions to join the call.
|
||||
</p>
|
||||
)}
|
||||
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
||||
</div>
|
||||
{state === GroupCallState.LocalCallFeedInitialized && (
|
||||
<div className={styles.previewButtons}>
|
||||
<MicButton
|
||||
muted={microphoneMuted}
|
||||
onClick={toggleMicrophoneMuted}
|
||||
/>
|
||||
<VideoButton
|
||||
enabled={localVideoMuted}
|
||||
onClick={toggleLocalVideoMuted}
|
||||
/>
|
||||
<div className={styles.joinRoomContent}>
|
||||
<h1>{roomName}</h1>
|
||||
<div className={styles.preview}>
|
||||
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||
<p className={styles.webcamPermissions}>
|
||||
Webcam/microphone permissions needed to join the call.
|
||||
</p>
|
||||
)}
|
||||
{state === GroupCallState.InitializingLocalCallFeed && (
|
||||
<p className={styles.webcamPermissions}>
|
||||
Accept webcam/microphone permissions to join the call.
|
||||
</p>
|
||||
)}
|
||||
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
||||
{state === GroupCallState.LocalCallFeedInitialized && (
|
||||
<>
|
||||
<Button
|
||||
className={styles.joinCallButton}
|
||||
disabled={state !== GroupCallState.LocalCallFeedInitialized}
|
||||
onPress={onEnter}
|
||||
>
|
||||
Join call now
|
||||
</Button>
|
||||
<div className={styles.previewButtons}>
|
||||
<MicButton
|
||||
muted={microphoneMuted}
|
||||
onPress={toggleMicrophoneMuted}
|
||||
/>
|
||||
<VideoButton
|
||||
muted={localVideoMuted}
|
||||
onPress={toggleLocalVideoMuted}
|
||||
/>
|
||||
<OverflowMenu
|
||||
roomId={roomId}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
client={client}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
disabled={state !== GroupCallState.LocalCallFeedInitialized}
|
||||
onClick={onEnter}
|
||||
>
|
||||
Enter Call
|
||||
</Button>
|
||||
<p>Or</p>
|
||||
<CopyButton
|
||||
value={window.location.href}
|
||||
className={styles.copyButton}
|
||||
copiedMessage="Call link copied"
|
||||
>
|
||||
Copy call link and join later
|
||||
</CopyButton>
|
||||
</div>
|
||||
<div className={styles.joinRoomFooter}>
|
||||
<Link className={styles.homeLink} to="/">
|
||||
Take me Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useMediaHandler(client) {
|
||||
const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
|
||||
useState({
|
||||
audioInput: null,
|
||||
videoInput: null,
|
||||
audioInputs: [],
|
||||
videoInputs: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function updateDevices() {
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
const audioInputs = devices.filter(
|
||||
(device) => device.kind === "audioinput"
|
||||
);
|
||||
const videoInputs = devices.filter(
|
||||
(device) => device.kind === "videoinput"
|
||||
);
|
||||
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
audioInputs,
|
||||
videoInputs,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
updateDevices();
|
||||
|
||||
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
|
||||
|
||||
return () => {
|
||||
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setAudioInput = useCallback(
|
||||
(deviceId) => {
|
||||
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
|
||||
client.getMediaHandler().setAudioInput(deviceId);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
const setVideoInput = useCallback(
|
||||
(deviceId) => {
|
||||
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
|
||||
client.getMediaHandler().setVideoInput(deviceId);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return {
|
||||
audioInput,
|
||||
audioInputs,
|
||||
setAudioInput,
|
||||
videoInput,
|
||||
videoInputs,
|
||||
setVideoInput,
|
||||
};
|
||||
}
|
||||
|
||||
function InRoomView({
|
||||
client,
|
||||
groupCall,
|
||||
@@ -351,25 +358,11 @@ function InRoomView({
|
||||
isScreensharing,
|
||||
screenshareFeeds,
|
||||
simpleGrid,
|
||||
setShowInspector,
|
||||
showInspector,
|
||||
roomId,
|
||||
}) {
|
||||
const [showInspector, setShowInspector] = useState(false);
|
||||
|
||||
const [layout, toggleLayout] = useVideoGridLayout();
|
||||
|
||||
const {
|
||||
audioInput,
|
||||
audioInputs,
|
||||
setAudioInput,
|
||||
videoInput,
|
||||
videoInputs,
|
||||
setVideoInput,
|
||||
} = useMediaHandler(client);
|
||||
|
||||
useEffect(() => {
|
||||
if (screenshareFeeds.length > 0 && layout === "gallery") {
|
||||
toggleLayout();
|
||||
}
|
||||
}, [screenshareFeeds]);
|
||||
const [layout, setLayout] = useVideoGridLayout();
|
||||
|
||||
const items = useMemo(() => {
|
||||
const participants = [];
|
||||
@@ -377,7 +370,7 @@ function InRoomView({
|
||||
for (const callFeed of userMediaFeeds) {
|
||||
participants.push({
|
||||
id: callFeed.stream.id,
|
||||
callFeed,
|
||||
usermediaCallFeed: callFeed,
|
||||
isActiveSpeaker:
|
||||
screenshareFeeds.length === 0
|
||||
? callFeed.userId === activeSpeaker
|
||||
@@ -386,11 +379,10 @@ function InRoomView({
|
||||
}
|
||||
|
||||
for (const callFeed of screenshareFeeds) {
|
||||
participants.push({
|
||||
id: callFeed.stream.id,
|
||||
callFeed,
|
||||
isActiveSpeaker: true,
|
||||
});
|
||||
const participant = participants.find(
|
||||
(p) => p.usermediaCallFeed.userId === callFeed.userId
|
||||
);
|
||||
participant.screenshareCallFeed = callFeed;
|
||||
}
|
||||
|
||||
return participants;
|
||||
@@ -398,7 +390,7 @@ function InRoomView({
|
||||
|
||||
const onFocusTile = useCallback(
|
||||
(tiles, focusedTile) => {
|
||||
if (layout === "gallery") {
|
||||
if (layout === "freedom") {
|
||||
return tiles.map((tile) => {
|
||||
if (tile === focusedTile) {
|
||||
return { ...tile, presenter: !tile.presenter };
|
||||
@@ -407,7 +399,7 @@ function InRoomView({
|
||||
return tile;
|
||||
});
|
||||
} else {
|
||||
toggleLayout();
|
||||
setLayout("spotlight");
|
||||
|
||||
return tiles.map((tile) => {
|
||||
if (tile === focusedTile) {
|
||||
@@ -418,27 +410,18 @@ function InRoomView({
|
||||
});
|
||||
}
|
||||
},
|
||||
[layout, toggleLayout]
|
||||
[layout, setLayout]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames(styles.room, styles.inRoom)}>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
<CenterNav>
|
||||
<h3>{roomName}</h3>
|
||||
</CenterNav>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={roomName} />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<SettingsButton
|
||||
title={showInspector ? "Hide Inspector" : "Show Inspector"}
|
||||
on={showInspector}
|
||||
onClick={() => setShowInspector((prev) => !prev)}
|
||||
/>
|
||||
<LayoutToggleButton
|
||||
title={layout === "spotlight" ? "Spotlight" : "Gallery"}
|
||||
layout={layout}
|
||||
onClick={toggleLayout}
|
||||
/>
|
||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||
<UserMenu />
|
||||
</RightNav>
|
||||
</Header>
|
||||
{items.length === 0 ? (
|
||||
@@ -448,45 +431,65 @@ function InRoomView({
|
||||
) : simpleGrid ? (
|
||||
<SimpleVideoGrid items={items} />
|
||||
) : (
|
||||
<VideoGrid items={items} layout={layout} onFocusTile={onFocusTile} />
|
||||
<VideoGrid
|
||||
items={items}
|
||||
layout={layout}
|
||||
onFocusTile={onFocusTile}
|
||||
disableAnimations={isSafari}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.footer}>
|
||||
<DropdownButton
|
||||
value={audioInput}
|
||||
onChange={({ value }) => setAudioInput(value)}
|
||||
options={audioInputs.map(({ label, deviceId }) => ({
|
||||
label,
|
||||
value: deviceId,
|
||||
}))}
|
||||
>
|
||||
<MicButton muted={microphoneMuted} onClick={toggleMicrophoneMuted} />
|
||||
</DropdownButton>
|
||||
<DropdownButton
|
||||
value={videoInput}
|
||||
onChange={({ value }) => setVideoInput(value)}
|
||||
options={videoInputs.map(({ label, deviceId }) => ({
|
||||
label,
|
||||
value: deviceId,
|
||||
}))}
|
||||
>
|
||||
<VideoButton
|
||||
enabled={localVideoMuted}
|
||||
onClick={toggleLocalVideoMuted}
|
||||
/>
|
||||
</DropdownButton>
|
||||
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
|
||||
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
|
||||
{canScreenshare && !isSafari && (
|
||||
<ScreenshareButton
|
||||
enabled={isScreensharing}
|
||||
onClick={toggleScreensharing}
|
||||
onPress={toggleScreensharing}
|
||||
/>
|
||||
)}
|
||||
<HangupButton onClick={onLeave} />
|
||||
<OverflowMenu
|
||||
roomId={roomId}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
client={client}
|
||||
/>
|
||||
<HangupButton onPress={onLeave} />
|
||||
</div>
|
||||
<GroupCallInspector
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
show={showInspector}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallEndedScreen() {
|
||||
return (
|
||||
<FullScreenView className={styles.callEndedScreen}>
|
||||
<h1>Your call is now ended</h1>
|
||||
<hr />
|
||||
<h2>Why not finish by creating an account?</h2>
|
||||
<p>You'll be able to:</p>
|
||||
<ul>
|
||||
<li>Easily access all your previous call links</li>
|
||||
<li>Set a username and avatar</li>
|
||||
<li>
|
||||
Get your own Matrix ID so you can log in to{" "}
|
||||
<a href="https://element.io" target="_blank">
|
||||
Element
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<LinkButton
|
||||
className={styles.callEndedButton}
|
||||
size="lg"
|
||||
variant="default"
|
||||
to="/login"
|
||||
>
|
||||
Create account
|
||||
</LinkButton>
|
||||
<Link to="/">Not now, return to home screen</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,9 +18,14 @@ limitations under the License.
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.inRoom {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.joinRoom {
|
||||
@@ -29,23 +34,49 @@ limitations under the License.
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
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;
|
||||
max-width: 400px;
|
||||
max-height: 225px;
|
||||
max-width: 816px;
|
||||
max-height: 75vh;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
background-color: #444;
|
||||
background-color: var(--bgColor3);
|
||||
margin: 40px 20px 20px 20px;
|
||||
}
|
||||
|
||||
.preview video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
background-color: black;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.webcamPermissions {
|
||||
@@ -60,11 +91,31 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.previewButtons {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 66px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
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 > * {
|
||||
@@ -113,11 +164,31 @@ limitations under the License.
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.room {
|
||||
position: fixed;
|
||||
}
|
||||
.callEndedScreen h1 {
|
||||
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;
|
||||
}
|
||||
|
||||
.callEndedButton {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.roomContainer {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -125,4 +196,8 @@ limitations under the License.
|
||||
.footer {
|
||||
height: 118px;
|
||||
}
|
||||
|
||||
.joinRoomContent h1 {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import classNames from "classnames";
|
||||
import styles from "./RoomButton.module.css";
|
||||
import { ReactComponent as MicIcon } from "./icons/Mic.svg";
|
||||
import { ReactComponent as MuteMicIcon } from "./icons/MuteMic.svg";
|
||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||
import { ReactComponent as DisableVideoIcon } from "./icons/DisableVideo.svg";
|
||||
import { ReactComponent as HangupIcon } from "./icons/Hangup.svg";
|
||||
import { ReactComponent as SettingsIcon } from "./icons/Settings.svg";
|
||||
import { ReactComponent as GridIcon } from "./icons/Grid.svg";
|
||||
import { ReactComponent as SpeakerIcon } from "./icons/Speaker.svg";
|
||||
import { ReactComponent as ScreenshareIcon } from "./icons/Screenshare.svg";
|
||||
import { ReactComponent as ChevronIcon } from "./icons/Chevron.svg";
|
||||
|
||||
export function RoomButton({ on, className, children, ...rest }) {
|
||||
return (
|
||||
<button
|
||||
className={classNames(styles.roomButton, className, { [styles.on]: on })}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function DropdownButton({ onChange, options, value, children }) {
|
||||
const buttonRef = useRef();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function onClick() {
|
||||
if (open) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("click", onClick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", onClick);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className={styles.dropdownButtonContainer}>
|
||||
{children}
|
||||
<button
|
||||
ref={buttonRef}
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<ChevronIcon />
|
||||
</button>
|
||||
{open && (
|
||||
<div className={styles.dropdownContainer}>
|
||||
<ul>
|
||||
{options.map((item) => (
|
||||
<li
|
||||
key={item.value}
|
||||
className={classNames({
|
||||
[styles.dropdownActiveItem]: item.value === value,
|
||||
})}
|
||||
onClick={() => onChange(item)}
|
||||
>
|
||||
{item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MicButton({ muted, ...rest }) {
|
||||
return (
|
||||
<RoomButton {...rest} on={muted}>
|
||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||
</RoomButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function VideoButton({ enabled, ...rest }) {
|
||||
return (
|
||||
<RoomButton {...rest} on={enabled}>
|
||||
{enabled ? <DisableVideoIcon /> : <VideoIcon />}
|
||||
</RoomButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScreenshareButton({ enabled, className, ...rest }) {
|
||||
return (
|
||||
<RoomButton
|
||||
className={classNames(styles.screenshareButton, className)}
|
||||
{...rest}
|
||||
on={enabled}
|
||||
>
|
||||
<ScreenshareIcon />
|
||||
</RoomButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function HangupButton({ className, ...rest }) {
|
||||
return (
|
||||
<RoomButton
|
||||
className={classNames(styles.hangupButton, className)}
|
||||
{...rest}
|
||||
>
|
||||
<HangupIcon />
|
||||
</RoomButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeaderButton({ on, className, children, ...rest }) {
|
||||
return (
|
||||
<button
|
||||
className={classNames(styles.headerButton, className, {
|
||||
[styles.on]: on,
|
||||
})}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsButton(props) {
|
||||
return (
|
||||
<HeaderButton {...props}>
|
||||
<SettingsIcon />
|
||||
</HeaderButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutToggleButton({ layout, ...rest }) {
|
||||
return (
|
||||
<HeaderButton {...rest}>
|
||||
{layout === "spotlight" ? (
|
||||
<SpeakerIcon width={20} height={20} />
|
||||
) : (
|
||||
<GridIcon width={20} height={20} />
|
||||
)}
|
||||
</HeaderButton>
|
||||
);
|
||||
}
|
||||
@@ -1,129 +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.
|
||||
*/
|
||||
|
||||
.roomButton,
|
||||
.headerButton,
|
||||
.dropdownButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.roomButton {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50px;
|
||||
background-color: #394049;
|
||||
}
|
||||
|
||||
.roomButton:hover {
|
||||
background-color: #8d97a5;
|
||||
}
|
||||
|
||||
.roomButton:active {
|
||||
}
|
||||
|
||||
.roomButton.on {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.headerButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
.headerButton svg * {
|
||||
fill: #8e99a4;
|
||||
}
|
||||
|
||||
.headerButton:hover {
|
||||
background-color: #8d97a5;
|
||||
}
|
||||
|
||||
.headerButton:hover svg * {
|
||||
fill: #394049;
|
||||
}
|
||||
|
||||
.headerButton.on svg * {
|
||||
fill: #0dbd8b;
|
||||
}
|
||||
|
||||
.hangupButton,
|
||||
.hangupButton:hover {
|
||||
background-color: #ff5b55;
|
||||
}
|
||||
|
||||
.screenshareButton.on svg * {
|
||||
fill: #0dbd8b;
|
||||
}
|
||||
|
||||
.dropdownButtonContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdownButton {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 15px;
|
||||
background-color: #394049;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdownButton:hover {
|
||||
background-color: #8d97a5;
|
||||
}
|
||||
|
||||
.dropdownButton:hover svg * {
|
||||
fill: #8d97a5;
|
||||
}
|
||||
|
||||
.dropdownContainer {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(0, -100%);
|
||||
top: -5px;
|
||||
background-color: #394049;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdownContainer ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dropdownContainer li {
|
||||
padding: 12px;
|
||||
width: 200px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdownContainer li:hover {
|
||||
background-color: #8d97a5;
|
||||
}
|
||||
|
||||
.dropdownActiveItem {
|
||||
color: #0dbd8b;
|
||||
}
|
||||
57
src/SelectInput.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { useRef } from "react";
|
||||
import { HiddenSelect, useSelect } from "@react-aria/select";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { useSelectState } from "@react-stately/select";
|
||||
import { Popover } from "./Popover";
|
||||
import { ListBox } from "./ListBox";
|
||||
import styles from "./SelectInput.module.css";
|
||||
import classNames from "classnames";
|
||||
import { ReactComponent as ArrowDownIcon } from "./icons/ArrowDown.svg";
|
||||
|
||||
export function SelectInput(props) {
|
||||
const state = useSelectState(props);
|
||||
|
||||
const ref = useRef();
|
||||
const { labelProps, triggerProps, valueProps, menuProps } = useSelect(
|
||||
props,
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
const { buttonProps } = useButton(triggerProps, ref);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.selectInput, props.className)}>
|
||||
<h4 {...labelProps} className={styles.label}>
|
||||
{props.label}
|
||||
</h4>
|
||||
<HiddenSelect
|
||||
state={state}
|
||||
triggerRef={ref}
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
/>
|
||||
<button {...buttonProps} ref={ref} className={styles.selectTrigger}>
|
||||
<span {...valueProps} className={styles.selectedItem}>
|
||||
{state.selectedItem
|
||||
? state.selectedItem.rendered
|
||||
: "Select an option"}
|
||||
</span>
|
||||
<ArrowDownIcon />
|
||||
</button>
|
||||
{state.isOpen && (
|
||||
<Popover
|
||||
isOpen={state.isOpen}
|
||||
onClose={state.close}
|
||||
className={styles.popover}
|
||||
>
|
||||
<ListBox
|
||||
{...menuProps}
|
||||
state={state}
|
||||
optionClassName={styles.option}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
src/SelectInput.module.css
Normal file
@@ -0,0 +1,42 @@
|
||||
.selectInput {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 28px;
|
||||
max-width: 444px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.selectTrigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
background-color: var(--bgColor1);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
font-size: 15px;
|
||||
color: var(--textColor1);
|
||||
height: 40px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selectedItem {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.popover {
|
||||
position: absolute;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
95
src/SettingsModal.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import { Modal } from "./Modal";
|
||||
import styles from "./SettingsModal.module.css";
|
||||
import { TabContainer, TabItem } from "./Tabs";
|
||||
import { ReactComponent as AudioIcon } from "./icons/Audio.svg";
|
||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||
import { ReactComponent as DeveloperIcon } from "./icons/Developer.svg";
|
||||
import { SelectInput } from "./SelectInput";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { useMediaHandler } from "./useMediaHandler";
|
||||
import { FieldRow, InputField } from "./Input";
|
||||
|
||||
export function SettingsModal({
|
||||
client,
|
||||
setShowInspector,
|
||||
showInspector,
|
||||
...rest
|
||||
}) {
|
||||
const {
|
||||
audioInput,
|
||||
audioInputs,
|
||||
setAudioInput,
|
||||
videoInput,
|
||||
videoInputs,
|
||||
setVideoInput,
|
||||
} = useMediaHandler(client);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Settings"
|
||||
isDismissable
|
||||
mobileFullScreen
|
||||
className={styles.settingsModal}
|
||||
{...rest}
|
||||
>
|
||||
<TabContainer className={styles.tabContainer}>
|
||||
<TabItem
|
||||
title={
|
||||
<>
|
||||
<AudioIcon width={16} height={16} />
|
||||
<span>Audio</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SelectInput
|
||||
label="Microphone"
|
||||
selectedKey={audioInput}
|
||||
onSelectionChange={setAudioInput}
|
||||
>
|
||||
{audioInputs.map(({ deviceId, label }) => (
|
||||
<Item key={deviceId}>{label}</Item>
|
||||
))}
|
||||
</SelectInput>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
title={
|
||||
<>
|
||||
<VideoIcon width={16} height={16} />
|
||||
<span>Video</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SelectInput
|
||||
label="Webcam"
|
||||
selectedKey={videoInput}
|
||||
onSelectionChange={setVideoInput}
|
||||
>
|
||||
{videoInputs.map(({ deviceId, label }) => (
|
||||
<Item key={deviceId}>{label}</Item>
|
||||
))}
|
||||
</SelectInput>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
title={
|
||||
<>
|
||||
<DeveloperIcon width={16} height={16} />
|
||||
<span>Developer</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="showInspector"
|
||||
name="inspector"
|
||||
label="Show Call Inspector"
|
||||
type="checkbox"
|
||||
checked={showInspector}
|
||||
onChange={(e) => setShowInspector(e.target.checked)}
|
||||
/>
|
||||
</FieldRow>
|
||||
</TabItem>
|
||||
</TabContainer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
8
src/SettingsModal.module.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.settingsModal {
|
||||
width: 774px;
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
margin: 27px 16px;
|
||||
}
|
||||
53
src/Tabs.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useTabList, useTab, useTabPanel } from "@react-aria/tabs";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { useTabListState } from "@react-stately/tabs";
|
||||
import styles from "./Tabs.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function TabContainer(props) {
|
||||
const state = useTabListState(props);
|
||||
const ref = useRef();
|
||||
const { tabListProps } = useTabList(props, state, ref);
|
||||
return (
|
||||
<div className={classNames(styles.tabContainer, props.className)}>
|
||||
<ul {...tabListProps} ref={ref} className={styles.tabList}>
|
||||
{[...state.collection].map((item) => (
|
||||
<Tab key={item.key} item={item} state={state} />
|
||||
))}
|
||||
</ul>
|
||||
<TabPanel key={state.selectedItem?.key} state={state} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Tab({ item, state }) {
|
||||
const { key, rendered } = item;
|
||||
const ref = useRef();
|
||||
const { tabProps } = useTab({ key }, state, ref);
|
||||
|
||||
return (
|
||||
<li
|
||||
{...tabProps}
|
||||
ref={ref}
|
||||
className={classNames(styles.tab, {
|
||||
[styles.selected]: state.selectedKey === key,
|
||||
[styles.disabled]: state.disabledKeys.has(key),
|
||||
})}
|
||||
>
|
||||
{rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function TabPanel({ state, ...props }) {
|
||||
const ref = useRef();
|
||||
const { tabPanelProps } = useTabPanel(props, state, ref);
|
||||
return (
|
||||
<div {...tabPanelProps} ref={ref} className={styles.tabPanel}>
|
||||
{state.selectedItem?.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const TabItem = Item;
|
||||
62
src/Tabs.module.css
Normal file
@@ -0,0 +1,62 @@
|
||||
.tabContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tabList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
max-width: 190px;
|
||||
min-width: fit-content;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab > * {
|
||||
color: var(--textColor4);
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.tab svg * {
|
||||
fill: var(--textColor4);
|
||||
}
|
||||
|
||||
.tab > :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tab.selected {
|
||||
background-color: #0dbd8b;
|
||||
}
|
||||
|
||||
.tab.selected * {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tab.selected svg * {
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
.tab.disabled {
|
||||
}
|
||||
|
||||
.tabPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 0 40px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
99
src/UserMenu.jsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { ButtonTooltip, Button } from "./button";
|
||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
||||
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
||||
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
||||
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
|
||||
import styles from "./UserMenu.module.css";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { Menu } from "./Menu";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { useClient, useDisplayName } from "./ConferenceCallManagerHooks";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { ProfileModal } from "./ProfileModal";
|
||||
|
||||
export function UserMenu() {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const { isGuest, isPasswordlessUser, logout, userName, client } = useClient();
|
||||
const { displayName } = useDisplayName(client);
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
|
||||
const onAction = useCallback(
|
||||
(value) => {
|
||||
switch (value) {
|
||||
case "user":
|
||||
modalState.open();
|
||||
break;
|
||||
case "logout":
|
||||
logout();
|
||||
break;
|
||||
case "login":
|
||||
history.push("/login", { state: { from: location } });
|
||||
break;
|
||||
case "register":
|
||||
history.push("/register", { state: { from: location } });
|
||||
break;
|
||||
}
|
||||
},
|
||||
[history, location, logout, modalState]
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const arr = [
|
||||
{
|
||||
key: "user",
|
||||
icon: UserIcon,
|
||||
label: displayName || userName,
|
||||
},
|
||||
];
|
||||
|
||||
if (isGuest || isPasswordlessUser) {
|
||||
arr.push({
|
||||
key: "login",
|
||||
label: "Sign In",
|
||||
icon: LoginIcon,
|
||||
});
|
||||
}
|
||||
|
||||
if (isGuest) {
|
||||
arr.push({
|
||||
key: "register",
|
||||
label: "Register",
|
||||
icon: LoginIcon,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isGuest) {
|
||||
arr.push({
|
||||
key: "logout",
|
||||
label: "Sign Out",
|
||||
icon: LogoutIcon,
|
||||
});
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [isGuest, isPasswordlessUser, userName, displayName]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
<Button variant="icon" className={styles.userButton}>
|
||||
<ButtonTooltip>Profile</ButtonTooltip>
|
||||
<UserIcon />
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Menu {...props} label="User menu" onAction={onAction}>
|
||||
{items.map(({ key, icon: Icon, label }) => (
|
||||
<Item key={key} textValue={label}>
|
||||
<Icon />
|
||||
<span>{label}</span>
|
||||
</Item>
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</PopoverMenuTrigger>
|
||||
{modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
src/UserMenu.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.userButton svg * {
|
||||
fill: var(--textColor1);
|
||||
}
|
||||
125
src/button/Button.jsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import styles from "./Button.module.css";
|
||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||
import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
|
||||
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
|
||||
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
||||
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { useObjectRef } from "@react-aria/utils";
|
||||
|
||||
export const variantToClassName = {
|
||||
default: [styles.button],
|
||||
toolbar: [styles.toolbarButton],
|
||||
icon: [styles.iconButton],
|
||||
secondary: [styles.secondary],
|
||||
copy: [styles.copyButton],
|
||||
iconCopy: [styles.iconCopyButton],
|
||||
};
|
||||
|
||||
export const sizeToClassName = {
|
||||
lg: [styles.lg],
|
||||
};
|
||||
|
||||
export const Button = forwardRef(
|
||||
(
|
||||
{
|
||||
variant = "default",
|
||||
size,
|
||||
on,
|
||||
off,
|
||||
iconStyle,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const buttonRef = useObjectRef(ref);
|
||||
const { buttonProps } = useButton(rest, buttonRef);
|
||||
|
||||
// TODO: react-aria's useButton hook prevents form submission via keyboard
|
||||
// Remove the hack below after this is merged https://github.com/adobe/react-spectrum/pull/904
|
||||
let filteredButtonProps = buttonProps;
|
||||
|
||||
if (rest.type === "submit" && !rest.onPress) {
|
||||
const { onKeyDown, onKeyUp, ...filtered } = buttonProps;
|
||||
filteredButtonProps = filtered;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
variantToClassName[variant],
|
||||
sizeToClassName[size],
|
||||
styles[iconStyle],
|
||||
className,
|
||||
{
|
||||
[styles.on]: on,
|
||||
[styles.off]: off,
|
||||
}
|
||||
)}
|
||||
{...filteredButtonProps}
|
||||
ref={buttonRef}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export function ButtonTooltip({ className, children }) {
|
||||
return (
|
||||
<div className={classNames(styles.buttonTooltip, className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MicButton({ muted, ...rest }) {
|
||||
return (
|
||||
<Button variant="toolbar" {...rest} off={muted}>
|
||||
<ButtonTooltip>
|
||||
{muted ? "Unmute microphone" : "Mute microphone"}
|
||||
</ButtonTooltip>
|
||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function VideoButton({ muted, ...rest }) {
|
||||
return (
|
||||
<Button variant="toolbar" {...rest} off={muted}>
|
||||
<ButtonTooltip>
|
||||
{muted ? "Turn on camera" : "Turn off camera"}
|
||||
</ButtonTooltip>
|
||||
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScreenshareButton({ enabled, className, ...rest }) {
|
||||
return (
|
||||
<Button variant="toolbar" {...rest} on={enabled}>
|
||||
<ButtonTooltip>
|
||||
{enabled ? "Stop sharing screen" : "Share screen"}
|
||||
</ButtonTooltip>
|
||||
<ScreenshareIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function HangupButton({ className, ...rest }) {
|
||||
return (
|
||||
<Button
|
||||
variant="toolbar"
|
||||
className={classNames(styles.hangupButton, className)}
|
||||
{...rest}
|
||||
>
|
||||
<ButtonTooltip>Leave</ButtonTooltip>
|
||||
<HangupIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
179
src/button/Button.module.css
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.button,
|
||||
.toolbarButton,
|
||||
.iconButton,
|
||||
.iconCopyButton,
|
||||
.secondary,
|
||||
.copyButton {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.secondary,
|
||||
.button,
|
||||
.copyButton {
|
||||
padding: 7px 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #fff;
|
||||
background-color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.toolbarButton {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50px;
|
||||
background-color: var(--bgColor2);
|
||||
}
|
||||
|
||||
.toolbarButton:hover {
|
||||
background-color: var(--bgColor4);
|
||||
}
|
||||
|
||||
.toolbarButton.on,
|
||||
.toolbarButton.off {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.iconButton:not(.stroke) svg * {
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
.iconButton:not(.stroke):hover svg * {
|
||||
fill: #0dbd8b;
|
||||
}
|
||||
|
||||
.iconButton.on:not(.stroke) svg * {
|
||||
fill: #0dbd8b;
|
||||
}
|
||||
|
||||
.iconButton.on.stroke svg * {
|
||||
stroke: #0dbd8b;
|
||||
}
|
||||
|
||||
.hangupButton,
|
||||
.hangupButton:hover {
|
||||
background-color: #ff5b55;
|
||||
}
|
||||
|
||||
.toolbarButton.on svg * {
|
||||
fill: #0dbd8b;
|
||||
}
|
||||
|
||||
.toolbarButton.off svg * {
|
||||
fill: #21262c;
|
||||
}
|
||||
|
||||
.buttonTooltip {
|
||||
display: none;
|
||||
background-color: var(--bgColor2);
|
||||
position: absolute;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
color: var(--textColor1);
|
||||
border-radius: 8px;
|
||||
max-width: 135px;
|
||||
width: max-content;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.buttonTooltip.bottomRight {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.toolbarButton:hover .buttonTooltip {
|
||||
display: flex;
|
||||
bottom: calc(100% + 6px);
|
||||
}
|
||||
|
||||
.iconButton:hover .buttonTooltip {
|
||||
display: flex;
|
||||
top: calc(100% + 6px);
|
||||
}
|
||||
|
||||
.secondary,
|
||||
.copyButton {
|
||||
color: #0dbd8b;
|
||||
border: 2px solid #0dbd8b;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
transition: border-color 250ms, background-color 250ms;
|
||||
}
|
||||
|
||||
.copyButton span {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.copyButton svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.copyButton:not(.on) svg * {
|
||||
fill: #0dbd8b;
|
||||
}
|
||||
|
||||
.copyButton.on {
|
||||
border-color: transparent;
|
||||
background-color: #0dbd8b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.copyButton.on svg * {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.iconCopyButton svg * {
|
||||
fill: var(--textColor3);
|
||||
}
|
||||
|
||||
.iconCopyButton:hover svg * {
|
||||
fill: #0dbd8b;
|
||||
}
|
||||
|
||||
.iconCopyButton.on svg *,
|
||||
.iconCopyButton.on:hover svg * {
|
||||
fill: transparent;
|
||||
stroke: #0dbd8b;
|
||||
}
|
||||
|
||||
.lg {
|
||||
height: 40px;
|
||||
}
|
||||
39
src/button/CopyButton.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useCallback } from "react";
|
||||
import useClipboard from "react-use-clipboard";
|
||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||
import { ReactComponent as CopyIcon } from "../icons/Copy.svg";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export function CopyButton({
|
||||
value,
|
||||
children,
|
||||
onClassName,
|
||||
variant,
|
||||
copiedMessage,
|
||||
...rest
|
||||
}) {
|
||||
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...rest}
|
||||
variant={variant === "icon" ? "iconCopy" : "copy"}
|
||||
on={isCopied}
|
||||
onClassName={onClassName}
|
||||
onPress={setCopied}
|
||||
iconStyle={isCopied ? "stroke" : "fill"}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
{variant !== "icon" && <span>{copiedMessage || "Copied!"}</span>}
|
||||
<CheckIcon />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{variant !== "icon" && <span>{children || value}</span>}
|
||||
<CopyIcon />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
19
src/button/LinkButton.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { variantToClassName, sizeToClassName } from "./Button";
|
||||
|
||||
export function LinkButton({ className, variant, size, children, ...rest }) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
variantToClassName[variant || "secondary"],
|
||||
sizeToClassName[size],
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
3
src/button/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./Button";
|
||||
export * from "./CopyButton";
|
||||
export * from "./LinkButton";
|
||||
4
src/icons/AddUser.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.8914 9.0784C18.5132 9.0784 18.2066 8.777 18.2066 8.4052V5.71251H15.4678C15.0896 5.71251 14.783 5.41111 14.783 5.03931C14.783 4.66751 15.0896 4.36611 15.4678 4.36611H18.2066V1.6732C18.2066 1.3014 18.5132 1 18.8914 1C19.2696 1 19.5761 1.3014 19.5761 1.6732V4.36611H22.3153C22.6934 4.36611 23 4.66751 23 5.03931C23 5.41111 22.6934 5.71251 22.3153 5.71251H19.5761V8.4052C19.5761 8.777 19.2696 9.0784 18.8914 9.0784Z" fill="#ffffff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5002 11.5094C19.8949 11.5094 20.281 11.4736 20.6555 11.4051C20.7505 11.9558 20.8 12.5221 20.8 13.1C20.8 18.5676 16.3676 23 10.9 23C5.43238 23 1 18.5676 1 13.1C1 7.63238 5.43238 3.2 10.9 3.2C11.7973 3.2 12.6666 3.31937 13.4931 3.54308C13.3177 4.11118 13.2234 4.71396 13.2234 5.33841C13.2234 8.74655 16.0336 11.5094 19.5002 11.5094ZM11.1308 21.4653C13.2927 21.4653 15.2551 20.6258 16.6993 19.2603C15.7939 17.0769 13.6417 15.5412 11.1308 15.5412C8.61988 15.5412 6.46768 17.0769 5.56225 19.2603C7.00643 20.6258 8.96888 21.4653 11.1308 21.4653ZM11.2799 7.81768C9.78854 7.9515 8.62035 9.18434 8.62035 10.6854C8.62035 12.2759 9.93179 13.5652 11.5495 13.5652C12.6276 13.5652 13.5696 12.9927 14.0781 12.1401C12.6655 11.1035 11.6462 9.57751 11.2799 7.81768Z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
10
src/icons/ArrowDown.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_965_9448)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.70711 5.36321C2.09763 4.97268 2.73182 4.97166 3.1236 5.36091L8.08934 10.2946L13.0391 5.34488C13.4296 4.95435 14.0638 4.95333 14.4556 5.34258C14.8474 5.73184 14.8484 6.36398 14.4579 6.75451L8.80101 12.4114C8.41049 12.8019 7.7763 12.8029 7.38452 12.4137L1.70939 6.77513C1.3176 6.38587 1.31658 5.75373 1.70711 5.36321Z" fill="#8E99A4"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_965_9448">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 633 B |
3
src/icons/ArrowLeft.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.6663 8H1.33301M1.33301 8L7.83301 14.5M1.33301 8L7.83301 1.5" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 258 B |
5
src/icons/Audio.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.97991 1.48403L4 4.80062L1 4.80062C0.447715 4.80062 0 5.24834 0 5.80062V10.2006C0 10.7529 0.447714 11.2006 0.999999 11.2006L4 11.2006L7.97991 14.5172C8.30557 14.7886 8.8 14.557 8.8 14.1331V1.86814C8.8 1.44422 8.30557 1.21265 7.97991 1.48403Z" fill="white"/>
|
||||
<path d="M14.1258 2.79107C13.8998 2.50044 13.4809 2.44808 13.1903 2.67413C12.9 2.89992 12.8475 3.3181 13.0726 3.6087L13.0731 3.60935L13.0738 3.61021L13.0829 3.62231C13.0917 3.63418 13.1059 3.65355 13.1248 3.68011C13.1625 3.73326 13.2187 3.81496 13.2872 3.92256C13.4243 4.13812 13.6097 4.45554 13.7955 4.85371C14.169 5.65407 14.5329 6.75597 14.5329 8.00036C14.5329 9.24475 14.169 10.3466 13.7955 11.147C13.6097 11.5452 13.4243 11.8626 13.2872 12.0782C13.2187 12.1858 13.1625 12.2675 13.1248 12.3206C13.1059 12.3472 13.0917 12.3665 13.0829 12.3784L13.0738 12.3905L13.0731 12.3914L13.0725 12.3921C12.8475 12.6827 12.9 13.1008 13.1903 13.3266C13.4809 13.5526 13.8998 13.5003 14.1258 13.2097L13.629 12.8232C14.1258 13.2096 14.1258 13.2097 14.1258 13.2097L14.1272 13.2079L14.1291 13.2055L14.1346 13.1982L14.1523 13.1748C14.1669 13.1552 14.187 13.1277 14.2119 13.0926C14.2617 13.0225 14.3305 12.9221 14.4121 12.794C14.5749 12.5381 14.7895 12.1698 15.0037 11.7109C15.4302 10.7969 15.8663 9.49883 15.8663 8.00036C15.8663 6.50189 15.4302 5.20379 15.0037 4.28987C14.7895 3.83089 14.5749 3.4626 14.4121 3.20673C14.3305 3.07862 14.2617 2.97818 14.2119 2.90811C14.187 2.87306 14.1669 2.84556 14.1523 2.82596L14.1346 2.80249L14.1291 2.79525L14.1272 2.79278L14.1264 2.79183C14.1264 2.79183 14.1258 2.79107 13.5996 3.20036L14.1258 2.79107Z" fill="white"/>
|
||||
<path d="M11.7264 5.19121C11.5004 4.90058 11.0815 4.84823 10.7909 5.07427C10.501 5.29973 10.4482 5.71698 10.6722 6.00752L10.6745 6.01057C10.6775 6.01457 10.6831 6.02223 10.691 6.03338C10.7069 6.05572 10.7318 6.09189 10.7628 6.14057C10.8249 6.23827 10.9103 6.38426 10.9961 6.56815C11.1696 6.93993 11.3335 7.44183 11.3335 8.00051C11.3335 8.55918 11.1696 9.06108 10.9961 9.43287C10.9103 9.61675 10.8249 9.76275 10.7628 9.86045C10.7318 9.90912 10.7069 9.94529 10.691 9.96763C10.6831 9.97879 10.6775 9.98645 10.6745 9.99044L10.6722 9.9935C10.4482 10.284 10.501 10.7013 10.7909 10.9267C11.0815 11.1528 11.5004 11.1004 11.7264 10.8098L11.2002 10.4005C11.7264 10.8098 11.7264 10.8098 11.7264 10.8098L11.7276 10.8083L11.7291 10.8064L11.7329 10.8014L11.7439 10.7868C11.7526 10.7751 11.7642 10.7593 11.7781 10.7396C11.806 10.7004 11.8436 10.6455 11.8876 10.5763C11.9755 10.4383 12.0901 10.2414 12.2043 9.99672C12.4308 9.51136 12.6669 8.81326 12.6669 8.00051C12.6669 7.18775 12.4308 6.48965 12.2043 6.0043C12.0901 5.75961 11.9755 5.56275 11.8876 5.42473C11.8436 5.35555 11.806 5.30065 11.7781 5.26138C11.7642 5.24173 11.7526 5.22596 11.7439 5.21422L11.7329 5.19964L11.7291 5.19465L11.7276 5.19274L11.727 5.19193C11.727 5.19193 11.7264 5.19121 11.2002 5.60051L11.7264 5.19121Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
3
src/icons/Close.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.2758 2.69628C20.8924 2.07969 20.8924 1.08 20.2758 0.463417C19.6592 -0.15317 18.6596 -0.15317 18.043 0.463417L10.8919 7.61447L3.74087 0.463417C3.12429 -0.15317 2.1246 -0.15317 1.50802 0.463417C0.891428 1.08 0.891428 2.07969 1.50802 2.69628L8.65907 9.84733L1.2017 17.3047C0.585111 17.9213 0.585111 18.921 1.2017 19.5376C1.81829 20.1541 2.81797 20.1541 3.43456 19.5376L10.8919 12.0802L18.3493 19.5375C18.9659 20.1541 19.9656 20.1541 20.5822 19.5375C21.1987 18.921 21.1987 17.9213 20.5822 17.3047L13.1248 9.84732L20.2758 2.69628Z" fill="#6F7882"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 660 B |
3
src/icons/Copy.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="24" viewBox="0 0 21 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.03125 3C4.3744 3 3.03125 4.34315 3.03125 6V13C3.03125 14.6569 4.3744 16 6.03125 16H7.07407V18C7.07407 19.6569 8.41722 21 10.0741 21H14.9683C16.6251 21 17.9683 19.6569 17.9683 18V11C17.9683 9.34315 16.6251 8 14.9683 8H13.9255V6C13.9255 4.34315 12.5823 3 10.9255 3H6.03125ZM11.9255 8V6C11.9255 5.44772 11.4777 5 10.9255 5H6.03125C5.47897 5 5.03125 5.44772 5.03125 6V13C5.03125 13.5523 5.47897 14 6.03125 14H7.07407V11C7.07407 9.34315 8.41722 8 10.0741 8H11.9255ZM9.07407 11C9.07407 10.4477 9.52179 10 10.0741 10H14.9683C15.5206 10 15.9683 10.4477 15.9683 11V18C15.9683 18.5523 15.5206 19 14.9683 19H10.0741C9.52179 19 9.07407 18.5523 9.07407 18V11Z" fill="#0DBD8B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 820 B |
3
src/icons/Developer.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3C2.23858 3 0 5.23858 0 8C0 10.7614 2.23858 13 5 13H11C13.7614 13 16 10.7614 16 8C16 5.23858 13.7614 3 11 3H5ZM8 8C8 9.65685 6.65685 11 5 11C3.34315 11 2 9.65685 2 8C2 6.34315 3.34315 5 5 5C6.65685 5 8 6.34315 8 8Z" fill="#A9B2BC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 388 B |
6
src/icons/Freedom.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="13" y="11" width="5" height="5" rx="1.16667" fill="white"/>
|
||||
<rect x="13" y="5" width="5" height="5" rx="1.16667" fill="white"/>
|
||||
<rect x="7" y="5" width="5" height="5" rx="1.16667" fill="white"/>
|
||||
<rect x="4" y="11" width="8" height="8" rx="1.14286" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 375 B |
@@ -1,11 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="9" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="9" y="9" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="17" y="9" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="1" y="17" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="9" y="17" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="17" y="17" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="1" y="1" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="9" y="1" width="6" height="6" rx="1" fill="white"/>
|
||||
<rect x="17" y="1" width="6" height="6" rx="1" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 658 B |
3
src/icons/Login.svg
Normal file
|
After Width: | Height: | Size: 24 KiB |
11
src/icons/Logo.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="199" height="30" viewBox="0 0 199 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="15" r="13" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 30C23.2843 30 30 23.2843 30 15C30 6.71573 23.2843 0 15 0C6.71573 0 0 6.71573 0 15C0 23.2843 6.71573 30 15 30ZM12.2579 6.98923C12.2579 6.38376 12.7497 5.89292 13.3565 5.89292C17.4687 5.89292 20.8024 9.21967 20.8024 13.3234C20.8024 13.9289 20.3106 14.4197 19.7038 14.4197C19.0971 14.4197 18.6052 13.9289 18.6052 13.3234C18.6052 10.4306 16.2553 8.08554 13.3565 8.08554C12.7497 8.08554 12.2579 7.59471 12.2579 6.98923ZM24.1066 13.3235C24.1066 12.7181 23.6148 12.2272 23.008 12.2272C22.4013 12.2272 21.9094 12.7181 21.9094 13.3235C21.9094 16.2163 19.5595 18.5614 16.6607 18.5614C16.0539 18.5614 15.5621 19.0523 15.5621 19.6577C15.5621 20.2632 16.0539 20.754 16.6607 20.754C20.7729 20.754 24.1066 17.4273 24.1066 13.3235ZM17.7601 23.011C17.7601 23.6164 17.2682 24.1073 16.6615 24.1073C12.5492 24.1073 9.21553 20.7805 9.21553 16.6768C9.21553 16.0713 9.70739 15.5805 10.3141 15.5805C10.9209 15.5805 11.4127 16.0713 11.4127 16.6768C11.4127 19.5696 13.7627 21.9146 16.6615 21.9146C17.2682 21.9146 17.7601 22.4055 17.7601 23.011ZM5.89281 16.6769C5.89281 17.2824 6.38467 17.7732 6.9914 17.7732C7.59813 17.7732 8.08999 17.2824 8.08999 16.6769C8.08999 13.7841 10.4399 11.439 13.3388 11.439C13.9455 11.439 14.4373 10.9482 14.4373 10.3427C14.4373 9.73722 13.9455 9.24639 13.3388 9.24639C9.22647 9.24639 5.89281 12.5731 5.89281 16.6769Z" fill="#0DBD8B"/>
|
||||
<path d="M53.5406 17.2581H42.8052C42.9321 18.3815 43.3398 19.2783 44.0283 19.9487C44.7168 20.601 45.6227 20.9272 46.7461 20.9272C47.4889 20.9272 48.1593 20.746 48.7573 20.3836C49.3552 20.0212 49.781 19.532 50.0346 18.916H53.296C52.8611 20.3474 52.0458 21.507 50.85 22.3948C49.6722 23.2645 48.2771 23.6993 46.6645 23.6993C44.5628 23.6993 42.8596 23.0017 41.5551 21.6066C40.2686 20.2115 39.6254 18.4449 39.6254 16.3069C39.6254 14.2232 40.2777 12.4748 41.5822 11.0615C42.8868 9.64824 44.5718 8.94161 46.6374 8.94161C48.7029 8.94161 50.3698 9.63918 51.6381 11.0343C52.9246 12.4113 53.5678 14.1507 53.5678 16.2525L53.5406 17.2581ZM46.6374 11.5779C45.6227 11.5779 44.7802 11.8768 44.1098 12.4748C43.4394 13.0727 43.0227 13.8699 42.8596 14.8664H50.3608C50.2158 13.8699 49.8172 13.0727 49.1649 12.4748C48.5127 11.8768 47.6701 11.5779 46.6374 11.5779Z" fill="white"/>
|
||||
<path d="M55.7934 19.1606V2.9896H59.0276V19.2149C59.0276 19.9397 59.4262 20.3021 60.2234 20.3021L60.7942 20.2749V23.346C60.4862 23.4004 60.16 23.4275 59.8158 23.4275C58.4206 23.4275 57.3969 23.0742 56.7446 22.3676C56.1105 21.661 55.7934 20.592 55.7934 19.1606Z" fill="white"/>
|
||||
<path d="M75.8564 17.2581H65.121C65.2478 18.3815 65.6555 19.2783 66.344 19.9487C67.0325 20.601 67.9385 20.9272 69.0618 20.9272C69.8047 20.9272 70.4751 20.746 71.073 20.3836C71.6709 20.0212 72.0967 19.532 72.3504 18.916H75.6118C75.1769 20.3474 74.3616 21.507 73.1657 22.3948C71.988 23.2645 70.5929 23.6993 68.9803 23.6993C66.8785 23.6993 65.1754 23.0017 63.8708 21.6066C62.5844 20.2115 61.9412 18.4449 61.9412 16.3069C61.9412 14.2232 62.5935 12.4748 63.898 11.0615C65.2026 9.64824 66.8876 8.94161 68.9531 8.94161C71.0187 8.94161 72.6856 9.63918 73.9539 11.0343C75.2403 12.4113 75.8835 14.1507 75.8835 16.2525L75.8564 17.2581ZM68.9531 11.5779C67.9385 11.5779 67.096 11.8768 66.4256 12.4748C65.7552 13.0727 65.3384 13.8699 65.1754 14.8664H72.6765C72.5316 13.8699 72.133 13.0727 71.4807 12.4748C70.8284 11.8768 69.9859 11.5779 68.9531 11.5779Z" fill="white"/>
|
||||
<path d="M90.448 15.2741V23.3732H87.2138V14.9208C87.2138 12.7828 86.326 11.7138 84.5504 11.7138C83.5901 11.7138 82.82 12.0218 82.2402 12.6378C81.6786 13.2539 81.3977 14.0964 81.3977 15.1654V23.3732H78.1635V9.26775H81.1531V11.143C81.4974 10.5089 82.0228 9.98344 82.7295 9.56671C83.4361 9.14998 84.3148 8.94161 85.3657 8.94161C87.3226 8.94161 88.7358 9.68448 89.6055 11.1702C90.8013 9.68448 92.3958 8.94161 94.3889 8.94161C96.0377 8.94161 97.306 9.45799 98.1938 10.4908C99.0816 11.5054 99.5255 12.8462 99.5255 14.5131V23.3732H96.2913V14.9208C96.2913 12.7828 95.4035 11.7138 93.6279 11.7138C92.6495 11.7138 91.8704 12.0309 91.2906 12.665C90.7289 13.281 90.448 14.1507 90.448 15.2741Z" fill="white"/>
|
||||
<path d="M115.61 17.2581H104.874C105.001 18.3815 105.409 19.2783 106.097 19.9487C106.786 20.601 107.692 20.9272 108.815 20.9272C109.558 20.9272 110.228 20.746 110.826 20.3836C111.424 20.0212 111.85 19.532 112.104 18.916H115.365C114.93 20.3474 114.115 21.507 112.919 22.3948C111.741 23.2645 110.346 23.6993 108.734 23.6993C106.632 23.6993 104.929 23.0017 103.624 21.6066C102.338 20.2115 101.694 18.4449 101.694 16.3069C101.694 14.2232 102.347 12.4748 103.651 11.0615C104.956 9.64824 106.641 8.94161 108.706 8.94161C110.772 8.94161 112.439 9.63918 113.707 11.0343C114.994 12.4113 115.637 14.1507 115.637 16.2525L115.61 17.2581ZM108.706 11.5779C107.692 11.5779 106.849 11.8768 106.179 12.4748C105.508 13.0727 105.092 13.8699 104.929 14.8664H112.43C112.285 13.8699 111.886 13.0727 111.234 12.4748C110.582 11.8768 109.739 11.5779 108.706 11.5779Z" fill="white"/>
|
||||
<path d="M120.906 9.26775V11.143C121.233 10.527 121.767 10.0106 122.51 9.59388C123.271 9.15903 124.186 8.94161 125.255 8.94161C126.922 8.94161 128.208 9.44893 129.114 10.4636C130.038 11.4782 130.5 12.8281 130.5 14.5131V23.3732H127.266V14.9208C127.266 13.9243 127.031 13.1452 126.559 12.5835C126.106 12.0037 125.409 11.7138 124.467 11.7138C123.434 11.7138 122.619 12.0218 122.021 12.6378C121.441 13.2539 121.151 14.1054 121.151 15.1926V23.3732H117.917V9.26775H120.906Z" fill="white"/>
|
||||
<path d="M139.946 20.4923V23.2916C139.547 23.4004 138.985 23.4547 138.261 23.4547C135.507 23.4547 134.13 22.0686 134.13 19.2965V11.8497H131.983V9.26775H134.13V5.5987H137.364V9.26775H140V11.8497H137.364V18.9703C137.364 20.0756 137.889 20.6282 138.94 20.6282L139.946 20.4923Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
11
src/icons/LogoLarge.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="300" height="66" viewBox="0 0 300 66" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="32.6901" cy="32.6904" r="23.496" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.6137 65.2274C50.6257 65.2274 65.2274 50.6257 65.2274 32.6137C65.2274 14.6016 50.6257 0 32.6137 0C14.6016 0 0 14.6016 0 32.6137C0 50.6257 14.6016 65.2274 32.6137 65.2274ZM26.6511 15.1961C26.6511 13.8796 27.7205 12.8124 29.0397 12.8124C37.9808 12.8124 45.2291 20.0456 45.2291 28.9681C45.2291 30.2846 44.1596 31.3518 42.8405 31.3518C41.5213 31.3518 40.4519 30.2846 40.4519 28.9681C40.4519 22.6785 35.3425 17.5797 29.0397 17.5797C27.7205 17.5797 26.6511 16.5125 26.6511 15.1961ZM52.4139 28.9683C52.4139 27.6518 51.3445 26.5846 50.0253 26.5846C48.7061 26.5846 47.6367 27.6518 47.6367 28.9683C47.6367 35.2579 42.5274 40.3567 36.2246 40.3567C34.9054 40.3567 33.836 41.4239 33.836 42.7404C33.836 44.0568 34.9054 45.124 36.2246 45.124C45.1657 45.124 52.4139 37.8909 52.4139 28.9683ZM38.6134 50.031C38.6134 51.3475 37.544 52.4146 36.2248 52.4146C27.2837 52.4146 20.0355 45.1815 20.0355 36.2589C20.0355 34.9425 21.1049 33.8753 22.4241 33.8753C23.7432 33.8753 24.8127 34.9425 24.8127 36.2589C24.8127 42.5486 29.922 47.6474 36.2248 47.6474C37.544 47.6474 38.6134 48.7146 38.6134 50.031ZM12.8113 36.2589C12.8113 37.5754 13.8808 38.6426 15.1999 38.6426C16.5191 38.6426 17.5885 37.5754 17.5885 36.2589C17.5885 29.9693 22.6979 24.8705 29.0007 24.8705C30.3198 24.8705 31.3893 23.8033 31.3893 22.4869C31.3893 21.1704 30.3198 20.1032 29.0007 20.1032C20.0595 20.1032 12.8113 27.3364 12.8113 36.2589Z" fill="#0DBD8B"/>
|
||||
<path d="M114.73 36.9816H91.7256C91.9974 39.3888 92.871 41.3107 94.3464 42.7472C95.8217 44.1449 97.763 44.8438 100.17 44.8438C101.762 44.8438 103.199 44.4555 104.48 43.679C105.761 42.9025 106.674 41.8542 107.217 40.5341H114.206C113.274 43.6014 111.527 46.0862 108.964 47.9887C106.441 49.8523 103.451 50.7842 99.9955 50.7842C95.4917 50.7842 91.8421 49.2894 89.0466 46.2998C86.29 43.3102 84.9117 39.5247 84.9117 34.9432C84.9117 30.4783 86.3094 26.7316 89.1049 23.7032C91.9003 20.6747 95.5111 19.1605 99.9373 19.1605C104.363 19.1605 107.935 20.6553 110.653 23.6449C113.41 26.5957 114.788 30.323 114.788 34.8267L114.73 36.9816ZM99.9373 24.8097C97.763 24.8097 95.9576 25.4503 94.5211 26.7316C93.0845 28.0128 92.1915 29.7212 91.8421 31.8566H107.916C107.605 29.7212 106.751 28.0128 105.353 26.7316C103.956 25.4503 102.15 24.8097 99.9373 24.8097Z" fill="white"/>
|
||||
<path d="M119.557 41.0583V6.40625H126.488V41.1748C126.488 42.7278 127.342 43.5043 129.05 43.5043L130.273 43.4461V50.0271C129.613 50.1435 128.914 50.2018 128.177 50.2018C125.187 50.2018 122.993 49.4447 121.596 47.9305C120.237 46.4163 119.557 44.1255 119.557 41.0583Z" fill="white"/>
|
||||
<path d="M162.549 36.9816H139.545C139.817 39.3888 140.69 41.3107 142.166 42.7472C143.641 44.1449 145.582 44.8438 147.99 44.8438C149.582 44.8438 151.018 44.4555 152.299 43.679C153.581 42.9025 154.493 41.8542 155.037 40.5341H162.025C161.093 43.6014 159.346 46.0862 156.784 47.9887C154.26 49.8523 151.27 50.7842 147.815 50.7842C143.311 50.7842 139.662 49.2894 136.866 46.2998C134.109 43.3102 132.731 39.5247 132.731 34.9432C132.731 30.4783 134.129 26.7316 136.924 23.7032C139.72 20.6747 143.331 19.1605 147.757 19.1605C152.183 19.1605 155.755 20.6553 158.473 23.6449C161.229 26.5957 162.608 30.323 162.608 34.8267L162.549 36.9816ZM147.757 24.8097C145.582 24.8097 143.777 25.4503 142.341 26.7316C140.904 28.0128 140.011 29.7212 139.662 31.8566H155.735C155.425 29.7212 154.571 28.0128 153.173 26.7316C151.775 25.4503 149.97 24.8097 147.757 24.8097Z" fill="white"/>
|
||||
<path d="M193.817 32.7302V50.0853H186.887V31.973C186.887 27.3916 184.984 25.1009 181.179 25.1009C179.122 25.1009 177.471 25.7609 176.229 27.081C175.025 28.4011 174.424 30.2065 174.424 32.4972V50.0853H167.493V19.8594H173.9V23.8779C174.637 22.519 175.763 21.393 177.277 20.5C178.792 19.607 180.675 19.1605 182.927 19.1605C187.12 19.1605 190.148 20.7524 192.012 23.9361C194.574 20.7524 197.991 19.1605 202.262 19.1605C205.795 19.1605 208.513 20.2671 210.415 22.4801C212.318 24.6544 213.269 27.5275 213.269 31.0995V50.0853H206.338V31.973C206.338 27.3916 204.436 25.1009 200.631 25.1009C198.535 25.1009 196.865 25.7803 195.623 27.1392C194.419 28.4593 193.817 30.323 193.817 32.7302Z" fill="white"/>
|
||||
<path d="M247.735 36.9816H224.731C225.002 39.3888 225.876 41.3107 227.351 42.7472C228.827 44.1449 230.768 44.8438 233.175 44.8438C234.767 44.8438 236.204 44.4555 237.485 43.679C238.766 42.9025 239.678 41.8542 240.222 40.5341H247.211C246.279 43.6014 244.532 46.0862 241.969 47.9887C239.446 49.8523 236.456 50.7842 233 50.7842C228.497 50.7842 224.847 49.2894 222.052 46.2998C219.295 43.3102 217.917 39.5247 217.917 34.9432C217.917 30.4783 219.314 26.7316 222.11 23.7032C224.905 20.6747 228.516 19.1605 232.942 19.1605C237.368 19.1605 240.94 20.6553 243.658 23.6449C246.415 26.5957 247.793 30.323 247.793 34.8267L247.735 36.9816ZM232.942 24.8097C230.768 24.8097 228.963 25.4503 227.526 26.7316C226.089 28.0128 225.196 29.7212 224.847 31.8566H240.921C240.61 29.7212 239.756 28.0128 238.358 26.7316C236.961 25.4503 235.155 24.8097 232.942 24.8097Z" fill="white"/>
|
||||
<path d="M259.085 19.8594V23.8779C259.784 22.5578 260.929 21.4513 262.521 20.5583C264.152 19.6264 266.113 19.1605 268.403 19.1605C271.975 19.1605 274.732 20.2477 276.673 22.4219C278.653 24.5961 279.643 27.4887 279.643 31.0995V50.0853H272.713V31.973C272.713 29.8376 272.208 28.1681 271.199 26.9645C270.228 25.7221 268.733 25.1009 266.714 25.1009C264.501 25.1009 262.754 25.7609 261.473 27.081C260.23 28.4011 259.609 30.2259 259.609 32.5554V50.0853H252.679V19.8594H259.085Z" fill="white"/>
|
||||
<path d="M299.884 43.912V49.9106C299.029 50.1435 297.826 50.26 296.273 50.26C290.371 50.26 287.42 47.2898 287.42 41.3495V25.3921H282.82V19.8594H287.42V11.9972H294.351V19.8594H300V25.3921H294.351V40.6506C294.351 43.019 295.477 44.2032 297.729 44.2032L299.884 43.912Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
3
src/icons/Logout.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2139 2.44646L12 3.99992H5.4375C4.64359 3.99992 4 4.64352 4 5.43742V18.5624C4 19.3563 4.64359 19.9999 5.4375 19.9999H12L18.2139 21.5534C19.1211 21.7802 20 21.094 20 20.1588V3.84104C20 2.90584 19.1211 2.21964 18.2139 2.44646ZM12 17.9999V5.99992H6V17.9999H12Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 429 B |
5
src/icons/Overflow.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.33398 15C8.33398 16.3807 7.2147 17.5 5.83398 17.5C4.45327 17.5 3.33398 16.3807 3.33398 15C3.33398 13.6193 4.45327 12.5 5.83398 12.5C7.2147 12.5 8.33398 13.6193 8.33398 15Z" fill="white"/>
|
||||
<path d="M17.5002 15C17.5002 16.3807 16.381 17.5 15.0002 17.5C13.6195 17.5 12.5002 16.3807 12.5002 15C12.5002 13.6193 13.6195 12.5 15.0002 12.5C16.381 12.5 17.5002 13.6193 17.5002 15Z" fill="white"/>
|
||||
<path d="M24.1665 17.5C25.5472 17.5 26.6665 16.3807 26.6665 15C26.6665 13.6193 25.5472 12.5 24.1665 12.5C22.7858 12.5 21.6665 13.6193 21.6665 15C21.6665 16.3807 22.7858 17.5 24.1665 17.5Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 707 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0988 9.01625C20.3188 9.59375 20.8825 9.97875 21.5013 9.97875C22.3263 9.97875 23 10.6525 23 11.4775V12.5225C23 13.3475 22.3263 14.0213 21.5013 14.0213C20.8825 14.0213 20.3188 14.4062 20.0988 14.9838C20.0806 15.0292 20.0624 15.0753 20.0441 15.1218C19.9792 15.2863 19.9125 15.4555 19.8375 15.6163C19.5763 16.18 19.7 16.84 20.14 17.28C20.7313 17.8575 20.7313 18.8062 20.14 19.3975L19.3975 20.14C18.82 20.7313 17.8713 20.7313 17.28 20.14C16.8538 19.7 16.18 19.5763 15.6163 19.8375C15.41 19.9338 15.2037 20.0163 14.9838 20.0988C14.4062 20.3188 14.0213 20.8825 14.0213 21.5013C14.0213 22.3263 13.3475 23 12.5225 23H11.4775C10.6525 23 9.97875 22.3263 9.97875 21.5013C9.97875 20.8825 9.59375 20.3188 9.01625 20.0988C8.97079 20.0806 8.92466 20.0624 8.87815 20.044C8.71368 19.9792 8.54454 19.9125 8.38375 19.8375C7.82 19.5763 7.16 19.7 6.72 20.14C6.1425 20.7313 5.19375 20.7313 4.6025 20.14L3.86 19.3975C3.26875 18.82 3.26875 17.8713 3.86 17.28C4.3 16.8538 4.42375 16.18 4.1625 15.6163C4.06625 15.41 3.98375 15.2037 3.90125 14.9838C3.68125 14.4062 3.1175 14.0213 2.49875 14.0213C1.67375 14.0213 1 13.3475 1 12.5225V11.4775C1 10.6525 1.67375 9.97875 2.49875 9.97875C3.1175 9.97875 3.68125 9.59375 3.90125 9.01625C3.94464 8.87742 3.99897 8.74406 4.0539 8.60926C4.086 8.53046 4.11831 8.45117 4.14875 8.37C4.41 7.80625 4.28625 7.14625 3.84625 6.70625C3.255 6.12875 3.255 5.18 3.84625 4.58875L4.6025 3.86C5.18 3.26875 6.12875 3.26875 6.72 3.86C7.14625 4.3 7.82 4.42375 8.38375 4.1625C8.59 4.06625 8.79625 3.97 9.01625 3.90125C9.59375 3.68125 9.97875 3.1175 9.97875 2.49875C9.97875 1.67375 10.6525 1 11.4775 1H12.5225C13.3475 1 14.0213 1.67375 14.0213 2.49875C14.0213 3.13125 14.4062 3.68125 14.9838 3.90125C15.0292 3.91943 15.0753 3.93762 15.1218 3.95595C15.2863 4.02079 15.4555 4.08746 15.6163 4.1625C16.18 4.42375 16.84 4.3 17.28 3.86C17.8575 3.26875 18.8062 3.26875 19.3975 3.86L20.14 4.6025C20.7313 5.18 20.7313 6.12875 20.14 6.72C19.7 7.14625 19.5763 7.82 19.8375 8.38375C19.9338 8.59 20.0163 8.79625 20.0988 9.01625ZM12 17.5C8.96125 17.5 6.5 15.0387 6.5 12C6.5 8.96125 8.96125 6.5 12 6.5C15.0387 6.5 17.5 8.96125 17.5 12C17.5 15.0387 15.0387 17.5 12 17.5Z" fill="#8E99A4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0988 9.01625C20.3188 9.59375 20.8825 9.97875 21.5013 9.97875C22.3263 9.97875 23 10.6525 23 11.4775V12.5225C23 13.3475 22.3263 14.0213 21.5013 14.0213C20.8825 14.0213 20.3188 14.4062 20.0988 14.9838C20.0806 15.0292 20.0624 15.0753 20.0441 15.1218C19.9792 15.2863 19.9125 15.4555 19.8375 15.6163C19.5763 16.18 19.7 16.84 20.14 17.28C20.7313 17.8575 20.7313 18.8062 20.14 19.3975L19.3975 20.14C18.82 20.7313 17.8713 20.7313 17.28 20.14C16.8538 19.7 16.18 19.5763 15.6163 19.8375C15.41 19.9338 15.2037 20.0163 14.9838 20.0988C14.4062 20.3188 14.0213 20.8825 14.0213 21.5013C14.0213 22.3263 13.3475 23 12.5225 23H11.4775C10.6525 23 9.97875 22.3263 9.97875 21.5013C9.97875 20.8825 9.59375 20.3188 9.01625 20.0988C8.97079 20.0806 8.92466 20.0624 8.87815 20.044C8.71368 19.9792 8.54454 19.9125 8.38375 19.8375C7.82 19.5763 7.16 19.7 6.72 20.14C6.1425 20.7313 5.19375 20.7313 4.6025 20.14L3.86 19.3975C3.26875 18.82 3.26875 17.8713 3.86 17.28C4.3 16.8538 4.42375 16.18 4.1625 15.6163C4.06625 15.41 3.98375 15.2037 3.90125 14.9838C3.68125 14.4062 3.1175 14.0213 2.49875 14.0213C1.67375 14.0213 1 13.3475 1 12.5225V11.4775C1 10.6525 1.67375 9.97875 2.49875 9.97875C3.1175 9.97875 3.68125 9.59375 3.90125 9.01625C3.94464 8.87742 3.99897 8.74406 4.0539 8.60926C4.086 8.53046 4.11831 8.45117 4.14875 8.37C4.41 7.80625 4.28625 7.14625 3.84625 6.70625C3.255 6.12875 3.255 5.18 3.84625 4.58875L4.6025 3.86C5.18 3.26875 6.12875 3.26875 6.72 3.86C7.14625 4.3 7.82 4.42375 8.38375 4.1625C8.59 4.06625 8.79625 3.97 9.01625 3.90125C9.59375 3.68125 9.97875 3.1175 9.97875 2.49875C9.97875 1.67375 10.6525 1 11.4775 1H12.5225C13.3475 1 14.0213 1.67375 14.0213 2.49875C14.0213 3.13125 14.4062 3.68125 14.9838 3.90125C15.0292 3.91943 15.0753 3.93762 15.1218 3.95595C15.2863 4.02079 15.4555 4.08746 15.6163 4.1625C16.18 4.42375 16.84 4.3 17.28 3.86C17.8575 3.26875 18.8062 3.26875 19.3975 3.86L20.14 4.6025C20.7313 5.18 20.7313 6.12875 20.14 6.72C19.7 7.14625 19.5763 7.82 19.8375 8.38375C19.9338 8.59 20.0163 8.79625 20.0988 9.01625ZM12 17.5C8.96125 17.5 6.5 15.0387 6.5 12C6.5 8.96125 8.96125 6.5 12 6.5C15.0387 6.5 17.5 8.96125 17.5 12C17.5 15.0387 15.0387 17.5 12 17.5Z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 354 B |
@@ -1,4 +1,7 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.8914 9.0784C18.5132 9.0784 18.2066 8.777 18.2066 8.4052V5.71251H15.4678C15.0896 5.71251 14.783 5.41111 14.783 5.03931C14.783 4.66751 15.0896 4.36611 15.4678 4.36611H18.2066V1.6732C18.2066 1.3014 18.5132 1 18.8914 1C19.2696 1 19.5761 1.3014 19.5761 1.6732V4.36611H22.3153C22.6934 4.36611 23 4.66751 23 5.03931C23 5.41111 22.6934 5.71251 22.3153 5.71251H19.5761V8.4052C19.5761 8.777 19.2696 9.0784 18.8914 9.0784Z" fill="#8E99A4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5002 11.5094C19.8949 11.5094 20.281 11.4736 20.6555 11.4051C20.7505 11.9558 20.8 12.5221 20.8 13.1C20.8 18.5676 16.3676 23 10.9 23C5.43238 23 1 18.5676 1 13.1C1 7.63238 5.43238 3.2 10.9 3.2C11.7973 3.2 12.6666 3.31937 13.4931 3.54308C13.3177 4.11118 13.2234 4.71396 13.2234 5.33841C13.2234 8.74655 16.0336 11.5094 19.5002 11.5094ZM11.1308 21.4653C13.2927 21.4653 15.2551 20.6258 16.6993 19.2603C15.7939 17.0769 13.6417 15.5412 11.1308 15.5412C8.61988 15.5412 6.46768 17.0769 5.56225 19.2603C7.00643 20.6258 8.96888 21.4653 11.1308 21.4653ZM11.2799 7.81768C9.78854 7.9515 8.62035 9.18434 8.62035 10.6854C8.62035 12.2759 9.93179 13.5652 11.5495 13.5652C12.6276 13.5652 13.5696 12.9927 14.0781 12.1401C12.6655 11.1035 11.6462 9.57751 11.2799 7.81768Z" fill="#8E99A4"/>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="path-1-inside-1_898_2641" fill="white">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.1502 20.1214C15.3946 21.3074 13.2782 22 11 22C8.52367 22 6.23845 21.1817 4.4 19.8008C1.72821 17.794 0 14.5988 0 11C0 4.92487 4.92487 0 11 0C17.0751 0 22 4.92487 22 11C22 14.797 20.0762 18.1446 17.1502 20.1214ZM11 11.55C12.8225 11.55 14.3 9.94942 14.3 7.975C14.3 6.00058 12.8225 4.4 11 4.4C9.17746 4.4 7.7 6.00058 7.7 7.975C7.7 9.94942 9.17746 11.55 11 11.55ZM11 19.8C13.3782 19.8 15.536 18.8566 17.1197 17.3237C16.1403 14.9056 13.7693 13.2 11 13.2C8.23066 13.2 5.85969 14.9056 4.88028 17.3237C6.46399 18.8566 8.62183 19.8 11 19.8Z"/>
|
||||
</mask>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.1502 20.1214C15.3946 21.3074 13.2782 22 11 22C8.52367 22 6.23845 21.1817 4.4 19.8008C1.72821 17.794 0 14.5988 0 11C0 4.92487 4.92487 0 11 0C17.0751 0 22 4.92487 22 11C22 14.797 20.0762 18.1446 17.1502 20.1214ZM11 11.55C12.8225 11.55 14.3 9.94942 14.3 7.975C14.3 6.00058 12.8225 4.4 11 4.4C9.17746 4.4 7.7 6.00058 7.7 7.975C7.7 9.94942 9.17746 11.55 11 11.55ZM11 19.8C13.3782 19.8 15.536 18.8566 17.1197 17.3237C16.1403 14.9056 13.7693 13.2 11 13.2C8.23066 13.2 5.85969 14.9056 4.88028 17.3237C6.46399 18.8566 8.62183 19.8 11 19.8Z" fill="white"/>
|
||||
<path d="M17.1502 20.1214L17.9339 21.2814L17.1502 20.1214ZM4.4 19.8008L3.55919 20.9202H3.55919L4.4 19.8008ZM17.1197 17.3237L18.0934 18.3296L18.7717 17.6731L18.4173 16.7981L17.1197 17.3237ZM4.88028 17.3237L3.58268 16.7981L3.22829 17.6731L3.90659 18.3296L4.88028 17.3237ZM11 23.4C13.5662 23.4 15.9541 22.619 17.9339 21.2814L16.3665 18.9613C14.835 19.9959 12.9902 20.6 11 20.6V23.4ZM3.55919 20.9202C5.63176 22.477 8.21011 23.4 11 23.4V20.6C8.83723 20.6 6.84514 19.8865 5.24081 18.6814L3.55919 20.9202ZM-1.4 11C-1.4 15.0577 0.55052 18.6603 3.55919 20.9202L5.24081 18.6814C2.90591 16.9276 1.4 14.1399 1.4 11H-1.4ZM11 -1.4C4.15167 -1.4 -1.4 4.15167 -1.4 11H1.4C1.4 5.69807 5.69807 1.4 11 1.4V-1.4ZM23.4 11C23.4 4.15167 17.8483 -1.4 11 -1.4V1.4C16.3019 1.4 20.6 5.69807 20.6 11H23.4ZM17.9339 21.2814C21.2288 19.0554 23.4 15.2815 23.4 11H20.6C20.6 14.3124 18.9236 17.2337 16.3665 18.9613L17.9339 21.2814ZM12.9 7.975C12.9 9.28384 11.9459 10.15 11 10.15V12.95C13.6991 12.95 15.7 10.615 15.7 7.975H12.9ZM11 5.8C11.9459 5.8 12.9 6.66616 12.9 7.975H15.7C15.7 5.335 13.6991 3 11 3V5.8ZM9.1 7.975C9.1 6.66616 10.0541 5.8 11 5.8V3C8.30086 3 6.3 5.335 6.3 7.975H9.1ZM11 10.15C10.0541 10.15 9.1 9.28384 9.1 7.975H6.3C6.3 10.615 8.30086 12.95 11 12.95V10.15ZM16.146 16.3178C14.8129 17.6081 13.0004 18.4 11 18.4V21.2C13.756 21.2 16.2591 20.1051 18.0934 18.3296L16.146 16.3178ZM11 14.6C13.1797 14.6 15.0494 15.9415 15.8221 17.8493L18.4173 16.7981C17.2312 13.8697 14.359 11.8 11 11.8V14.6ZM6.17788 17.8493C6.95058 15.9415 8.8203 14.6 11 14.6V11.8C7.64102 11.8 4.7688 13.8697 3.58268 16.7981L6.17788 17.8493ZM11 18.4C8.99963 18.4 7.18709 17.6081 5.85397 16.3178L3.90659 18.3296C5.74088 20.1051 8.24402 21.2 11 21.2V18.4Z" fill="white" mask="url(#path-1-inside-1_898_2641)"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 3.0 KiB |
113
src/index.css
@@ -21,101 +21,116 @@ limitations under the License.
|
||||
(to avoid having to maintain a fork of Inter). */
|
||||
|
||||
:root {
|
||||
--inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-2664,U+2666-2763,U+2765-2b05,U+2b07-2b1b,U+2b1d-10FFFF;
|
||||
--inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f,
|
||||
U+25c2-2664, U+2666-2763, U+2765-2b05, U+2b07-2b1b, U+2b1d-10FFFF;
|
||||
--primaryColor: #0dbd8b;
|
||||
--bgColor1: #15191e;
|
||||
--bgColor2: #21262c;
|
||||
--bgColor3: #444;
|
||||
--bgColor4: #394049;
|
||||
--bgColor5: #8d97a5;
|
||||
--textColor1: #fff;
|
||||
--textColor2: #6f7882;
|
||||
--textColor3: #8e99a4;
|
||||
--textColor4: #a9b2bc;
|
||||
--inputBorderColor: #394049;
|
||||
--inputBorderColorFocused: #0086e6;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src: url("/fonts/Inter/Inter-Regular.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-Regular.woff") format("woff");
|
||||
url("/fonts/Inter/Inter-Regular.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-family: "Inter";
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src: url("/fonts/Inter/Inter-Italic.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-Italic.woff") format("woff");
|
||||
url("/fonts/Inter/Inter-Italic.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src: url("/fonts/Inter/Inter-Medium.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-Medium.woff") format("woff");
|
||||
url("/fonts/Inter/Inter-Medium.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-family: "Inter";
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src: url("/fonts/Inter/Inter-MediumItalic.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-MediumItalic.woff") format("woff");
|
||||
url("/fonts/Inter/Inter-MediumItalic.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src: url("/fonts/Inter/Inter-SemiBold.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-SemiBold.woff") format("woff");
|
||||
url("/fonts/Inter/Inter-SemiBold.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-family: "Inter";
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src: url("/fonts/Inter/Inter-SemiBoldItalic.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-SemiBoldItalic.woff") format("woff");
|
||||
url("/fonts/Inter/Inter-SemiBoldItalic.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src: url("/fonts/Inter/Inter-Bold.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-Bold.woff") format("woff");
|
||||
url("/fonts/Inter/Inter-Bold.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-family: "Inter";
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
unicode-range: var(--inter-unicode-range);
|
||||
src: url("/fonts/Inter/Inter-BoldItalic.woff2") format("woff2"),
|
||||
url("/fonts/Inter/Inter-BoldItalic.woff") format("woff");
|
||||
url("/fonts/Inter/Inter-BoldItalic.woff") format("woff");
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #15191e;
|
||||
color: #fff;
|
||||
background-color: var(--bgColor1);
|
||||
color: var(--textColor1);
|
||||
margin: 0;
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -125,10 +140,42 @@ html, body, #root {
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0086e6;
|
||||
font-weight: bold;
|
||||
color: var(--primaryColor);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover, a:active {
|
||||
a:hover,
|
||||
a:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: calc(100% - 24px);
|
||||
border: none;
|
||||
border-top: 1px solid var(--bgColor4);
|
||||
color: var(--textColor2);
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
height: 5px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
summary {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
details > :not(summary) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
details[open] > summary {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#root > [data-overlay-container] {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
24
src/main.jsx
@@ -21,6 +21,28 @@ 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;
|
||||
style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR);
|
||||
style.setProperty("--bgColor1", import.meta.env.VITE_BG_COLOR_1);
|
||||
style.setProperty("--bgColor2", import.meta.env.VITE_BG_COLOR_2);
|
||||
style.setProperty("--bgColor3", import.meta.env.VITE_BG_COLOR_3);
|
||||
style.setProperty("--bgColor4", import.meta.env.VITE_BG_COLOR_4);
|
||||
style.setProperty("--bgColor5", import.meta.env.VITE_BG_COLOR_5);
|
||||
style.setProperty("--textColor1", import.meta.env.VITE_TEXT_COLOR_1);
|
||||
style.setProperty("--textColor2", import.meta.env.VITE_TEXT_COLOR_2);
|
||||
style.setProperty("--textColor4", import.meta.env.VITE_TEXT_COLOR_4);
|
||||
style.setProperty(
|
||||
"--inputBorderColor",
|
||||
import.meta.env.VITE_INPUT_BORDER_COLOR
|
||||
);
|
||||
style.setProperty(
|
||||
"--inputBorderColorFocused",
|
||||
import.meta.env.VITE_INPUT_BORDER_COLOR_FOCUSED
|
||||
);
|
||||
}
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
@@ -36,7 +58,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>,
|
||||
|
||||
74
src/useMediaHandler.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
let audioOutput;
|
||||
|
||||
export function useMediaHandler(client) {
|
||||
const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
|
||||
useState(() => {
|
||||
const mediaHandler = client.getMediaHandler();
|
||||
|
||||
return {
|
||||
audioInput: mediaHandler.audioInput,
|
||||
videoInput: mediaHandler.videoInput,
|
||||
audioInputs: [],
|
||||
videoInputs: [],
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const mediaHandler = client.getMediaHandler();
|
||||
|
||||
function updateDevices() {
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
const audioInputs = devices.filter(
|
||||
(device) => device.kind === "audioinput"
|
||||
);
|
||||
const videoInputs = devices.filter(
|
||||
(device) => device.kind === "videoinput"
|
||||
);
|
||||
|
||||
setState((prevState) => ({
|
||||
audioInput: mediaHandler.audioInput,
|
||||
videoInput: mediaHandler.videoInput,
|
||||
audioInputs,
|
||||
videoInputs,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
updateDevices();
|
||||
|
||||
mediaHandler.on("local_streams_changed", updateDevices);
|
||||
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
|
||||
|
||||
return () => {
|
||||
mediaHandler.removeListener("local_streams_changed", updateDevices);
|
||||
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setAudioInput = useCallback(
|
||||
(deviceId) => {
|
||||
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
|
||||
client.getMediaHandler().setAudioInput(deviceId);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
const setVideoInput = useCallback(
|
||||
(deviceId) => {
|
||||
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
|
||||
client.getMediaHandler().setVideoInput(deviceId);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return {
|
||||
audioInput,
|
||||
audioInputs,
|
||||
setAudioInput,
|
||||
videoInput,
|
||||
videoInputs,
|
||||
setVideoInput,
|
||||
};
|
||||
}
|
||||
487
yarn.lock
@@ -176,6 +176,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6":
|
||||
version "7.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
||||
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz"
|
||||
@@ -213,6 +220,258 @@
|
||||
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
|
||||
integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
|
||||
|
||||
"@formatjs/ecma402-abstract@1.11.0":
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.0.tgz#7e91e6cc7cfebdc07171e00a3288719705e0108c"
|
||||
integrity sha512-TOp5La9wmSh9G5bqFGN/ApmOXtJDzBGkYW+OTRd3ukY7J32RVGZPpN4O9BD651JUy66nj3g9CIENTNCgm4IRXQ==
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher" "0.2.21"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/fast-memoize@1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.0.tgz#1123bfcc5d21d761f15d8b1c32d10e1b6530355d"
|
||||
integrity sha512-fObitP9Tlc31SKrPHgkPgQpGo4+4yXfQQITTCNH8AZdEqB7Mq4nPrjpUL/tNGN3lEeJcFxDbi0haX8HM7QvQ8w==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/icu-messageformat-parser@2.0.15":
|
||||
version "2.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.0.15.tgz#9e3ccadc582dbf076481bb95f98a689cfb10e7d5"
|
||||
integrity sha512-nnRbkK+nz4ZL1l1lUbztL8qrEUGQKF/NU38itLnzLm8QLEacFS5qGOxxp/0DSIehhX99tNroNtudtjdOvzruAQ==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.11.0"
|
||||
"@formatjs/icu-skeleton-parser" "1.3.2"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/icu-skeleton-parser@1.3.2":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.2.tgz#a8ab9c668ea7f044ceba2043ac1d872d71307e22"
|
||||
integrity sha512-ChKmnVCE/LbJzedRgA/EeL5+tfjx/6ZWunqNiEC5BtqHnnwmLN/oPuCPb8b3NhuGiwTqp+LkaS70tga5kXRHxg==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.11.0"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/intl-localematcher@0.2.21":
|
||||
version "0.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.21.tgz#39ef33d701fe8084f3d693cd3ff7cbe03cdd3a49"
|
||||
integrity sha512-JTJeLiNwexN4Gy0cMxoUPvJbKhXdnSuo5jPrDafEZpnDWlJ5VDYta8zUVVozO/pwzEmFVHEUpgiEDj+39L4oMg==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@internationalized/date@3.0.0-alpha.1":
|
||||
version "3.0.0-alpha.1"
|
||||
resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.0.0-alpha.1.tgz#987a86a98b837f275bce084ef502421bc5cdb5f7"
|
||||
integrity sha512-fxciU4AQ/4XBYfse/mT9h1nsyNkmQkxwQtTmQVu6b4Tp2u95Y3m5BNgWgV2m3vLiiKZ82NtHJXAIGoqiK53w4g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
|
||||
"@internationalized/message@^3.0.2":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@internationalized/message/-/message-3.0.2.tgz#c3db2b6b7f75af815819f77da11f8424381416e3"
|
||||
integrity sha512-ZZ8FQDCsri3vUB2mfDD76Vbf97DH361AiZUXKHV4BqwCtYyaNYiZqIr8KXrcMCxJvrIYVQLSn8+jeIQRO3bvtw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
intl-messageformat "^9.6.12"
|
||||
|
||||
"@internationalized/number@^3.0.2":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.0.3.tgz#d29003dffdff54ca6f2287ec0cb77ff3d045478f"
|
||||
integrity sha512-ewFoVvsxSyd9QZnknvOWPjirYqdMQhXTeDhJg3hM6C/FeZt0banpGH1nZ0SGMZXHz8NK9uAa2KVIq+jqAIOg4w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
|
||||
"@react-aria/button@^3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.3.4.tgz#3af6eb4e0a479a76ba7386d541051d1273cd68fa"
|
||||
integrity sha512-vebTcf9YpwaKCvsca2VWhn6eYPa15OJtMENwaGop72UrL35Oa7xDgU0RG22RAjRjt8HRVlAfLpHkJQW6GBGU3g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/focus" "^3.5.0"
|
||||
"@react-aria/interactions" "^3.6.0"
|
||||
"@react-aria/utils" "^3.9.0"
|
||||
"@react-stately/toggle" "^3.2.3"
|
||||
"@react-types/button" "^3.4.1"
|
||||
|
||||
"@react-aria/dialog@^3.1.4":
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/dialog/-/dialog-3.1.4.tgz#7fe3f33e09b75dcdf598d0523e982262d6c89220"
|
||||
integrity sha512-OtQGBol3CfcbBpjqXDqXzH5Ygny44PIuyAsZ1e3dfIdtaI+XHsoglyZnvDaVVealIgedHkMubreZnyNYnlzPLg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/focus" "^3.4.1"
|
||||
"@react-aria/utils" "^3.8.2"
|
||||
"@react-stately/overlays" "^3.1.3"
|
||||
"@react-types/dialog" "^3.3.1"
|
||||
|
||||
"@react-aria/focus@^3.4.1", "@react-aria/focus@^3.5.0":
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.5.0.tgz#02b85f97d6114af1eccc0902ce40723b626cb7f9"
|
||||
integrity sha512-Eib75Q6QgQdn8VVVByg5Vipaaj/C//8Bs++sQY7nkomRx4sdArOnXbDppul3YHP6mRfU9VRLvAigEUlReQF/Xw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/interactions" "^3.6.0"
|
||||
"@react-aria/utils" "^3.9.0"
|
||||
"@react-types/shared" "^3.9.0"
|
||||
clsx "^1.1.1"
|
||||
|
||||
"@react-aria/i18n@^3.3.3":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/i18n/-/i18n-3.3.4.tgz#172b8bcff0273410e67af31f7d84e49dd3ada463"
|
||||
integrity sha512-1DV3I82UfL2dT8WBI/88TwtokO80B7ISSyuz6rO/6n7q76A/nC2AtVINbrGYrcKsCcxCEoEMxW5RVJ39fcLijA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@internationalized/date" "3.0.0-alpha.1"
|
||||
"@internationalized/message" "^3.0.2"
|
||||
"@internationalized/number" "^3.0.2"
|
||||
"@react-aria/ssr" "^3.0.3"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/interactions@^3.5.1", "@react-aria/interactions@^3.6.0", "@react-aria/interactions@^3.7.0":
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.7.0.tgz#eb19c1068b557a6b6df1e1c4abef07de719e9f25"
|
||||
integrity sha512-Xomchjb9bqvh3ocil+QCEYFSxsTy8PHEz43mNP6z2yuu3UqTpl2FsWfyKgF/Yy0WKVkyV2dO2uz758KJTCLZhw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/label@^3.2.1":
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/label/-/label-3.2.1.tgz#e6562259e6b17e3856c4c3e0060903cf705d094b"
|
||||
integrity sha512-QZ5/dpJKRjB1JtFZfOVd5GUiCpA2yMgmNA6ky6jT5XNAo7H14QqGRFUGDTLAQYGd+Bc3s+NayOT3NKUYur/3Xw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-types/label" "^3.5.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/listbox@^3.4.0":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/listbox/-/listbox-3.4.0.tgz#384f72f544540b53cb56f5734949aff6f3be723e"
|
||||
integrity sha512-Tc6JAPHrNKbjFMOCI50YHFBltSxBc84CaLIQdVo4c9KYiwgoAy1ULeSnRyp4ru3qpnffJZEUCNWD+864+MZVEQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/focus" "^3.4.1"
|
||||
"@react-aria/interactions" "^3.7.0"
|
||||
"@react-aria/label" "^3.2.1"
|
||||
"@react-aria/selection" "^3.7.0"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-stately/collections" "^3.3.3"
|
||||
"@react-stately/list" "^3.4.0"
|
||||
"@react-types/listbox" "^3.2.1"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/menu@^3.3.0":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/menu/-/menu-3.3.0.tgz#09364a306b3b0dec7f3cf532bfa184a1f4e26da7"
|
||||
integrity sha512-e/5zlWSwcsUYxH+kLrACPhLxh/Z+8/xvAB90G7xjBble1RusYQ+iH+M2U1n5vqoenZ3vjBpmEDsdo6vHeFeKxQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/interactions" "^3.7.0"
|
||||
"@react-aria/overlays" "^3.7.3"
|
||||
"@react-aria/selection" "^3.7.0"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-stately/collections" "^3.3.3"
|
||||
"@react-stately/menu" "^3.2.3"
|
||||
"@react-stately/tree" "^3.2.0"
|
||||
"@react-types/button" "^3.4.1"
|
||||
"@react-types/menu" "^3.3.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/overlays@^3.7.3":
|
||||
version "3.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/overlays/-/overlays-3.7.3.tgz#b107b1d31c04c538355e566b1034d23e5696c18a"
|
||||
integrity sha512-N5F/TVJ9KIYgGuOknVMrRnqqzkNKcFos4nxLHQz4TeFZTp4/P+NqEHd/VBmjsSTNEjEuNAivG+U2o4F1NWn/Pw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/i18n" "^3.3.3"
|
||||
"@react-aria/interactions" "^3.7.0"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-aria/visually-hidden" "^3.2.3"
|
||||
"@react-stately/overlays" "^3.1.3"
|
||||
"@react-types/button" "^3.4.1"
|
||||
"@react-types/overlays" "^3.5.1"
|
||||
dom-helpers "^3.3.1"
|
||||
|
||||
"@react-aria/select@^3.6.0":
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/select/-/select-3.6.0.tgz#d5750614de36af7b26c6d2285bf996647818f8cc"
|
||||
integrity sha512-jHLyeiy1iR1qaoFdJpQa2V7RL4Nb9JfVDNHNbgp8I5peoU+2oIN34NbqHTnsKlOBfhBRih2PnLogT2Iw4FZ3+Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/i18n" "^3.3.3"
|
||||
"@react-aria/interactions" "^3.7.0"
|
||||
"@react-aria/label" "^3.2.1"
|
||||
"@react-aria/listbox" "^3.4.0"
|
||||
"@react-aria/menu" "^3.3.0"
|
||||
"@react-aria/selection" "^3.7.0"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-aria/visually-hidden" "^3.2.3"
|
||||
"@react-stately/select" "^3.1.3"
|
||||
"@react-types/button" "^3.4.1"
|
||||
"@react-types/select" "^3.5.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/selection@^3.7.0":
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/selection/-/selection-3.7.0.tgz#77dc483dca82303f20b8567bfac24a2e57bade6d"
|
||||
integrity sha512-OHvxxTZeI8vJYQXDG9KUx/MFJ2DY70phW0XausbzYzUFwaXiWIOb8YQrTHA2CpVOV8E4c+qd522sxHGBq8hPDg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/focus" "^3.5.0"
|
||||
"@react-aria/i18n" "^3.3.3"
|
||||
"@react-aria/interactions" "^3.7.0"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-stately/collections" "^3.3.3"
|
||||
"@react-stately/selection" "^3.8.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/ssr@^3.0.3", "@react-aria/ssr@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.1.0.tgz#b7163e6224725c30121932a8d1422ef91d1fab22"
|
||||
integrity sha512-RxqQKmE8sO7TGdrcSlHTcVzMP450hqowtBSd2bBS9oPlcokVkaGq28c3Rwa8ty5ctw4EBCjXqjP7xdcKMGDzug==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
|
||||
"@react-aria/tabs@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/tabs/-/tabs-3.1.0.tgz#e64b17a592610195466026d7866bde0b32784e69"
|
||||
integrity sha512-3mU8UclpGVI7muLVTXlJVgHn7RJ+eyBWxPzlgFPBR35acdBrhIt1hBv7PGTAhCg6Zj75CQr07BM/Kdi1WWOP5g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/focus" "^3.5.0"
|
||||
"@react-aria/i18n" "^3.3.3"
|
||||
"@react-aria/interactions" "^3.7.0"
|
||||
"@react-aria/selection" "^3.7.0"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-stately/list" "^3.4.0"
|
||||
"@react-stately/tabs" "^3.0.1"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
"@react-types/tabs" "^3.0.1"
|
||||
|
||||
"@react-aria/utils@^3.10.0", "@react-aria/utils@^3.8.2", "@react-aria/utils@^3.9.0":
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.10.0.tgz#2f6f0b0ccede17241fca1cbd76978e1bf8f5a2b0"
|
||||
integrity sha512-he/1pV8gsTVwmYqbKI6DPtRUkWjzz/4icgemVVNjWNsiKEJSBj8Cr4I+0i3vIgXEPLnn1t+/LUsJMGFbKnqc9w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/ssr" "^3.1.0"
|
||||
"@react-stately/utils" "^3.3.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
clsx "^1.1.1"
|
||||
|
||||
"@react-aria/visually-hidden@^3.2.3":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/visually-hidden/-/visually-hidden-3.2.3.tgz#4779df0a468873550afb42a7f5fcb2411d82db8d"
|
||||
integrity sha512-iAe5EFI7obEOwTnIdAwWrKq+CrIJFGTw85v8fXnQ7CIVGRDblX85GOUww9bzQNPDLLRYWS4VF702ii8kV4+JCw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/interactions" "^3.5.1"
|
||||
"@react-aria/utils" "^3.8.2"
|
||||
clsx "^1.1.1"
|
||||
|
||||
"@react-spring/animated@~9.2.0":
|
||||
version "9.2.4"
|
||||
resolved "https://registry.npmjs.org/@react-spring/animated/-/animated-9.2.4.tgz"
|
||||
@@ -258,6 +517,177 @@
|
||||
"@react-spring/shared" "~9.2.0"
|
||||
"@react-spring/types" "~9.2.0"
|
||||
|
||||
"@react-stately/collections@^3.3.3", "@react-stately/collections@^3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/collections/-/collections-3.3.4.tgz#ca9e481d2769dbebb593d5a1ed4bcc54013aef9d"
|
||||
integrity sha512-HnlQip9RH+3nGNh8U2U1YkCdA+zGSedQLWVCdR53w/n6To7kIp7yDLby5dkmP+9VGQEkyfsoSjwGJ1NF94CCZg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-stately/list@^3.3.0", "@react-stately/list@^3.4.0":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/list/-/list-3.4.0.tgz#34ef0645f126efac14dbdcf80ca767dea7a87c3d"
|
||||
integrity sha512-nqVuECSySZU79lF53+YMCl+N1krCoulYNSIohSsply8MN54gJsNHKOKWnhIx05Cdw6hz1rHmdSJ+/sRHhVvBSw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/collections" "^3.3.3"
|
||||
"@react-stately/selection" "^3.8.0"
|
||||
"@react-stately/utils" "^3.3.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-stately/menu@^3.2.3":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/menu/-/menu-3.2.3.tgz#eb58e3cfc941d49637bac04aa474935f08bc7215"
|
||||
integrity sha512-r09qH8F+OaH7PTc9t2iAOfeCPy3jSg9uAwlDiGaev3zknM618XafIoQ1sWUNQYecSQ5BWWUyBYh5Vl8i2HnEvw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/overlays" "^3.1.3"
|
||||
"@react-stately/utils" "^3.2.2"
|
||||
"@react-types/menu" "^3.3.0"
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-stately/overlays@^3.1.3":
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/overlays/-/overlays-3.1.3.tgz#b0bb4061c1b20e712dfc32c933ae4bb23e5ccc0e"
|
||||
integrity sha512-X8H/h9F8ZjevwJ7P8ak7v500qQd5x4Y76LsXUXrR6LtcO8FXfp2I+W8sGmBtLZwLQpTJiF1U0WMQqXLE1g6eLA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/utils" "^3.2.2"
|
||||
"@react-types/overlays" "^3.5.1"
|
||||
|
||||
"@react-stately/select@^3.1.3":
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/select/-/select-3.1.3.tgz#539340e9ccdf8d0b331d289f18f1bb5c81ab3655"
|
||||
integrity sha512-r0M2gcyyfo7vDDZGsOb64XQlVHtNQl+3mId3gYA46sHEu81C8Lhy4YSPZjItppnCLigBlm88hISl/i0e+XBx8g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/collections" "^3.3.3"
|
||||
"@react-stately/list" "^3.3.0"
|
||||
"@react-stately/menu" "^3.2.3"
|
||||
"@react-stately/selection" "^3.7.0"
|
||||
"@react-stately/utils" "^3.2.2"
|
||||
"@react-types/select" "^3.3.1"
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-stately/selection@^3.7.0", "@react-stately/selection@^3.8.0":
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/selection/-/selection-3.8.0.tgz#c5292d9b0e67cb48b1bfabd050da57949950e431"
|
||||
integrity sha512-FDO2kJRTF43wxx6DD8fQtxCg7UvUtwO4KJi6hk/W7ZOyQZuwq9INQux826mvSXT7Ixw1OTkvKowrO6/knol0mA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/collections" "^3.3.3"
|
||||
"@react-stately/utils" "^3.3.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-stately/tabs@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/tabs/-/tabs-3.0.1.tgz#7df000f8c5c14b3bf973348c491e12bcfcafe49d"
|
||||
integrity sha512-XhF/5mt8eme3mu0+4nC7Du+e5OWSu0W8SeKfbH9JmTWTCayZpPtui68nRStJK6OkgHs28gA+j55RSsTqT/N1Fg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/list" "^3.3.0"
|
||||
"@react-stately/utils" "^3.2.2"
|
||||
"@react-types/tabs" "^3.0.1"
|
||||
|
||||
"@react-stately/toggle@^3.2.3":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/toggle/-/toggle-3.2.3.tgz#a4de6edc16982990492c6c557e5194f46dacc809"
|
||||
integrity sha512-p5eVjXwNo4y4CeybxfjYmbTzNMNiI67uspbRAJnawWBVWw8X+yIvRfpjYAsqmvsJ+DsvwybSTlQDT6taGoWEsA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/utils" "^3.2.2"
|
||||
"@react-types/checkbox" "^3.2.3"
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-stately/tree@^3.2.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/tree/-/tree-3.2.0.tgz#151c90f161c5c8339b6876f59a4f0502be08670b"
|
||||
integrity sha512-FfHfs79KeXN0Yi8X5O/oRWL1ZTZByhIT++NEUX4aCO3VSxnqhcl9/ErSmH/fYr36q0xdYZsX3BFvk/fm1EDkXQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/collections" "^3.3.3"
|
||||
"@react-stately/selection" "^3.7.0"
|
||||
"@react-stately/utils" "^3.2.2"
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-stately/utils@^3.2.2", "@react-stately/utils@^3.3.0":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.3.0.tgz#99866c5788539268a06035acd5925b25bb4cedde"
|
||||
integrity sha512-f//Y8q0+FFcS04xvCNvbba7WWRLHzj2AegLgdgwTxsnk9Gb+AyuasdRrRY7bGQhdHuEJ7OIiQZ9EQWndDbrTcg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
|
||||
"@react-types/button@^3.4.1":
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.4.1.tgz#715ac9d4997c79233be4d9020b58f85936b8252b"
|
||||
integrity sha512-B54M84LxdEppwjXNlkBEJyMfe9fd+bvFV7R6+NJvupGrZm/LuFNYjFcHk7yjMKWTdWm6DbpIuQz54n5qTW7Vlg==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-types/checkbox@^3.2.3":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/checkbox/-/checkbox-3.2.3.tgz#2b9d529c55c9884519c7f626f0fe8be7d0f18be1"
|
||||
integrity sha512-YqeAFyrpaxI/eW6zQ7tVkKIASgzpywRrc6C/rV6Mw0zzGGSSvmYvdOBx9yHOEvpts7dLgaGlmLK6CeG7s4yGKg==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-types/dialog@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/dialog/-/dialog-3.3.1.tgz#eb07e3d703643f7967243d56951d58a2cf77096f"
|
||||
integrity sha512-1i6fVtixUNlftSNbVPFRieyEy3N/GNqcqpeOsJUB1jby28ppbM+JCp3Icb0ijaNC9Nl8c/oI8srtOWIQIKUJiQ==
|
||||
dependencies:
|
||||
"@react-types/overlays" "^3.5.1"
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-types/label@^3.5.0":
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/label/-/label-3.5.0.tgz#c7093871f42c62e1b5523f61a0856a2f58d4cf2a"
|
||||
integrity sha512-a9lpQUyV4XwsZv0gV1jPjPWicSSa+DRliuXLTwORirxNLF0kMk89DLYf0a9CZhiEniJYqoqR3laJDvLAFW1x/Q==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.9.0"
|
||||
|
||||
"@react-types/listbox@^3.2.1":
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/listbox/-/listbox-3.2.1.tgz#881bbc9690343f35fe08a99526a90618f53328bf"
|
||||
integrity sha512-uBYx5BgL8gyH62UhSXAFyFDwAD4ALcK5gjOk+p/vWsFm0vvmtutALkb3yYjDQvwdI89pSZDjN4j7QChCmlNcmQ==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-types/menu@^3.3.0":
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/menu/-/menu-3.4.1.tgz#42f58ce3b79b844441627c5cd3126705b3b00063"
|
||||
integrity sha512-9xx7x13h2/DpnD8m2+eN09ViEwXldYXGtD0WSExO99ZulNi4tbzwJfnUBBJWj1aq9v2ZgObECOSqwkIDOJo6qA==
|
||||
dependencies:
|
||||
"@react-types/overlays" "^3.5.1"
|
||||
"@react-types/shared" "^3.9.0"
|
||||
|
||||
"@react-types/overlays@^3.5.1":
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/overlays/-/overlays-3.5.1.tgz#35350dfca639d04a8fbd973de59b141450df1b46"
|
||||
integrity sha512-T3o6wQ5NNm1rSniIa01bIa6fALC8jbwpYxFMaQRrdEpIvwktt0Fi5Xo6/97+oe4HvzzU0JMhtwWDTdRySvgeZw==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-types/select@^3.3.1", "@react-types/select@^3.5.0":
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/select/-/select-3.5.0.tgz#50ae1e7b14925189ea45c8322a776fd40473d31e"
|
||||
integrity sha512-XdLS/kvvlOZbVP/wn8tX5iAL0kpND3ZSea8KXG3EkwIw8sn1xcd8tYx7TkdF89IdNbg2pmzn9stOv9RUbC8MoQ==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-types/shared@^3.10.0", "@react-types/shared@^3.8.0", "@react-types/shared@^3.9.0":
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.10.0.tgz#bdafed2ebcd31149c178312252dda0babde316d0"
|
||||
integrity sha512-B1gTRpE5qkSpfGxw8BHeOwvBPP3gnfKnzPHV0FJQHtgJ46oJS64WyloDAp1D9cLVsFHaI6s/HviXL51kVce2ww==
|
||||
|
||||
"@react-types/tabs@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/tabs/-/tabs-3.0.1.tgz#a32931e95303d4442e51d4c687d4e154654014f6"
|
||||
integrity sha512-GvPVU9GAqImHFhU+Do+pdGK/vZA4kqA699Gly1V95DUmtdG3GSwTnwlvM/Sy80/F9fKZDGokZnQmBFo8MFZyIw==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@sentry/browser@6.13.3":
|
||||
version "6.13.3"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.13.3.tgz#d4511791b1e484ad48785eba3bce291fdf115c1e"
|
||||
@@ -834,6 +1264,11 @@ cliui@^5.0.0:
|
||||
strip-ansi "^5.2.0"
|
||||
wrap-ansi "^5.1.0"
|
||||
|
||||
clsx@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz"
|
||||
@@ -885,6 +1320,13 @@ convert-source-map@^1.7.0:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.1"
|
||||
|
||||
copy-to-clipboard@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
|
||||
integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
|
||||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
core-js@^1.0.0:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
|
||||
@@ -1053,6 +1495,13 @@ dijkstrajs@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
|
||||
integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
|
||||
|
||||
dom-helpers@^3.3.1:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
|
||||
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
|
||||
dom-helpers@^5.0.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
|
||||
@@ -1548,6 +1997,13 @@ history@^4.9.0:
|
||||
tiny-warning "^1.0.0"
|
||||
value-equal "^1.0.1"
|
||||
|
||||
history@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/history/-/history-5.1.0.tgz#2e93c09c064194d38d52ed62afd0afc9d9b01ece"
|
||||
integrity sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.7.6"
|
||||
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
@@ -1614,6 +2070,16 @@ indexof@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
|
||||
integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
|
||||
|
||||
intl-messageformat@^9.6.12:
|
||||
version "9.10.0"
|
||||
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.10.0.tgz#f9864f6e15dde343851398082993911e57a6446e"
|
||||
integrity sha512-OTOLlGPfwbrFyYD2iQuDbqEs8xccyLy+f1P3ZGJB2/EZo7Z9fVaaIWcM+JGvuWIFVRDnw4Um6z4t0mSSitUxGQ==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.11.0"
|
||||
"@formatjs/fast-memoize" "1.2.0"
|
||||
"@formatjs/icu-messageformat-parser" "2.0.15"
|
||||
tslib "^2.1.0"
|
||||
|
||||
ip-regex@^4.0.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
|
||||
@@ -2666,6 +3132,13 @@ react-router@5.2.1:
|
||||
tiny-invariant "^1.0.2"
|
||||
tiny-warning "^1.0.0"
|
||||
|
||||
react-router@6:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.0.2.tgz#bd2b0fa84fd1d152671e9f654d9c0b1f5a7c86da"
|
||||
integrity sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==
|
||||
dependencies:
|
||||
history "^5.1.0"
|
||||
|
||||
react-textarea-autosize@^8.3.2:
|
||||
version "8.3.3"
|
||||
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.3.tgz#f70913945369da453fd554c168f6baacd1fa04d8"
|
||||
@@ -2685,6 +3158,13 @@ react-transition-group@^4.4.1:
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-use-clipboard@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/react-use-clipboard/-/react-use-clipboard-1.0.7.tgz#34cccdfb4f17f5e6522e38f2cee62d05f605597a"
|
||||
integrity sha512-blIprqARyITp0uVw/2Rh87mcujqXdH6vZ5NrcuXEhI5EmjBGxcGnwt/79+vdN7rwM6OliGj481lOj6ZCcsiYEQ==
|
||||
dependencies:
|
||||
copy-to-clipboard "^3.3.1"
|
||||
|
||||
react-use-gesture@^9.1.3:
|
||||
version "9.1.3"
|
||||
resolved "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-9.1.3.tgz"
|
||||
@@ -2962,6 +3442,11 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
toggle-selection@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
||||
integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
|
||||
|
||||
tough-cookie@~2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
||||
@@ -2980,7 +3465,7 @@ tslib@^1.9.3:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.3, tslib@^2.2.0:
|
||||
tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
||||