Passwordless user flow

This commit is contained in:
Robert Long 2021-12-09 12:58:30 -08:00
parent 20350e66a2
commit fc3960ce63
12 changed files with 589 additions and 369 deletions

View file

@ -15,86 +15,42 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
BrowserRouter as Router,
Switch,
Route,
Redirect,
useLocation,
} from "react-router-dom";
import * as Sentry from "@sentry/react"; 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 { 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); const SentryRoute = Sentry.withSentryRouting(Route);
export default function App() { const { protocol, host } = window.location;
const { protocol, host } = window.location; // Assume homeserver is hosted on same domain (proxied in development by vite)
// Assume homeserver is hosted on same domain (proxied in development by vite) const homeserverUrl = `${protocol}//${host}`;
const homeserverUrl = `${protocol}//${host}`;
const {
loading,
authenticated,
client,
login,
logout,
registerGuest,
register,
} = useClient(homeserverUrl);
export default function App() {
return ( return (
<Router> <Router>
<OverlayProvider> <ClientProvider homeserverUrl={homeserverUrl}>
{loading ? ( <OverlayProvider>
<Center>
<p>Loading...</p>
</Center>
) : (
<Switch> <Switch>
<AuthenticatedRoute authenticated={authenticated} exact path="/"> <SentryRoute exact path="/">
<Home client={client} onLogout={logout} /> <Home />
</AuthenticatedRoute> </SentryRoute>
<SentryRoute exact path="/login"> <SentryRoute exact path="/login">
<LoginPage onLogin={login} /> <LoginPage />
</SentryRoute> </SentryRoute>
<SentryRoute exact path="/register"> <SentryRoute exact path="/register">
<RegisterPage onRegister={register} /> <RegisterPage />
</SentryRoute> </SentryRoute>
<SentryRoute path="/room/:roomId?"> <SentryRoute path="/room/:roomId?">
{authenticated ? ( <Room />
<Room client={client} onLogout={logout} />
) : (
<GuestAuthPage onLoginAsGuest={registerGuest} />
)}
</SentryRoute> </SentryRoute>
</Switch> </Switch>
)} </OverlayProvider>
</OverlayProvider> </ClientProvider>
</Router> </Router>
); );
} }
function AuthenticatedRoute({ authenticated, children, ...rest }) {
const location = useLocation();
return (
<SentryRoute {...rest}>
{authenticated ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location },
}}
/>
)}
</SentryRoute>
);
}

View file

