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

View file

@ -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

View file

@ -52,3 +52,11 @@
width: 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.
*/
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);
});

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.
*/
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>
);
}

View file

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

View file

@ -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 (

View file

@ -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 (

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 { 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 ? (

View file

@ -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">