Add guest access for rooms

This commit is contained in:
Robert Long 2021-09-03 15:45:07 -07:00
parent 196c8eeeeb
commit 46f8eb84fc
11 changed files with 338 additions and 60 deletions

View file

@ -24,7 +24,7 @@ import {
} from "react-router-dom"; } from "react-router-dom";
import { useConferenceCallManager } from "./ConferenceCallManagerHooks"; import { useConferenceCallManager } from "./ConferenceCallManagerHooks";
import { Home } from "./Home"; import { Home } from "./Home";
import { Room } from "./Room"; import { Room, RoomAuth } from "./Room";
import { GridDemo } from "./GridDemo"; import { GridDemo } from "./GridDemo";
import { RegisterPage } from "./RegisterPage"; import { RegisterPage } from "./RegisterPage";
import { LoginPage } from "./LoginPage"; import { LoginPage } from "./LoginPage";
@ -34,8 +34,15 @@ 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, error, manager, login, register } = const {
useConferenceCallManager(homeserverUrl); loading,
authenticated,
error,
manager,
login,
loginAsGuest,
register,
} = useConferenceCallManager(homeserverUrl);
return ( return (
<Router> <Router>
@ -55,12 +62,13 @@ export default function App() {
<Route exact path="/register"> <Route exact path="/register">
<RegisterPage onRegister={register} error={error} /> <RegisterPage onRegister={register} error={error} />
</Route> </Route>
<AuthenticatedRoute <Route path="/room/:roomId">
authenticated={authenticated} {authenticated ? (
path="/room/:roomId" <Room manager={manager} error={error} />
> ) : (
<Room manager={manager} error={error} /> <RoomAuth error={error} onLoginAsGuest={loginAsGuest} />
</AuthenticatedRoute> )}
</Route>
<Route exact path="/grid"> <Route exact path="/grid">
<GridDemo /> <GridDemo />
</Route> </Route>

View file

@ -107,6 +107,36 @@ export class ConferenceCallManager extends EventEmitter {
} }
} }
static async loginAsGuest(homeserverUrl, displayName) {
const registrationClient = matrixcs.createClient(homeserverUrl);
const { user_id, device_id, access_token } =
await registrationClient.registerGuest();
const client = matrixcs.createClient({
baseUrl: homeserverUrl,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
await client.setDisplayName(displayName);
client.setGuest(true);
const manager = new ConferenceCallManager(client);
await client.startClient({
// dirty hack to reduce chance of gappy syncs
// should be fixed by spotting gaps and backpaginating
initialSyncLimit: 50,
});
await waitForSync(client);
return manager;
}
static async register(homeserverUrl, username, password) { static async register(homeserverUrl, username, password) {
try { try {
const registrationClient = matrixcs.createClient(homeserverUrl); const registrationClient = matrixcs.createClient(homeserverUrl);
@ -250,6 +280,7 @@ export class ConferenceCallManager extends EventEmitter {
this.localParticipant = { this.localParticipant = {
local: true, local: true,
userId, userId,
displayName: this.client.getUser(this.client.getUserId()).rawDisplayName,
sessionId: this.sessionId, sessionId: this.sessionId,
call: null, call: null,
stream, stream,
@ -478,12 +509,15 @@ export class ConferenceCallManager extends EventEmitter {
"m.room.member", "m.room.member",
participant.userId participant.userId
); );
const participantInfo = memberStateEvent.getContent()[CONF_PARTICIPANT];
const memberStateContent = memberStateEvent.getContent();
if ( if (
!participantInfo || !memberStateContent ||
typeof participantInfo !== "object" || !memberStateContent[CONF_PARTICIPANT] ||
(participantInfo.expiresAt && participantInfo.expiresAt < now) typeof memberStateContent[CONF_PARTICIPANT] !== "object" ||
(memberStateContent[CONF_PARTICIPANT].expiresAt &&
memberStateContent[CONF_PARTICIPANT].expiresAt < now)
) { ) {
this.emit("debugstate", participant.userId, null, "inactive"); this.emit("debugstate", participant.userId, null, "inactive");
@ -533,27 +567,21 @@ export class ConferenceCallManager extends EventEmitter {
const audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false; const audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false;
const videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false; const videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false;
if (remoteFeed) {
remoteFeed.on("mute_state_changed", () =>
this._onCallFeedMuteStateChanged(participant, remoteFeed)
);
remoteFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
remoteFeed.measureVolumeActivity(true);
remoteFeed.on("speaking", (speaking) => {
this._onCallFeedSpeaking(participant, speaking);
});
remoteFeed.on("volume_changed", (maxVolume) =>
this._onCallFeedVolumeChange(participant, maxVolume)
);
}
const userId = call.opponentMember.userId; const userId = call.opponentMember.userId;
const memberStateEvent = this.room.currentState.getStateEvents( const memberStateEvent = this.room.currentState.getStateEvents(
"m.room.member", "m.room.member",
userId userId
); );
const { sessionId } = memberStateEvent.getContent()[CONF_PARTICIPANT];
const memberStateContent = memberStateEvent.getContent();
if (!memberStateContent || !memberStateContent[CONF_PARTICIPANT]) {
call.reject();
return;
}
const { sessionId } = memberStateContent[CONF_PARTICIPANT];
// Check if the user calling has an existing participant and use this call instead. // Check if the user calling has an existing participant and use this call instead.
const existingParticipant = this.participants.find( const existingParticipant = this.participants.find(
@ -562,6 +590,8 @@ export class ConferenceCallManager extends EventEmitter {
let participant; let participant;
console.log(call.opponentMember);
if (existingParticipant) { if (existingParticipant) {
participant = existingParticipant; participant = existingParticipant;
// This also fires the hangup event and triggers those side-effects // This also fires the hangup event and triggers those side-effects
@ -577,6 +607,7 @@ export class ConferenceCallManager extends EventEmitter {
participant = { participant = {
local: false, local: false,
userId, userId,
displayName: call.opponentMember.rawDisplayName,
sessionId, sessionId,
call, call,
stream, stream,
@ -588,6 +619,20 @@ export class ConferenceCallManager extends EventEmitter {
this.participants.push(participant); this.participants.push(participant);
} }
if (remoteFeed) {
remoteFeed.on("mute_state_changed", () =>
this._onCallFeedMuteStateChanged(participant, remoteFeed)
);
remoteFeed.setSpeakingThreshold(SPEAKING_THRESHOLD);
remoteFeed.measureVolumeActivity(true);
remoteFeed.on("speaking", (speaking) => {
this._onCallFeedSpeaking(participant, speaking);
});
remoteFeed.on("volume_changed", (maxVolume) =>
this._onCallFeedVolumeChange(participant, maxVolume)
);
}
call.on("state", (state) => call.on("state", (state) =>
this._onCallStateChanged(participant, call, state) this._onCallStateChanged(participant, call, state)
); );
@ -629,7 +674,13 @@ export class ConferenceCallManager extends EventEmitter {
"m.room.member", "m.room.member",
member.userId member.userId
); );
const participantInfo = memberStateEvent.getContent()[CONF_PARTICIPANT]; const memberStateContent = memberStateEvent.getContent();
if (!memberStateContent) {
return;
}
const participantInfo = memberStateContent[CONF_PARTICIPANT];
if (!participantInfo || typeof participantInfo !== "object") { if (!participantInfo || typeof participantInfo !== "object") {
return; return;
@ -680,6 +731,7 @@ export class ConferenceCallManager extends EventEmitter {
participant = { participant = {
local: false, local: false,
userId: member.userId, userId: member.userId,
displayName: member.rawDisplayName,
sessionId, sessionId,
call, call,
stream: null, stream: null,

View file

@ -95,6 +95,34 @@ export function useConferenceCallManager(homeserverUrl) {
}); });
}, []); }, []);
const loginAsGuest = useCallback(async (displayName) => {
setState((prevState) => ({
...prevState,
authenticated: false,
error: undefined,
}));
ConferenceCallManager.loginAsGuest(homeserverUrl, displayName)
.then((manager) => {
setState({
manager,
loading: false,
authenticated: true,
error: undefined,
});
})
.catch((err) => {
console.error(err);
setState({
manager: undefined,
loading: false,
authenticated: false,
error: err,
});
});
}, []);
const register = useCallback(async (username, password, cb) => { const register = useCallback(async (username, password, cb) => {
setState((prevState) => ({ setState((prevState) => ({
...prevState, ...prevState,
@ -135,7 +163,15 @@ export function useConferenceCallManager(homeserverUrl) {
}; };
}, [manager]); }, [manager]);
return { loading, authenticated, manager, error, login, register }; return {
loading,
authenticated,
manager,
error,
login,
loginAsGuest,
register,
};
} }
export function useVideoRoom(manager, roomId, timeout = 5000) { export function useVideoRoom(manager, roomId, timeout = 5000) {

View file

@ -28,6 +28,7 @@ const colorHash = new ColorHash({ lightness: 0.3 });
export function Home({ manager }) { export function Home({ manager }) {
const history = useHistory(); const history = useHistory();
const roomNameRef = useRef(); const roomNameRef = useRef();
const guestAccessRef = useRef();
const [createRoomError, setCreateRoomError] = useState(); const [createRoomError, setCreateRoomError] = useState();
const rooms = useRooms(manager); const rooms = useRooms(manager);
@ -36,16 +37,51 @@ export function Home({ manager }) {
e.preventDefault(); e.preventDefault();
setCreateRoomError(undefined); setCreateRoomError(undefined);
manager.client async function createRoom(name, guestAccess) {
.createRoom({ const { room_id } = await manager.client.createRoom({
visibility: "private", visibility: "private",
preset: "public_chat", preset: "public_chat",
name: roomNameRef.current.value, name,
}) power_level_content_override: guestAccess
.then(({ room_id }) => { ? {
history.push(`/room/${room_id}`); invite: 100,
}) kick: 100,
.catch(setCreateRoomError); 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,
},
users: {
[manager.client.getUserId()]: 100,
},
}
: undefined,
});
if (guestAccess) {
await manager.client.setGuestAccess(room_id, {
allowJoin: true,
allowRead: true,
});
}
history.push(`/room/${room_id}`);
}
createRoom(
roomNameRef.current.value,
guestAccessRef.current.checked
).catch(setCreateRoomError);
}, },
[manager] [manager]
); );
@ -87,6 +123,15 @@ export function Home({ manager }) {
ref={roomNameRef} ref={roomNameRef}
/> />
</FieldRow> </FieldRow>
<FieldRow>
<InputField
id="guestAccess"
name="guestAccess"
label="Allow Guest Access"
type="checkbox"
ref={guestAccessRef}
/>
</FieldRow>
{createRoomError && ( {createRoomError && (
<FieldRow> <FieldRow>
<ErrorMessage>{createRoomError.message}</ErrorMessage> <ErrorMessage>{createRoomError.message}</ErrorMessage>

View file

@ -1,6 +1,7 @@
import React, { forwardRef } from "react"; import React, { forwardRef } from "react";
import classNames from "classnames"; import classNames from "classnames";
import styles from "./Input.module.css"; import styles from "./Input.module.css";
import { ReactComponent as CheckIcon } from "./icons/Check.svg";
export function FieldRow({ children, rightAlign, className, ...rest }) { export function FieldRow({ children, rightAlign, className, ...rest }) {
return ( return (
@ -21,11 +22,23 @@ export function Field({ children, className, ...rest }) {
} }
export const InputField = forwardRef( export const InputField = forwardRef(
({ id, label, className, ...rest }, ref) => { ({ id, label, className, type, checked, ...rest }, ref) => {
return ( return (
<Field> <Field
<input id={id} {...rest} ref={ref} /> className={classNames(
<label htmlFor={id}>{label}</label> type === "checkbox" ? styles.checkboxField : styles.inputField,
className
)}
>
<input id={id} {...rest} ref={ref} type={type} checked={checked} />
<label htmlFor={id}>
{type === "checkbox" && (
<div className={styles.checkbox}>
<CheckIcon />
</div>
)}
{label}
</label>
</Field> </Field>
); );
} }

View file

@ -9,9 +9,6 @@
min-width: 0; min-width: 0;
position: relative; position: relative;
margin: 1em 0; margin: 1em 0;
border-radius: 4px;
transition: border-color .25s;
border: 1px solid #394049;
} }
.fieldRow.rightAlign { .fieldRow.rightAlign {
@ -30,7 +27,13 @@
margin-right: 0; margin-right: 0;
} }
.field input { .inputField {
border-radius: 4px;
transition: border-color .25s;
border: 1px solid #394049;
}
.inputField input {
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
border: none; border: none;
@ -42,17 +45,17 @@
min-width: 0; min-width: 0;
} }
.field input::placeholder { .inputField input::placeholder {
transition: color 0.25s ease-in 0s; transition: color 0.25s ease-in 0s;
color: transparent; color: transparent;
} }
.field input:placeholder-shown:focus::placeholder { .inputField input:placeholder-shown:focus::placeholder {
transition: color .25s ease-in .1s; transition: color .25s ease-in .1s;
color: #6f7882; color: #6f7882;
} }
.field label { .inputField label {
transition: font-size .25s ease-out .1s,color .25s ease-out .1s,top .25s ease-out .1s,background-color .25s ease-out .1s; transition: font-size .25s ease-out .1s,color .25s ease-out .1s,top .25s ease-out .1s,background-color .25s ease-out .1s;
color: white; color: white;
background-color: transparent; background-color: transparent;
@ -69,15 +72,15 @@
max-width: calc(100% - 20px); max-width: calc(100% - 20px);
} }
.field:focus-within { .inputField:focus-within {
border-color: #0086e6; border-color: #0086e6;
} }
.field input:focus { .inputField input:focus {
outline: 0; outline: 0;
} }
.field input:focus + label, .field input:not(:placeholder-shown) + label { .inputField input:focus + label, .inputField input:not(:placeholder-shown) + label {
background-color: #21262c; background-color: #21262c;
transition: font-size .25s ease-out 0s,color .25s ease-out 0s,top .25s ease-out 0s,background-color .25s ease-out 0s; transition: font-size .25s ease-out 0s,color .25s ease-out 0s,top .25s ease-out 0s,background-color .25s ease-out 0s;
font-size: 10px; font-size: 10px;
@ -86,10 +89,59 @@
pointer-events: auto; pointer-events: auto;
} }
.field input:focus + label { .inputField input:focus + label {
color: #0086e6; color: #0086e6;
} }
.checkboxField {
display: flex;
align-items: flex-start;
}
.checkboxField label {
display: flex;
align-items: center;
flex-grow: 1;
font-size: 13px;
}
.checkboxField input {
appearance: none;
margin: 0;
padding: 0;
}
.checkbox {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
flex-shrink: 0;
height: 16px;
width: 16px;
border: 1.5px solid rgba(185,190,198,.5);
box-sizing: border-box;
border-radius: 4px;
margin-right: 10px;
}
.checkbox svg {
display: none;
}
.checkbox svg * {
stroke: #FFF;
}
.checkboxField input[type="checkbox"]:checked + label > .checkbox {
background: #0dbd8b;
border-color: #0dbd8b;
}
.checkboxField input[type="checkbox"]:checked + label > .checkbox svg {
display: flex;
}
.button { .button {
vertical-align: middle; vertical-align: middle;
border: 0; border: 0;

View file

@ -60,8 +60,8 @@ export function LoginPage({ onLogin, error }) {
ref={loginUsernameRef} ref={loginUsernameRef}
placeholder="Username" placeholder="Username"
label="Username" label="Username"
autocorrect="off" autoCorrect="off"
autocapitalize="none" autoCapitalize="none"
/> />
</FieldRow> </FieldRow>
<FieldRow> <FieldRow>

View file

@ -60,8 +60,8 @@ export function RegisterPage({ onRegister, error }) {
ref={registerUsernameRef} ref={registerUsernameRef}
placeholder="Username" placeholder="Username"
label="Username" label="Username"
autocorrect="off" autoCorrect="off"
autocapitalize="none" autoCapitalize="none"
/> />
</FieldRow> </FieldRow>
<FieldRow> <FieldRow>

View file

@ -22,7 +22,7 @@ import React, {
useCallback, useCallback,
} from "react"; } from "react";
import styles from "./Room.module.css"; import styles from "./Room.module.css";
import { useParams, useLocation } from "react-router-dom"; import { useParams, useLocation, useHistory, Link } from "react-router-dom";
import { useVideoRoom } from "./ConferenceCallManagerHooks"; import { useVideoRoom } from "./ConferenceCallManagerHooks";
import { DevTools } from "./DevTools"; import { DevTools } from "./DevTools";
import { VideoGrid } from "./VideoGrid"; import { VideoGrid } from "./VideoGrid";
@ -34,7 +34,8 @@ import {
LayoutToggleButton, LayoutToggleButton,
} from "./RoomButton"; } from "./RoomButton";
import { Header, LeftNav, RightNav, CenterNav } from "./Header"; import { Header, LeftNav, RightNav, CenterNav } from "./Header";
import { Button } from "./Input"; import { Button, FieldRow, InputField, ErrorMessage } from "./Input";
import { Center, Content, Info, Modal } from "./Layout";
function useQuery() { function useQuery() {
const location = useLocation(); const location = useLocation();
@ -145,6 +146,74 @@ export function Room({ manager }) {
); );
} }
export function RoomAuth({ onLoginAsGuest, error }) {
const displayNameRef = useRef();
const history = useHistory();
const location = useLocation();
const onSubmitLoginForm = useCallback(
(e) => {
e.preventDefault();
onLoginAsGuest(displayNameRef.current.value);
},
[onLoginAsGuest, location, history]
);
return (
<>
<Header>
<LeftNav />
</Header>
<Content>
<Center>
<Modal>
<h2>Login As Guest</h2>
<form onSubmit={onSubmitLoginForm}>
<FieldRow>
<InputField
type="text"
ref={displayNameRef}
placeholder="Display Name"
label="Display Name"
autoCorrect="off"
autoCapitalize="none"
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
</FieldRow>
)}
<FieldRow rightAlign>
<Button type="submit">Login as guest</Button>
</FieldRow>
</form>
<Info>
<Link
to={{
pathname: "/login",
state: location.state,
}}
>
Sign in
</Link>
{" or "}
<Link
to={{
pathname: "/register",
state: location.state,
}}
>
Create account
</Link>
</Info>
</Modal>
</Center>
</Content>
</>
);
}
function JoinRoom({ function JoinRoom({
joining, joining,
joinCall, joinCall,

View file

@ -746,7 +746,7 @@ function ParticipantTile({ style, participant, remove, presenter, ...rest }) {
) : participant.audioMuted ? ( ) : participant.audioMuted ? (
<MuteMicIcon className={styles.muteMicIcon} /> <MuteMicIcon className={styles.muteMicIcon} />
) : null} ) : null}
<span>{participant.userId}</span> <span>{participant.displayName}</span>
</div> </div>
{participant.videoMuted && ( {participant.videoMuted && (
<DisableVideoIcon <DisableVideoIcon

3
src/icons/Check.svg Normal file
View file

@ -0,0 +1,3 @@
<svg fill="none" height="24" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m20 6-11 11-5-5"/>
</svg>

After

Width:  |  Height:  |  Size: 213 B