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";
import { useConferenceCallManager } from "./ConferenceCallManagerHooks";
import { Home } from "./Home";
import { Room } from "./Room";
import { Room, RoomAuth } from "./Room";
import { GridDemo } from "./GridDemo";
import { RegisterPage } from "./RegisterPage";
import { LoginPage } from "./LoginPage";
@ -34,8 +34,15 @@ 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, error, manager, login, register } =
useConferenceCallManager(homeserverUrl);
const {
loading,
authenticated,
error,
manager,
login,
loginAsGuest,
register,
} = useConferenceCallManager(homeserverUrl);
return (
<Router>
@ -55,12 +62,13 @@ export default function App() {
<Route exact path="/register">
<RegisterPage onRegister={register} error={error} />
</Route>
<AuthenticatedRoute
authenticated={authenticated}
path="/room/:roomId"
>
<Route path="/room/:roomId">
{authenticated ? (
<Room manager={manager} error={error} />
</AuthenticatedRoute>
) : (
<RoomAuth error={error} onLoginAsGuest={loginAsGuest} />
)}
</Route>
<Route exact path="/grid">
<GridDemo />
</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) {
try {
const registrationClient = matrixcs.createClient(homeserverUrl);
@ -250,6 +280,7 @@ export class ConferenceCallManager extends EventEmitter {
this.localParticipant = {
local: true,
userId,
displayName: this.client.getUser(this.client.getUserId()).rawDisplayName,
sessionId: this.sessionId,
call: null,
stream,
@ -478,12 +509,15 @@ export class ConferenceCallManager extends EventEmitter {
"m.room.member",
participant.userId
);
const participantInfo = memberStateEvent.getContent()[CONF_PARTICIPANT];
const memberStateContent = memberStateEvent.getContent();
if (
!participantInfo ||
typeof participantInfo !== "object" ||
(participantInfo.expiresAt && participantInfo.expiresAt < now)
!memberStateContent ||
!memberStateContent[CONF_PARTICIPANT] ||
typeof memberStateContent[CONF_PARTICIPANT] !== "object" ||
(memberStateContent[CONF_PARTICIPANT].expiresAt &&
memberStateContent[CONF_PARTICIPANT].expiresAt < now)
) {
this.emit("debugstate", participant.userId, null, "inactive");
@ -533,27 +567,21 @@ export class ConferenceCallManager extends EventEmitter {
const audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : 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 memberStateEvent = this.room.currentState.getStateEvents(
"m.room.member",
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.
const existingParticipant = this.participants.find(
@ -562,6 +590,8 @@ export class ConferenceCallManager extends EventEmitter {
let participant;
console.log(call.opponentMember);
if (existingParticipant) {
participant = existingParticipant;
// This also fires the hangup event and triggers those side-effects
@ -577,6 +607,7 @@ export class ConferenceCallManager extends EventEmitter {
participant = {
local: false,
userId,
displayName: call.opponentMember.rawDisplayName,
sessionId,
call,
stream,
@ -588,6 +619,20 @@ export class ConferenceCallManager extends EventEmitter {
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) =>
this._onCallStateChanged(participant, call, state)
);
@ -629,7 +674,13 @@ export class ConferenceCallManager extends EventEmitter {
"m.room.member",
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") {
return;
@ -680,6 +731,7 @@ export class ConferenceCallManager extends EventEmitter {
participant = {
local: false,
userId: member.userId,
displayName: member.rawDisplayName,
sessionId,
call,
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) => {
setState((prevState) => ({
...prevState,
@ -135,7 +163,15 @@ export function useConferenceCallManager(homeserverUrl) {
};
}, [manager]);
return { loading, authenticated, manager, error, login, register };
return {
loading,
authenticated,
manager,
error,
login,
loginAsGuest,
register,
};
}
export function useVideoRoom(manager, roomId, timeout = 5000) {

View file

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

View file

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

View file

@ -9,9 +9,6 @@
min-width: 0;
position: relative;
margin: 1em 0;
border-radius: 4px;
transition: border-color .25s;
border: 1px solid #394049;
}
.fieldRow.rightAlign {
@ -30,7 +27,13 @@
margin-right: 0;
}
.field input {
.inputField {
border-radius: 4px;
transition: border-color .25s;
border: 1px solid #394049;
}
.inputField input {
font-weight: 400;
font-size: 14px;
border: none;
@ -42,17 +45,17 @@
min-width: 0;
}
.field input::placeholder {
.inputField input::placeholder {
transition: color 0.25s ease-in 0s;
color: transparent;
}
.field input:placeholder-shown:focus::placeholder {
.inputField input:placeholder-shown:focus::placeholder {
transition: color .25s ease-in .1s;
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;
color: white;
background-color: transparent;
@ -69,15 +72,15 @@
max-width: calc(100% - 20px);
}
.field:focus-within {
.inputField:focus-within {
border-color: #0086e6;
}
.field input:focus {
.inputField input:focus {
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;
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;
@ -86,10 +89,59 @@
pointer-events: auto;
}
.field input:focus + label {
.inputField input:focus + label {
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 {
vertical-align: middle;
border: 0;

View file

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

View file

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

View file

@ -22,7 +22,7 @@ import React, {
useCallback,
} from "react";
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 { DevTools } from "./DevTools";
import { VideoGrid } from "./VideoGrid";
@ -34,7 +34,8 @@ import {
LayoutToggleButton,
} from "./RoomButton";
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() {
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({
joining,
joinCall,

View file

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