@ -4,9 +4,28 @@ import { CopyButton } from "./button";
import { Facepile } from "./Facepile"; import { Facepile } from "./Facepile";
import { Avatar } from "./Avatar"; import { Avatar } from "./Avatar";
import { ReactComponent as VideoIcon } from "./icons/Video.svg"; 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 ( return (
<Link to={roomUrl} className={styles.callTile}> <Link to={roomUrl} className={styles.callTile}>
<Avatar <Avatar

View file

@ -52,3 +52,11 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
.callList {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 24px;
flex: 1;
}

View file

@ -14,8 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License. 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 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) { function waitForSync(client) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -86,12 +101,16 @@ export async function fetchGroupCall(
}); });
} }
export function useClient(homeserverUrl) { export function ClientProvider({ homeserverUrl, children }) {
const [{ loading, authenticated, client }, setState] = useState({ const history = useHistory();
loading: true, const [{ loading, isAuthenticated, isGuest, client, userName }, setState] =
authenticated: false, useState({
client: undefined, loading: true,
}); isAuthenticated: false,
isGuest: false,
client: undefined,
userName: null,
});
useEffect(() => { useEffect(() => {
async function restore() { async function restore() {
@ -117,8 +136,10 @@ export function useClient(homeserverUrl) {
JSON.stringify({ user_id, device_id, access_token, guest }) JSON.stringify({ user_id, device_id, access_token, guest })
); );
return client; return { client, guest };
} }
return { client: undefined, guest: false };
} catch (err) { } catch (err) {
localStorage.removeItem("matrix-auth-store"); localStorage.removeItem("matrix-auth-store");
throw err; throw err;
@ -126,15 +147,23 @@ export function useClient(homeserverUrl) {
} }
restore() restore()
.then((client) => { .then(({ client, guest }) => {
if (client) { setState({
setState({ client, loading: false, authenticated: true }); client,
} else { loading: false,
setState({ client: undefined, loading: false, authenticated: false }); isAuthenticated: !!client,
} isGuest: guest,
userName: client?.getUserIdLocalpart(),
});
}) })
.catch(() => { .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 }) 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) { } catch (err) {
localStorage.removeItem("matrix-auth-store"); 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; throw err;
} }
}, []); }, []);
@ -210,10 +251,22 @@ export function useClient(homeserverUrl) {
JSON.stringify({ user_id, device_id, access_token, guest: true }) 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) { } catch (err) {
localStorage.removeItem("matrix-auth-store"); 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; throw err;
} }
}, []); }, []);
@ -239,10 +292,25 @@ export function useClient(homeserverUrl) {
JSON.stringify({ user_id, device_id, access_token }) 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) { } catch (err) {
localStorage.removeItem("matrix-auth-store"); 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; throw err;
} }
}, []); }, []);
@ -252,17 +320,164 @@ export function useClient(homeserverUrl) {
window.location = "/"; window.location = "/";
}, [history]); }, [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 { return {
loading, creatingRoom,
authenticated, createRoomError,
client, createRoom: onCreateRoom,
login,
registerGuest,
register,
logout,
}; };
} }
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 = {}; const tsCache = {};
function getLastTs(client, r) { function getLastTs(client, r) {
@ -324,6 +539,10 @@ export function useGroupCallRooms(client) {
const groupCall = client.getGroupCallForRoom(room.roomId); const groupCall = client.getGroupCallForRoom(room.roomId);
return { return {
roomId: room.roomId,
roomName: room.name,
avatarUrl: null,
roomUrl: `/room/${room.getCanonicalAlias() || room.roomId}`,
room, room,
groupCall, groupCall,
participants: [...groupCall.participants], participants: [...groupCall.participants],
@ -352,9 +571,16 @@ export function usePublicRooms(client, publicSpaceRoomId, maxRooms = 50) {
useEffect(() => { useEffect(() => {
if (publicSpaceRoomId) { if (publicSpaceRoomId) {
client.getRoomHierarchy(publicSpaceRoomId, maxRooms).then(({ rooms }) => { client.getRoomHierarchy(publicSpaceRoomId, maxRooms).then(({ rooms }) => {
const filteredRooms = rooms.filter( const filteredRooms = rooms
(room) => room.room_type !== "m.space" .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); setRooms(filteredRooms);
}); });

View file

@ -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>
);
}

View file

@ -1,8 +0,0 @@
.guestAuthPage {
position: relative;
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
overflow: hidden;
}

View file

@ -14,110 +14,57 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useCallback, useState } from "react"; import React, { useCallback } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { import {
useClient,
useGroupCallRooms, useGroupCallRooms,
usePublicRooms, usePublicRooms,
useCreateRoom,
useCreateRoomAsPasswordlessUser,
} from "./ConferenceCallManagerHooks"; } from "./ConferenceCallManagerHooks";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header"; import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
import styles from "./Home.module.css"; import styles from "./Home.module.css";
import { FieldRow, InputField, ErrorMessage } from "./Input"; import { FieldRow, InputField, ErrorMessage } from "./Input";
import {
GroupCallIntent,
GroupCallType,
} from "matrix-js-sdk/src/browser-index";
import { UserMenu } from "./UserMenu"; import { UserMenu } from "./UserMenu";
import { Button } from "./button"; import { Button } from "./button";
import { CallTile } from "./CallTile"; import { CallList } from "./CallList";
import classNames from "classnames";
import { ErrorModal } from "./ErrorModal";
function roomAliasFromRoomName(roomName) { export function Home() {
return roomName const { isAuthenticated, isGuest, loading, error, client } = useClient();
.trim()
.replace(/\s/g, "-") if (loading) {
.replace(/[^\w-]/g, "") return <div>Loading...</div>;
.toLowerCase(); } 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 history = useHistory();
const [roomName, setRoomName] = useState(""); const { createRoomError, creatingRoom, createRoom } =
const [guestAccess, setGuestAccess] = useState(false); useCreateRoomAsPasswordlessUser();
const [createRoomError, setCreateRoomError] = useState();
const rooms = useGroupCallRooms(client);
const publicRooms = usePublicRooms(
client,
import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
);
const onCreateRoom = useCallback( const onCreateRoom = useCallback(
(e) => { (e) => {
e.preventDefault(); 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 data = new FormData(e.target);
const roomName = data.get("roomName"); const roomName = data.get("roomName");
const guestAccess = data.get("guestAccess"); const userName = data.get("userName");
createRoom(roomName, roomAliasFromRoomName(roomName), guestAccess).catch( createRoom(roomName, userName).then((roomIdOrAlias) => {
(error) => { history.push(`/room/${roomIdOrAlias}`);
setCreateRoomError(error); });
setShowAdvanced(true);
}
);
}, },
[client] [history]
); );
const [roomId, setRoomId] = useState("");
const onJoinRoom = useCallback( const onJoinRoom = useCallback(
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
@ -129,12 +76,134 @@ export function Home({ client, onLogout }) {
); );
return ( return (
<div class={styles.home}> <div className={styles.home}>
<div className={styles.left}> <div className={classNames(styles.left, styles.fullWidth)}>
<Header> <Header>
<LeftNav> <LeftNav>
<HeaderLogo /> <HeaderLogo />
</LeftNav> </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> </Header>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.centered}> <div className={styles.centered}>
@ -149,8 +218,6 @@ export function Home({ client, onLogout }) {
required required
autoComplete="off" autoComplete="off"
placeholder="Call ID" placeholder="Call ID"
value={roomId}
onChange={(e) => setRoomId(e.target.value)}
/> />
</FieldRow> </FieldRow>
<FieldRow className={styles.fieldRow}> <FieldRow className={styles.fieldRow}>
@ -171,18 +238,6 @@ export function Home({ client, onLogout }) {
required required
autoComplete="off" autoComplete="off"
placeholder="Room Name" 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> </FieldRow>
{createRoomError && ( {createRoomError && (
@ -191,55 +246,36 @@ export function Home({ client, onLogout }) {
</FieldRow> </FieldRow>
)} )}
<FieldRow className={styles.fieldRow}> <FieldRow className={styles.fieldRow}>
<Button className={styles.button} type="submit"> <Button
Create call className={styles.button}
type="submit"
disabled={creatingRoom}
>
{creatingRoom ? "Creating call..." : "Create call"}
</Button> </Button>
</FieldRow> </FieldRow>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<div className={styles.right}> {!hideCallList && (
<Header> <div className={styles.right}>
<LeftNav /> <Header>
<RightNav> <LeftNav />
<UserMenu <RightNav>
signedIn <UserMenu />
userName={client.getUserIdLocalpart()} </RightNav>
onLogout={onLogout} </Header>
/> <div className={styles.content}>
</RightNav> {publicRooms.length > 0 && (
</Header> <CallList title="Public Calls" rooms={publicRooms} />
<div className={styles.content}> )}
{publicRooms.length > 0 && ( {recentRooms.length > 0 && (
<> <CallList title="Recent Calls" rooms={recentRooms} />
<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}
/>
))}
</div> </div>
</div> </div>
</div> )}
</div> </div>
); );
} }

View file

@ -15,6 +15,11 @@
background-color: var(--bgColor2); background-color: var(--bgColor2);
} }
.fullWidth {
background-color: var(--bgColor1);
overflow-y: auto;
}
.centered { .centered {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -65,6 +70,10 @@
top: -12px; top: -12px;
} }
.fullWidth .content hr:after {
background-color: var(--bgColor1);
}
.left .content form { .left .content form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -99,11 +108,3 @@
.right .content h3:first-child { .right .content h3:first-child {
margin-top: 0; margin-top: 0;
} }
.roomList {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 24px;
flex: 1;
}

View file

@ -20,8 +20,10 @@ import { Header, HeaderLogo, LeftNav } from "./Header";
import { FieldRow, InputField, ErrorMessage } from "./Input"; import { FieldRow, InputField, ErrorMessage } from "./Input";
import { Center, Content, Info, Modal } from "./Layout"; import { Center, Content, Info, Modal } from "./Layout";
import { Button } from "./button"; import { Button } from "./button";
import { useClient } from "./ConferenceCallManagerHooks";
export function LoginPage({ onLogin }) { export function LoginPage() {
const { login } = useClient();
const [homeserver, setHomeServer] = useState( const [homeserver, setHomeServer] = useState(
`${window.location.protocol}//${window.location.host}` `${window.location.protocol}//${window.location.host}`
); );
@ -32,17 +34,19 @@ export function LoginPage({ onLogin }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(); const [error, setError] = useState();
// TODO: Handle hitting login page with authenticated client
const onSubmitLoginForm = useCallback( const onSubmitLoginForm = useCallback(
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
onLogin(homeserver, usernameRef.current.value, passwordRef.current.value) login(homeserver, usernameRef.current.value, passwordRef.current.value)
.then(() => { .then(() => {
if (location.state && location.state.from) { if (location.state && location.state.from) {
history.replace(location.state.from); history.push(location.state.from);
} else { } else {
history.replace("/"); history.push("/");
} }
}) })
.catch((error) => { .catch((error) => {
@ -50,7 +54,7 @@ export function LoginPage({ onLogin }) {
setLoading(false); setLoading(false);
}); });
}, },
[onLogin, location, history, homeserver] [login, location, history, homeserver]
); );
return ( return (

View file

@ -20,8 +20,11 @@ import { Header, LeftNav, HeaderLogo } from "./Header";
import { FieldRow, InputField, ErrorMessage } from "./Input"; import { FieldRow, InputField, ErrorMessage } from "./Input";
import { Center, Content, Info, Modal } from "./Layout"; import { Center, Content, Info, Modal } from "./Layout";
import { Button } from "./button"; 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 registerUsernameRef = useRef();
const registerPasswordRef = useRef(); const registerPasswordRef = useRef();
const history = useHistory(); const history = useHistory();
@ -33,15 +36,15 @@ export function RegisterPage({ onRegister }) {
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
onRegister( register(
registerUsernameRef.current.value, registerUsernameRef.current.value,
registerPasswordRef.current.value registerPasswordRef.current.value
) )
.then(() => { .then(() => {
if (location.state && location.state.from) { if (location.state && location.state.from) {
history.replace(location.state.from); history.push(location.state.from);
} else { } else {
history.replace("/"); history.push("/");
} }
}) })
.catch((error) => { .catch((error) => {
@ -49,7 +52,7 @@ export function RegisterPage({ onRegister }) {
setLoading(false); setLoading(false);
}); });
}, },
[onRegister, location, history] [register, location, history]
); );
return ( return (

View file

@ -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 { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed"; import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed";
import { useMediaStream } from "matrix-react-sdk/src/hooks/useMediaStream"; import { useMediaStream } from "matrix-react-sdk/src/hooks/useMediaStream";
import { fetchGroupCall } from "./ConferenceCallManagerHooks"; import { useClient, useLoadGroupCall } from "./ConferenceCallManagerHooks";
import { ErrorModal } from "./ErrorModal"; import { ErrorModal } from "./ErrorModal";
import { GroupCallInspector } from "./GroupCallInspector"; import { GroupCallInspector } from "./GroupCallInspector";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
@ -56,24 +56,39 @@ const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
// For now we can disable screensharing in Safari. // For now we can disable screensharing in Safari.
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
function useLoadGroupCall(client, roomId, viaServers) { export function Room() {
const [state, setState] = useState({ const [registeringGuest, setRegisteringGuest] = useState(false);
loading: true, const [registrationError, setRegistrationError] = useState();
error: undefined, const { loading, isAuthenticated, error, client, registerGuest } =
groupCall: undefined, useClient();
});
useEffect(() => { useEffect(() => {
setState({ loading: true }); if (!loading && !isAuthenticated) {
fetchGroupCall(client, roomId, viaServers, 30000) setRegisteringGuest(true);
.then((groupCall) => setState({ loading: false, groupCall }))
.catch((error) => setState({ loading: false, error }));
}, [roomId]);
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 { roomId: maybeRoomId } = useParams();
const { hash, search } = useLocation(); const { hash, search } = useLocation();
const [simpleGrid, viaServers] = useMemo(() => { const [simpleGrid, viaServers] = useMemo(() => {
@ -115,7 +130,6 @@ export function Room({ client, onLogout }) {
return ( return (
<div className={styles.room}> <div className={styles.room}>
<GroupCallView <GroupCallView
onLogout={onLogout}
client={client} client={client}
groupCall={groupCall} groupCall={groupCall}
simpleGrid={simpleGrid} 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 [showInspector, setShowInspector] = useState(false);
const { const {
state, state,
@ -184,7 +198,6 @@ export function GroupCallView({ client, groupCall, simpleGrid, onLogout }) {
} else if (state === GroupCallState.Entered) { } else if (state === GroupCallState.Entered) {
return ( return (
<InRoomView <InRoomView
onLogout={onLogout}
groupCall={groupCall} groupCall={groupCall}
client={client} client={client}
roomName={groupCall.room.name} roomName={groupCall.room.name}
@ -209,7 +222,6 @@ export function GroupCallView({ client, groupCall, simpleGrid, onLogout }) {
} else { } else {
return ( return (
<RoomSetupView <RoomSetupView
onLogout={onLogout}
client={client} client={client}
hasLocalParticipant={hasLocalParticipant} hasLocalParticipant={hasLocalParticipant}
roomName={groupCall.room.name} roomName={groupCall.room.name}
@ -249,7 +261,6 @@ export function EnteringRoomView() {
} }
function RoomSetupView({ function RoomSetupView({
onLogout,
client, client,
roomName, roomName,
state, state,
@ -282,11 +293,7 @@ function RoomSetupView({
/> />
</LeftNav> </LeftNav>
<RightNav> <RightNav>
<UserMenu <UserMenu />
signedIn
userName={client.getUserIdLocalpart()}
onLogout={onLogout}
/>
</RightNav> </RightNav>
</Header> </Header>
<div className={styles.joinRoom}> <div className={styles.joinRoom}>
@ -347,7 +354,6 @@ function RoomSetupView({
} }
function InRoomView({ function InRoomView({
onLogout,
client, client,
groupCall, groupCall,
roomName, roomName,
@ -424,11 +430,7 @@ function InRoomView({
</LeftNav> </LeftNav>
<RightNav> <RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} /> <GridLayoutMenu layout={layout} setLayout={setLayout} />
<UserMenu <UserMenu />
signedIn
userName={client.getUserIdLocalpart()}
onLogout={onLogout}
/>
</RightNav> </RightNav>
</Header> </Header>
{items.length === 0 ? ( {items.length === 0 ? (

View file

@ -7,45 +7,67 @@ import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
import styles from "./UserMenu.module.css"; import styles from "./UserMenu.module.css";
import { Item } from "@react-stately/collections"; import { Item } from "@react-stately/collections";
import { Menu } from "./Menu"; import { Menu } from "./Menu";
import { useHistory, useLocation } from "react-router-dom";
import { useClient } from "./ConferenceCallManagerHooks";
export function UserMenu({ userName, signedIn, onLogin, onLogout }) { export function UserMenu() {
const onAction = useCallback((value) => { const location = useLocation();
switch (value) { const history = useHistory();
case "user": const { isAuthenticated, isGuest, logout, userName } = useClient();
break;
case "logout": const onAction = useCallback(
onLogout(); (value) => {
break; switch (value) {
case "login": case "user":
onLogin(); break;
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(() => { const items = useMemo(() => {
if (signedIn) { const arr = [];
return [
{ if (isAuthenticated) {
key: "user", arr.push({
icon: UserIcon, key: "user",
label: userName, icon: UserIcon,
}, label: userName,
{ });
key: "logout", }
label: "Sign Out",
icon: LogoutIcon, if (!isAuthenticated || isGuest) {
}, arr.push(
];
} else {
return [
{ {
key: "login", key: "login",
label: "Sign In", label: "Sign In",
icon: LoginIcon, 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 ( return (
<PopoverMenuTrigger placement="bottom right"> <PopoverMenuTrigger placement="bottom right">