Add guest access for rooms
This commit is contained in:
parent
196c8eeeeb
commit
46f8eb84fc
11 changed files with 338 additions and 60 deletions
26
src/App.jsx
26
src/App.jsx
|
@ -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"
|
||||
>
|
||||
<Room manager={manager} error={error} />
|
||||
</AuthenticatedRoute>
|
||||
<Route path="/room/:roomId">
|
||||
{authenticated ? (
|
||||
<Room manager={manager} error={error} />
|
||||
) : (
|
||||
<RoomAuth error={error} onLoginAsGuest={loginAsGuest} />
|
||||
)}
|
||||
</Route>
|
||||
<Route exact path="/grid">
|
||||
<GridDemo />
|
||||
</Route>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
61
src/Home.jsx
61
src/Home.jsx
|
@ -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 }) => {
|
||||
history.push(`/room/${room_id}`);
|
||||
})
|
||||
.catch(setCreateRoomError);
|
||||
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}`);
|
||||
}
|
||||
|
||||
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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
73
src/Room.jsx
73
src/Room.jsx
|
@ -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,
|
||||
|
|
|
@ -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
3
src/icons/Check.svg
Normal 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 |
Loading…
Reference in a new issue