From 46f8eb84fc10ee6b56e85e21d7f9372bcb1bfc34 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Fri, 3 Sep 2021 15:45:07 -0700 Subject: [PATCH] Add guest access for rooms --- src/App.jsx | 26 ++++++--- src/ConferenceCallManager.js | 92 ++++++++++++++++++++++++------- src/ConferenceCallManagerHooks.js | 38 ++++++++++++- src/Home.jsx | 61 +++++++++++++++++--- src/Input.jsx | 21 +++++-- src/Input.module.css | 74 +++++++++++++++++++++---- src/LoginPage.jsx | 4 +- src/RegisterPage.jsx | 4 +- src/Room.jsx | 73 +++++++++++++++++++++++- src/VideoGrid.jsx | 2 +- src/icons/Check.svg | 3 + 11 files changed, 338 insertions(+), 60 deletions(-) create mode 100644 src/icons/Check.svg diff --git a/src/App.jsx b/src/App.jsx index d173575..383dd7c 100644 --- a/src/App.jsx +++ b/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 ( @@ -55,12 +62,13 @@ export default function App() { - - - + + {authenticated ? ( + + ) : ( + + )} + diff --git a/src/ConferenceCallManager.js b/src/ConferenceCallManager.js index 62e909e..ec36c60 100644 --- a/src/ConferenceCallManager.js +++ b/src/ConferenceCallManager.js @@ -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, diff --git a/src/ConferenceCallManagerHooks.js b/src/ConferenceCallManagerHooks.js index 09941b9..e654747 100644 --- a/src/ConferenceCallManagerHooks.js +++ b/src/ConferenceCallManagerHooks.js @@ -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) { diff --git a/src/Home.jsx b/src/Home.jsx index 3536e35..5ced0f5 100644 --- a/src/Home.jsx +++ b/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} /> + + + {createRoomError && ( {createRoomError.message} diff --git a/src/Input.jsx b/src/Input.jsx index c79cd4b..5b523d5 100644 --- a/src/Input.jsx +++ b/src/Input.jsx @@ -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 ( - - - + + + ); } diff --git a/src/Input.module.css b/src/Input.module.css index a40024d..eb10533 100644 --- a/src/Input.module.css +++ b/src/Input.module.css @@ -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; diff --git a/src/LoginPage.jsx b/src/LoginPage.jsx index 8d31f98..da54eab 100644 --- a/src/LoginPage.jsx +++ b/src/LoginPage.jsx @@ -60,8 +60,8 @@ export function LoginPage({ onLogin, error }) { ref={loginUsernameRef} placeholder="Username" label="Username" - autocorrect="off" - autocapitalize="none" + autoCorrect="off" + autoCapitalize="none" /> diff --git a/src/RegisterPage.jsx b/src/RegisterPage.jsx index 26c3a3b..f12ed68 100644 --- a/src/RegisterPage.jsx +++ b/src/RegisterPage.jsx @@ -60,8 +60,8 @@ export function RegisterPage({ onRegister, error }) { ref={registerUsernameRef} placeholder="Username" label="Username" - autocorrect="off" - autocapitalize="none" + autoCorrect="off" + autoCapitalize="none" /> diff --git a/src/Room.jsx b/src/Room.jsx index 432337f..e6b295a 100644 --- a/src/Room.jsx +++ b/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 ( + <> +
+ +
+ +
+ +

Login As Guest

+
+ + + + {error && ( + + {error.message} + + )} + + + +
+ + + Sign in + + {" or "} + + Create account + + +
+
+
+ + ); +} + function JoinRoom({ joining, joinCall, diff --git a/src/VideoGrid.jsx b/src/VideoGrid.jsx index 3eb300b..b552ea7 100644 --- a/src/VideoGrid.jsx +++ b/src/VideoGrid.jsx @@ -746,7 +746,7 @@ function ParticipantTile({ style, participant, remove, presenter, ...rest }) { ) : participant.audioMuted ? ( ) : null} - {participant.userId} + {participant.displayName} {participant.videoMuted && ( + +