Passwordless user flow
This commit is contained in:
parent
20350e66a2
commit
fc3960ce63
12 changed files with 589 additions and 369 deletions
84
src/App.jsx
84
src/App.jsx
|
@ -15,86 +15,42 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
useLocation,
|
||||
} from "react-router-dom";
|
||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { useClient } from "./ConferenceCallManagerHooks";
|
||||
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 { OverlayProvider } from "@react-aria/overlays";
|
||||
import { Home } from "./Home";
|
||||
import { LoginPage } from "./LoginPage";
|
||||
import { RegisterPage } from "./RegisterPage";
|
||||
import { Room } from "./Room";
|
||||
import { ClientProvider } from "./ConferenceCallManagerHooks";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
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 { protocol, host } = window.location;
|
||||
// Assume homeserver is hosted on same domain (proxied in development by vite)
|
||||
const homeserverUrl = `${protocol}//${host}`;
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Router>
|
||||
<OverlayProvider>
|
||||
{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} onLogout={logout} />
|
||||
) : (
|
||||
<GuestAuthPage onLoginAsGuest={registerGuest} />
|
||||
)}
|
||||
<Room />
|
||||
</SentryRoute>
|
||||
</Switch>
|
||||
)}
|
||||
</OverlayProvider>
|
||||
</OverlayProvider>
|
||||
</ClientProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthenticatedRoute({ authenticated, children, ...rest }) {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<SentryRoute {...rest}>
|
||||
{authenticated ? (
|
||||
children
|
||||
) : (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: "/login",
|
||||
state: { from: location },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SentryRoute>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,9 +4,28 @@ import { CopyButton } from "./button";
|
|||
import { Facepile } from "./Facepile";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||
import styles from "./CallTile.module.css";
|
||||
import styles from "./CallList.module.css";
|
||||
|
||||
export function CallTile({ name, avatarUrl, roomUrl, participants }) {
|
||||
export function CallList({ title, rooms }) {
|
||||
return (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<div className={styles.callList}>
|
||||
{rooms.map(({ roomId, roomName, roomUrl, avatarUrl, participants }) => (
|
||||
<CallTile
|
||||
key={roomId}
|
||||
name={roomName}
|
||||
avatarUrl={avatarUrl}
|
||||
roomUrl={roomUrl}
|
||||
participants={participants}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CallTile({ name, avatarUrl, roomUrl, participants }) {
|
||||
return (
|
||||
<Link to={roomUrl} className={styles.callTile}>
|
||||
<Avatar
|
|
@ -52,3 +52,11 @@
|
|||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.callList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 24px;
|
||||
flex: 1;
|
||||
}
|
|
@ -14,8 +14,23 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
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) => {
|
||||
|
@ -86,12 +101,16 @@ export async function fetchGroupCall(
|
|||
});
|
||||
}
|
||||
|
||||
export function useClient(homeserverUrl) {
|
||||
const [{ loading, authenticated, client }, setState] = useState({
|
||||
loading: true,
|
||||
authenticated: false,
|
||||
client: undefined,
|
||||
});
|
||||
export function ClientProvider({ homeserverUrl, children }) {
|
||||
const history = useHistory();
|
||||
const [{ loading, isAuthenticated, isGuest, client, userName }, setState] =
|
||||
useState({
|
||||
loading: true,
|
||||
isAuthenticated: false,
|
||||
isGuest: false,
|
||||
client: undefined,
|
||||
userName: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function restore() {
|
||||
|
@ -117,8 +136,10 @@ export function useClient(homeserverUrl) {
|
|||
JSON.stringify({ user_id, device_id, access_token, guest })
|
||||
);
|
||||
|
||||
return client;
|
||||
return { client, guest };
|
||||
}
|
||||
|
||||
return { client: undefined, guest: false };
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
throw err;
|
||||
|
@ -126,15 +147,23 @@ export function useClient(homeserverUrl) {
|
|||
}
|
||||
|
||||
restore()
|
||||
.then((client) => {
|
||||
if (client) {
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
} else {
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
}
|
||||
.then(({ client, guest }) => {
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isAuthenticated: !!client,
|
||||
isGuest: guest,
|
||||
userName: client?.getUserIdLocalpart(),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isAuthenticated: false,
|
||||
isGuest: false,
|
||||
userName: null,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
@ -176,10 +205,22 @@ export function useClient(homeserverUrl) {
|
|||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isAuthenticated: true,
|
||||
isGuest: false,
|
||||
userName: client.getUserIdLocalpart(),
|
||||
});
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isAuthenticated: false,
|
||||
isGuest: false,
|
||||
userName: null,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
@ -210,10 +251,22 @@ export function useClient(homeserverUrl) {
|
|||
JSON.stringify({ user_id, device_id, access_token, guest: true })
|
||||
);
|
||||
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isAuthenticated: true,
|
||||
isGuest: true,
|
||||
userName: client.getUserIdLocalpart(),
|
||||
});
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isAuthenticated: false,
|
||||
isGuest: false,
|
||||
userName: null,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
@ -239,10 +292,25 @@ export function useClient(homeserverUrl) {
|
|||
JSON.stringify({ user_id, device_id, access_token })
|
||||
);
|
||||
|
||||
setState({ client, loading: false, authenticated: true });
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isGuest: false,
|
||||
isAuthenticated: true,
|
||||
isGuest: false,
|
||||
userName: client.getUserIdLocalpart(),
|
||||
});
|
||||
|
||||
return client;
|
||||
} catch (err) {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
setState({ client: undefined, loading: false, authenticated: false });
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isGuest: false,
|
||||
isAuthenticated: false,
|
||||
userName: null,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
@ -252,17 +320,164 @@ export function useClient(homeserverUrl) {
|
|||
window.location = "/";
|
||||
}, [history]);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
loading,
|
||||
isAuthenticated,
|
||||
isGuest,
|
||||
client,
|
||||
login,
|
||||
registerGuest,
|
||||
register,
|
||||
logout,
|
||||
userName,
|
||||
}),
|
||||
[
|
||||
loading,
|
||||
isAuthenticated,
|
||||
isGuest,
|
||||
client,
|
||||
login,
|
||||
registerGuest,
|
||||
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(client) {
|
||||
const [creatingRoom, setCreatingRoom] = useState(false);
|
||||
const [createRoomError, setCreateRoomError] = useState();
|
||||
|
||||
const onCreateRoom = useCallback(
|
||||
(roomName) => {
|
||||
setCreateRoomError(undefined);
|
||||
setCreatingRoom(true);
|
||||
|
||||
return createRoom(client, roomName).catch((error) => {
|
||||
setCreateRoomError(error);
|
||||
setCreatingRoom(false);
|
||||
});
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return {
|
||||
loading,
|
||||
authenticated,
|
||||
client,
|
||||
login,
|
||||
registerGuest,
|
||||
register,
|
||||
logout,
|
||||
creatingRoom,
|
||||
createRoomError,
|
||||
createRoom: onCreateRoom,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCreateRoomAsPasswordlessUser() {
|
||||
const { register } = useClient();
|
||||
const [creatingRoom, setCreatingRoom] = useState(false);
|
||||
const [createRoomError, setCreateRoomError] = useState();
|
||||
|
||||
const onCreateRoom = useCallback(
|
||||
(roomName, userName) => {
|
||||
async function onCreateRoom(roomName, userName) {
|
||||
const client = await register(userName, randomString(16));
|
||||
return await createRoom(client, roomName);
|
||||
}
|
||||
|
||||
setCreateRoomError(undefined);
|
||||
setCreatingRoom(true);
|
||||
|
||||
return onCreateRoom(roomName, userName).catch((error) => {
|
||||
setCreateRoomError(error);
|
||||
setCreatingRoom(false);
|
||||
});
|
||||
},
|
||||
[register]
|
||||
);
|
||||
|
||||
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) {
|
||||
|
@ -324,6 +539,10 @@ export function useGroupCallRooms(client) {
|
|||
const groupCall = client.getGroupCallForRoom(room.roomId);
|
||||
|
||||
return {
|
||||
roomId: room.roomId,
|
||||
roomName: room.name,
|
||||
avatarUrl: null,
|
||||
roomUrl: `/room/${room.getCanonicalAlias() || room.roomId}`,
|
||||
room,
|
||||
groupCall,
|
||||
participants: [...groupCall.participants],
|
||||
|
@ -352,9 +571,16 @@ export function usePublicRooms(client, publicSpaceRoomId, maxRooms = 50) {
|
|||
useEffect(() => {
|
||||
if (publicSpaceRoomId) {
|
||||
client.getRoomHierarchy(publicSpaceRoomId, maxRooms).then(({ rooms }) => {
|
||||
const filteredRooms = rooms.filter(
|
||||
(room) => room.room_type !== "m.space"
|
||||
);
|
||||
const filteredRooms = rooms
|
||||
.filter((room) => room.room_type !== "m.space")
|
||||
.map((room) => ({
|
||||
roomId: room.room_id,
|
||||
roomName: room.name,
|
||||
avatarUrl: null,
|
||||
room,
|
||||
roomUrl: `/room/${room.room_id}`,
|
||||
participants: [],
|
||||
}));
|
||||
|
||||
setRooms(filteredRooms);
|
||||
});
|
|
@ -1,49 +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, HeaderLogo } 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>
|
||||
<HeaderLogo />
|
||||
</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;
|
||||
}
|
310
src/Home.jsx
310
src/Home.jsx
|
@ -14,110 +14,57 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
useClient,
|
||||
useGroupCallRooms,
|
||||
usePublicRooms,
|
||||
useCreateRoom,
|
||||
useCreateRoomAsPasswordlessUser,
|
||||
} from "./ConferenceCallManagerHooks";
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||
import styles from "./Home.module.css";
|
||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||
import {
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
} from "matrix-js-sdk/src/browser-index";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
import { Button } from "./button";
|
||||
import { CallTile } from "./CallTile";
|
||||
import { CallList } from "./CallList";
|
||||
import classNames from "classnames";
|
||||
import { ErrorModal } from "./ErrorModal";
|
||||
|
||||
function roomAliasFromRoomName(roomName) {
|
||||
return roomName
|
||||
.trim()
|
||||
.replace(/\s/g, "-")
|
||||
.replace(/[^\w-]/g, "")
|
||||
.toLowerCase();
|
||||
export function Home() {
|
||||
const { isAuthenticated, isGuest, loading, error, client } = useClient();
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
} else if (error) {
|
||||
return <ErrorModal error={error} />;
|
||||
} else if (!isAuthenticated || isGuest) {
|
||||
return <UnregisteredView />;
|
||||
} else {
|
||||
return <RegisteredView client={client} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function Home({ client, onLogout }) {
|
||||
function UnregisteredView() {
|
||||
const history = useHistory();
|
||||
const [roomName, setRoomName] = useState("");
|
||||
const [guestAccess, setGuestAccess] = useState(false);
|
||||
const [createRoomError, setCreateRoomError] = useState();
|
||||
const rooms = useGroupCallRooms(client);
|
||||
const publicRooms = usePublicRooms(
|
||||
client,
|
||||
import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
|
||||
);
|
||||
const { createRoomError, creatingRoom, createRoom } =
|
||||
useCreateRoomAsPasswordlessUser();
|
||||
|
||||
const onCreateRoom = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
setCreateRoomError(undefined);
|
||||
|
||||
async function createRoom(name, roomAlias, guestAccess) {
|
||||
const { room_id, room_alias } = await client.createRoom({
|
||||
visibility: "private",
|
||||
preset: "public_chat",
|
||||
name,
|
||||
room_alias_name: roomAlias,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
await client.createGroupCall(
|
||||
room_id,
|
||||
GroupCallType.Video,
|
||||
GroupCallIntent.Prompt
|
||||
);
|
||||
|
||||
history.push(`/room/${room_alias || room_id}`);
|
||||
}
|
||||
|
||||
const data = new FormData(e.target);
|
||||
const roomName = data.get("roomName");
|
||||
const guestAccess = data.get("guestAccess");
|
||||
const userName = data.get("userName");
|
||||
|
||||
createRoom(roomName, roomAliasFromRoomName(roomName), guestAccess).catch(
|
||||
(error) => {
|
||||
setCreateRoomError(error);
|
||||
setShowAdvanced(true);
|
||||
}
|
||||
);
|
||||
createRoom(roomName, userName).then((roomIdOrAlias) => {
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
});
|
||||
},
|
||||
[client]
|
||||
[history]
|
||||
);
|
||||
|
||||
const [roomId, setRoomId] = useState("");
|
||||
|
||||
const onJoinRoom = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -129,12 +76,134 @@ export function Home({ client, onLogout }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<div class={styles.home}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.home}>
|
||||
<div className={classNames(styles.left, styles.fullWidth)}>
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<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="userName"
|
||||
name="userName"
|
||||
label="Username"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder="Username"
|
||||
/>
|
||||
</FieldRow>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RegisteredView({ client }) {
|
||||
const history = useHistory();
|
||||
const { createRoomError, creatingRoom, createRoom } = useCreateRoom(client);
|
||||
|
||||
const onCreateRoom = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const roomName = data.get("roomName");
|
||||
|
||||
createRoom(roomName).then((roomIdOrAlias) => {
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
});
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
const onJoinRoom = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const roomId = data.get("roomId");
|
||||
history.push(`/room/${roomId}`);
|
||||
},
|
||||
[history]
|
||||
);
|
||||
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={styles.home}>
|
||||
<div
|
||||
className={classNames(styles.left, {
|
||||
[styles.fullWidth]: hideCallList,
|
||||
})}
|
||||
>
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
{hideCallList && (
|
||||
<RightNav>
|
||||
<UserMenu />
|
||||
</RightNav>
|
||||
)}
|
||||
</Header>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.centered}>
|
||||
|
@ -149,8 +218,6 @@ export function Home({ client, onLogout }) {
|
|||
required
|
||||
autoComplete="off"
|
||||
placeholder="Call ID"
|
||||
value={roomId}
|
||||
onChange={(e) => setRoomId(e.target.value)}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
|
@ -171,18 +238,6 @@ export function Home({ client, onLogout }) {
|
|||
required
|
||||
autoComplete="off"
|
||||
placeholder="Room Name"
|
||||
value={roomName}
|
||||
onChange={(e) => setRoomName(e.target.value)}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="guestAccess"
|
||||
name="guestAccess"
|
||||
label="Allow Guest Access"
|
||||
type="checkbox"
|
||||
checked={guestAccess}
|
||||
onChange={(e) => setGuestAccess(e.target.checked)}
|
||||
/>
|
||||
</FieldRow>
|
||||
{createRoomError && (
|
||||
|
@ -191,55 +246,36 @@ export function Home({ client, onLogout }) {
|
|||
</FieldRow>
|
||||
)}
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<Button className={styles.button} type="submit">
|
||||
Create call
|
||||
<Button
|
||||
className={styles.button}
|
||||
type="submit"
|
||||
disabled={creatingRoom}
|
||||
>
|
||||
{creatingRoom ? "Creating call..." : "Create call"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
<RightNav>
|
||||
<UserMenu
|
||||
signedIn
|
||||
userName={client.getUserIdLocalpart()}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.content}>
|
||||
{publicRooms.length > 0 && (
|
||||
<>
|
||||
<h3>Public Calls</h3>
|
||||
<div className={styles.roomList}>
|
||||
{publicRooms.map((room) => (
|
||||
<CallTile
|
||||
key={room.room_id}
|
||||
name={room.name}
|
||||
avatarUrl={null}
|
||||
roomUrl={`/room/${room.room_id}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<h3>Recent Calls</h3>
|
||||
<div className={styles.roomList}>
|
||||
{rooms.map(({ room, participants }) => (
|
||||
<CallTile
|
||||
key={room.roomId}
|
||||
name={room.name}
|
||||
avatarUrl={null}
|
||||
roomUrl={`/room/${room.getCanonicalAlias() || room.roomId}`}
|
||||
participants={participants}
|
||||
/>
|
||||
))}
|
||||
{!hideCallList && (
|
||||
<div className={styles.right}>
|
||||
<Header>
|
||||
<LeftNav />
|
||||
<RightNav>
|
||||
<UserMenu />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,11 @@
|
|||
background-color: var(--bgColor2);
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
background-color: var(--bgColor1);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -65,6 +70,10 @@
|
|||
top: -12px;
|
||||
}
|
||||
|
||||
.fullWidth .content hr:after {
|
||||
background-color: var(--bgColor1);
|
||||
}
|
||||
|
||||
.left .content form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -99,11 +108,3 @@
|
|||
.right .content h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.roomList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 24px;
|
||||
flex: 1;
|
||||
}
|
||||
|
|
|
@ -20,8 +20,10 @@ import { Header, HeaderLogo, LeftNav } from "./Header";
|
|||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||
import { Center, Content, Info, Modal } from "./Layout";
|
||||
import { Button } from "./button";
|
||||
import { useClient } from "./ConferenceCallManagerHooks";
|
||||
|
||||
export function LoginPage({ onLogin }) {
|
||||
export function LoginPage() {
|
||||
const { login } = useClient();
|
||||
const [homeserver, setHomeServer] = useState(
|
||||
`${window.location.protocol}//${window.location.host}`
|
||||
);
|
||||
|
@ -32,17 +34,19 @@ export function LoginPage({ onLogin }) {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
|
||||
// TODO: Handle hitting login page with authenticated client
|
||||
|
||||
const onSubmitLoginForm = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
onLogin(homeserver, usernameRef.current.value, passwordRef.current.value)
|
||||
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((error) => {
|
||||
|
@ -50,7 +54,7 @@ export function LoginPage({ onLogin }) {
|
|||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[onLogin, location, history, homeserver]
|
||||
[login, location, history, homeserver]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -20,8 +20,11 @@ import { Header, LeftNav, HeaderLogo } from "./Header";
|
|||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||
import { Center, Content, Info, Modal } from "./Layout";
|
||||
import { Button } from "./button";
|
||||
import { useClient } from "./ConferenceCallManagerHooks";
|
||||
|
||||
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();
|
||||
|
@ -33,15 +36,15 @@ export function RegisterPage({ onRegister }) {
|
|||
(e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
onRegister(
|
||||
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((error) => {
|
||||
|
@ -49,7 +52,7 @@ export function RegisterPage({ onRegister }) {
|
|||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[onRegister, location, history]
|
||||
[register, location, history]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
62
src/Room.jsx
62
src/Room.jsx
|
@ -42,7 +42,7 @@ 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 { useClient, useLoadGroupCall } from "./ConferenceCallManagerHooks";
|
||||
import { ErrorModal } from "./ErrorModal";
|
||||
import { GroupCallInspector } from "./GroupCallInspector";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
@ -56,24 +56,39 @@ 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, viaServers) {
|
||||
const [state, setState] = useState({
|
||||
loading: true,
|
||||
error: undefined,
|
||||
groupCall: undefined,
|
||||
});
|
||||
export function Room() {
|
||||
const [registeringGuest, setRegisteringGuest] = useState(false);
|
||||
const [registrationError, setRegistrationError] = useState();
|
||||
const { loading, isAuthenticated, error, client, registerGuest } =
|
||||
useClient();
|
||||
|
||||
useEffect(() => {
|
||||
setState({ loading: true });
|
||||
fetchGroupCall(client, roomId, viaServers, 30000)
|
||||
.then((groupCall) => setState({ loading: false, groupCall }))
|
||||
.catch((error) => setState({ loading: false, error }));
|
||||
}, [roomId]);
|
||||
if (!loading && !isAuthenticated) {
|
||||
setRegisteringGuest(true);
|
||||
|
||||
return state;
|
||||
registerGuest()
|
||||
.then(() => {
|
||||
setRegisteringGuest(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setRegistrationError(error);
|
||||
setRegisteringGuest(false);
|
||||
});
|
||||
}
|
||||
}, [loading, isAuthenticated]);
|
||||
|
||||
if (loading || registeringGuest) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (registrationError || error) {
|
||||
return <ErrorModal error={registrationError || error} />;
|
||||
}
|
||||
|
||||
return <GroupCall client={client} />;
|
||||
}
|
||||
|
||||
export function Room({ client, onLogout }) {
|
||||
export function GroupCall({ client }) {
|
||||
const { roomId: maybeRoomId } = useParams();
|
||||
const { hash, search } = useLocation();
|
||||
const [simpleGrid, viaServers] = useMemo(() => {
|
||||
|
@ -115,7 +130,6 @@ export function Room({ client, onLogout }) {
|
|||
return (
|
||||
<div className={styles.room}>
|
||||
<GroupCallView
|
||||
onLogout={onLogout}
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
simpleGrid={simpleGrid}
|
||||
|
@ -124,7 +138,7 @@ export function Room({ client, onLogout }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function GroupCallView({ client, groupCall, simpleGrid, onLogout }) {
|
||||
export function GroupCallView({ client, groupCall, simpleGrid }) {
|
||||
const [showInspector, setShowInspector] = useState(false);
|
||||
const {
|
||||
state,
|
||||
|
@ -184,7 +198,6 @@ export function GroupCallView({ client, groupCall, simpleGrid, onLogout }) {
|
|||
} else if (state === GroupCallState.Entered) {
|
||||
return (
|
||||
<InRoomView
|
||||
onLogout={onLogout}
|
||||
groupCall={groupCall}
|
||||
client={client}
|
||||
roomName={groupCall.room.name}
|
||||
|
@ -209,7 +222,6 @@ export function GroupCallView({ client, groupCall, simpleGrid, onLogout }) {
|
|||
} else {
|
||||
return (
|
||||
<RoomSetupView
|
||||
onLogout={onLogout}
|
||||
client={client}
|
||||
hasLocalParticipant={hasLocalParticipant}
|
||||
roomName={groupCall.room.name}
|
||||
|
@ -249,7 +261,6 @@ export function EnteringRoomView() {
|
|||
}
|
||||
|
||||
function RoomSetupView({
|
||||
onLogout,
|
||||
client,
|
||||
roomName,
|
||||
state,
|
||||
|
@ -282,11 +293,7 @@ function RoomSetupView({
|
|||
/>
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu
|
||||
signedIn
|
||||
userName={client.getUserIdLocalpart()}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
<UserMenu />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.joinRoom}>
|
||||
|
@ -347,7 +354,6 @@ function RoomSetupView({
|
|||
}
|
||||
|
||||
function InRoomView({
|
||||
onLogout,
|
||||
client,
|
||||
groupCall,
|
||||
roomName,
|
||||
|
@ -424,11 +430,7 @@ function InRoomView({
|
|||
</LeftNav>
|
||||
<RightNav>
|
||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||
<UserMenu
|
||||
signedIn
|
||||
userName={client.getUserIdLocalpart()}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
<UserMenu />
|
||||
</RightNav>
|
||||
</Header>
|
||||
{items.length === 0 ? (
|
||||
|
|
|
@ -7,45 +7,67 @@ 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 } from "./ConferenceCallManagerHooks";
|
||||
|
||||
export function UserMenu({ userName, signedIn, onLogin, onLogout }) {
|
||||
const onAction = useCallback((value) => {
|
||||
switch (value) {
|
||||
case "user":
|
||||
break;
|
||||
case "logout":
|
||||
onLogout();
|
||||
break;
|
||||
case "login":
|
||||
onLogin();
|
||||
break;
|
||||
}
|
||||
});
|
||||
export function UserMenu() {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const { isAuthenticated, isGuest, logout, userName } = useClient();
|
||||
|
||||
const onAction = useCallback(
|
||||
(value) => {
|
||||
switch (value) {
|
||||
case "user":
|
||||
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]
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (signedIn) {
|
||||
return [
|
||||
{
|
||||
key: "user",
|
||||
icon: UserIcon,
|
||||
label: userName,
|
||||
},
|
||||
{
|
||||
key: "logout",
|
||||
label: "Sign Out",
|
||||
icon: LogoutIcon,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
const arr = [];
|
||||
|
||||
if (isAuthenticated) {
|
||||
arr.push({
|
||||
key: "user",
|
||||
icon: UserIcon,
|
||||
label: userName,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isAuthenticated || isGuest) {
|
||||
arr.push(
|
||||
{
|
||||
key: "login",
|
||||
label: "Sign In",
|
||||
icon: LoginIcon,
|
||||
},
|
||||
];
|
||||
{
|
||||
key: "register",
|
||||
label: "Register",
|
||||
icon: LoginIcon,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
arr.push({
|
||||
key: "logout",
|
||||
label: "Sign Out",
|
||||
icon: LogoutIcon,
|
||||
});
|
||||
}
|
||||
}, [signedIn, userName]);
|
||||
|
||||
return arr;
|
||||
}, [isAuthenticated, isGuest, userName]);
|
||||
|
||||
return (
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
|
|
Loading…
Add table
Reference in a new issue