From 5784a005dc9f3135c041c4fffa816d3609ff9bc2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Jul 2022 14:34:15 +0100 Subject: [PATCH 01/17] Auto-register if displayName URL param is given Fixes https://github.com/vector-im/element-call/issues/442 --- src/auth/generateRandomName.ts | 4 +--- src/room/RoomAuthView.jsx | 35 +++++++--------------------------- src/room/RoomPage.jsx | 27 +++++++++++++++++++++++--- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/auth/generateRandomName.ts b/src/auth/generateRandomName.ts index b58c1114..2232d356 100644 --- a/src/auth/generateRandomName.ts +++ b/src/auth/generateRandomName.ts @@ -19,7 +19,6 @@ import { adjectives, colors, animals, - Config, } from "unique-names-generator"; const elements = [ @@ -143,12 +142,11 @@ const elements = [ "oganesson", ]; -export function generateRandomName(config: Config): string { +export function generateRandomName(): string { return uniqueNamesGenerator({ dictionaries: [colors, adjectives, animals, elements], style: "lowerCase", length: 3, separator: "-", - ...config, }); } diff --git a/src/room/RoomAuthView.jsx b/src/room/RoomAuthView.jsx index 4bf2303a..05613146 100644 --- a/src/room/RoomAuthView.jsx +++ b/src/room/RoomAuthView.jsx @@ -16,26 +16,21 @@ limitations under the License. import React, { useCallback, useState } from "react"; import styles from "./RoomAuthView.module.css"; -import { useClient } from "../ClientContext"; import { Button } from "../button"; import { Body, Caption, Link, Headline } from "../typography/Typography"; import { Header, HeaderLogo, LeftNav, RightNav } from "../Header"; import { useLocation } from "react-router-dom"; -import { useRecaptcha } from "../auth/useRecaptcha"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; -import { randomString } from "matrix-js-sdk/src/randomstring"; -import { useInteractiveRegistration } from "../auth/useInteractiveRegistration"; import { Form } from "../form/Form"; import { UserMenuContainer } from "../UserMenuContainer"; -import { generateRandomName } from "../auth/generateRandomName"; +import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; export function RoomAuthView() { - const { setClient } = useClient(); const [loading, setLoading] = useState(false); const [error, setError] = useState(); - const [privacyPolicyUrl, recaptchaKey, register] = - useInteractiveRegistration(); - const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); + + const { registerPasswordlessUser, recaptchaId, privacyPolicyUrl } = + useRegisterPasswordlessUser(); const onSubmit = useCallback( (e) => { @@ -43,29 +38,13 @@ export function RoomAuthView() { const data = new FormData(e.target); const displayName = data.get("displayName"); - async function submit() { - setError(undefined); - setLoading(true); - const recaptchaResponse = await execute(); - const userName = generateRandomName(); - const [client, session] = await register( - userName, - randomString(16), - displayName, - recaptchaResponse, - true - ); - setClient(client, session); - } - - submit().catch((error) => { - console.error(error); + registerPasswordlessUser(displayName).catch((error) => { + console.error("Failed to register passwordless user", e); setLoading(false); setError(error); - reset(); }); }, - [register, reset, execute] + [registerPasswordlessUser] ); const location = useLocation(); diff --git a/src/room/RoomPage.jsx b/src/room/RoomPage.jsx index 1fe7eea6..72f6bf44 100644 --- a/src/room/RoomPage.jsx +++ b/src/room/RoomPage.jsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useMemo } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useLocation, useParams } from "react-router-dom"; import { useClient } from "../ClientContext"; import { ErrorView, LoadingView } from "../FullScreenView"; @@ -22,6 +22,7 @@ import { RoomAuthView } from "./RoomAuthView"; import { GroupCallLoader } from "./GroupCallLoader"; import { GroupCallView } from "./GroupCallView"; import { MediaHandlerProvider } from "../settings/useMediaHandler"; +import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; export function RoomPage() { const { loading, isAuthenticated, error, client, isPasswordlessUser } = @@ -29,17 +30,37 @@ export function RoomPage() { const { roomId: maybeRoomId } = useParams(); const { hash, search } = useLocation(); - const [viaServers, isEmbedded, isPtt] = useMemo(() => { + const [viaServers, isEmbedded, isPtt, displayName] = useMemo(() => { const params = new URLSearchParams(search); return [ params.getAll("via"), params.has("embed"), params.get("ptt") === "true", + params.get("displayName"), ]; }, [search]); const roomId = (maybeRoomId || hash || "").toLowerCase(); + const { registerPasswordlessUser, recaptchaId } = + useRegisterPasswordlessUser(); + const [isRegistering, setIsRegistering] = useState(false); - if (loading) { + useEffect(() => { + // If we're not already authed and we've been given a display name as + // a URL param, automatically register a passwordless user + if (!isAuthenticated && displayName) { + setIsRegistering(true); + registerPasswordlessUser(displayName).finally(() => { + setIsRegistering(false); + }); + } + }, [ + isAuthenticated, + displayName, + setIsRegistering, + registerPasswordlessUser, + ]); + + if (loading || isRegistering) { return ; } From c1e45c4a303db294470603add7e95ef7f7cb0d97 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Jul 2022 16:02:17 +0100 Subject: [PATCH 02/17] Missed a file --- src/auth/useRegisterPasswordlessUser.ts | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/auth/useRegisterPasswordlessUser.ts diff --git a/src/auth/useRegisterPasswordlessUser.ts b/src/auth/useRegisterPasswordlessUser.ts new file mode 100644 index 00000000..2fd74579 --- /dev/null +++ b/src/auth/useRegisterPasswordlessUser.ts @@ -0,0 +1,59 @@ +/* +Copyright 2022 Matrix.org Foundation C.I.C. + +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 { useCallback } from "react"; +import { randomString } from "matrix-js-sdk/src/randomstring"; + +import { useClient } from "../ClientContext"; +import { useInteractiveRegistration } from "../auth/useInteractiveRegistration"; +import { generateRandomName } from "../auth/generateRandomName"; +import { useRecaptcha } from "../auth/useRecaptcha"; + +export interface UseRegisterPasswordlessUserType { + privacyPolicyUrl: string; + registerPasswordlessUser: (string) => Promise; + recaptchaId: string; +} + +export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType { + const { setClient } = useClient(); + const [privacyPolicyUrl, recaptchaKey, register] = + useInteractiveRegistration(); + const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); + + const registerPasswordlessUser = useCallback( + async (displayName: string) => { + try { + const recaptchaResponse = await execute(); + const userName = generateRandomName(); + const [client, session] = await register( + userName, + randomString(16), + displayName, + recaptchaResponse, + true + ); + setClient(client, session); + } catch (e) { + reset(); + throw e; + } + }, + [execute, reset, register, setClient] + ); + + return { privacyPolicyUrl, registerPasswordlessUser, recaptchaId }; +} From 4c145af7a3bc0a67f4201a57d16a1a787796539d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2022 13:07:30 +0100 Subject: [PATCH 03/17] Don't restore session unless crypto data is found Add a check to ensure that we find crypto data in the crypto store when we're restoring a session and otherwise abort the session restore. This will prevent us from restoring a session and generating new keys when there was a previous session with different keys. ***This will force a logout for all users*** See the linked issue (and the comment in code) for more detail. Fixes https://github.com/vector-im/element-call/issues/464 --- src/ClientContext.tsx | 15 +++++--- src/auth/useInteractiveLogin.ts | 15 +++++--- src/auth/useInteractiveRegistration.ts | 15 +++++--- src/matrix-utils.ts | 51 ++++++++++++++++++++++++-- 4 files changed, 75 insertions(+), 21 deletions(-) diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 68676ad5..503c3f8f 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -101,12 +101,15 @@ export const ClientProvider: FC = ({ children }) => { const { user_id, device_id, access_token, passwordlessUser } = session; - const client = await initClient({ - baseUrl: defaultHomeserver, - accessToken: access_token, - userId: user_id, - deviceId: device_id, - }); + const client = await initClient( + { + baseUrl: defaultHomeserver, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }, + true + ); /* eslint-enable camelcase */ return { client, isPasswordlessUser: passwordlessUser }; diff --git a/src/auth/useInteractiveLogin.ts b/src/auth/useInteractiveLogin.ts index 6379c67c..9ffa7ba6 100644 --- a/src/auth/useInteractiveLogin.ts +++ b/src/auth/useInteractiveLogin.ts @@ -57,12 +57,15 @@ export const useInteractiveLogin = () => passwordlessUser: false, }; - const client = await initClient({ - baseUrl: defaultHomeserver, - accessToken: access_token, - userId: user_id, - deviceId: device_id, - }); + const client = await initClient( + { + baseUrl: defaultHomeserver, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }, + false + ); /* eslint-enable camelcase */ return [client, session]; diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts index af611f9d..8b8647a0 100644 --- a/src/auth/useInteractiveRegistration.ts +++ b/src/auth/useInteractiveRegistration.ts @@ -90,12 +90,15 @@ export const useInteractiveRegistration = (): [ const { user_id, access_token, device_id } = (await interactiveAuth.attemptAuth()) as any; - const client = await initClient({ - baseUrl: defaultHomeserver, - accessToken: access_token, - userId: user_id, - deviceId: device_id, - }); + const client = await initClient( + { + baseUrl: defaultHomeserver, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }, + false + ); await client.setDisplayName(displayName); diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 1a1a2ea6..ea2920b0 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -24,6 +24,19 @@ export const defaultHomeserver = export const defaultHomeserverHost = new URL(defaultHomeserver).host; +export class CryptoStoreIntegrityError extends Error { + constructor() { + super("Crypto store data was expected, but none was found"); + } +} + +const SYNC_STORE_NAME = "element-call-sync"; +// Note that the crypto store name has changed from previous versions +// deliberately in order to force a logout for all users due to +// https://github.com/vector-im/element-call/issues/464 +// (It's a good opportunity to make the database names consistent.) +const CRYPTO_STORE_NAME = "element-call-crypto"; + function waitForSync(client: MatrixClient) { return new Promise((resolve, reject) => { const onSync = ( @@ -43,8 +56,18 @@ function waitForSync(client: MatrixClient) { }); } +/** + * Initialises and returns a new Matrix Client + * If true is passed for the 'restore' parameter, a check will be made + * to ensure that corresponding crypto data is stored and recovered. + * If the check fails, CryptoStoreIntegrityError will be thrown. + * @param clientOptions Object of options passed through to the client + * @param restore Whether the session is being restored from storage + * @returns The MatrixClient instance + */ export async function initClient( - clientOptions: ICreateClientOpts + clientOptions: ICreateClientOpts, + restore: boolean ): Promise { // TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10 window.OLM_OPTIONS = {}; @@ -62,17 +85,39 @@ export async function initClient( storeOpts.store = new IndexedDBStore({ indexedDB: window.indexedDB, localStorage, - dbName: "element-call-sync", + dbName: SYNC_STORE_NAME, workerFactory: () => new IndexedDBWorker(), }); } else if (localStorage) { storeOpts.store = new MemoryStore({ localStorage }); } + // Check whether we have crypto data store. If we are restoring a session + // from storage then we will have started the crypto store and therefore + // have generated keys for that device, so if we can't recover those keys, + // we must not continue or we'll generate new keys and anyone who saw our + // previous keys will not accept our new key. + if (restore) { + if (indexedDB) { + const cryptoStoreExists = await IndexedDBCryptoStore.exists( + indexedDB, + CRYPTO_STORE_NAME + ); + if (!cryptoStoreExists) throw new CryptoStoreIntegrityError(); + } else if (localStorage) { + if (!LocalStorageCryptoStore.exists(localStorage)) + throw new CryptoStoreIntegrityError(); + } else { + // if we get here then we're using the memory store, which cannot + // possibly have remembered a session, so it's an error. + throw new CryptoStoreIntegrityError(); + } + } + if (indexedDB) { storeOpts.cryptoStore = new IndexedDBCryptoStore( indexedDB, - "matrix-js-sdk:crypto" + CRYPTO_STORE_NAME ); } else if (localStorage) { storeOpts.cryptoStore = new LocalStorageCryptoStore(localStorage); From 1eab957d85080332350da730aeceb3f526373c4b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2022 13:11:47 +0100 Subject: [PATCH 04/17] Fix typescript syntax --- src/auth/useRegisterPasswordlessUser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/useRegisterPasswordlessUser.ts b/src/auth/useRegisterPasswordlessUser.ts index 2fd74579..73189471 100644 --- a/src/auth/useRegisterPasswordlessUser.ts +++ b/src/auth/useRegisterPasswordlessUser.ts @@ -24,7 +24,7 @@ import { useRecaptcha } from "../auth/useRecaptcha"; export interface UseRegisterPasswordlessUserType { privacyPolicyUrl: string; - registerPasswordlessUser: (string) => Promise; + registerPasswordlessUser: (displayName: string) => Promise; recaptchaId: string; } From 873e68e1e17db6f3bcb19fbb26976bf45eff9bb8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2022 13:24:22 +0100 Subject: [PATCH 05/17] Add notes from thinking through the need for storing what crypto db we use --- src/matrix-utils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index ea2920b0..3e576e78 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -97,6 +97,12 @@ export async function initClient( // have generated keys for that device, so if we can't recover those keys, // we must not continue or we'll generate new keys and anyone who saw our // previous keys will not accept our new key. + // It's worth mentioning here that if support for indexeddb or localstorage + // appears or disappears between sessions (it happens) then the failure mode + // here will be that we'll try a different store, not find crypto data and + // fail to restore the session. An alternative would be to continue using + // whatever we were using before, but that could be confusing since you could + // enable indexeddb and but the app would still not be using it. if (restore) { if (indexedDB) { const cryptoStoreExists = await IndexedDBCryptoStore.exists( From d01f7be58a269d3b2a0d733059eba8c63b09c078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 15 Jul 2022 11:28:16 +0200 Subject: [PATCH 06/17] Add `.env.example` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .env.example | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..2f62d615 --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +#### +# App Config +# Environment files are documented here: +# https://vitejs.dev/guide/env-and-mode.html#env-files +#### + +# Used for determining the homeserver to use for short urls etc. +# VITE_DEFAULT_HOMESERVER=http://localhost:8008 + +# Used for submitting debug logs to an external rageshake server +# VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit + +# The Sentry DSN to use for error reporting. Leave undefined to disable. +# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 + +# VITE_CUSTOM_THEME=true +# VITE_THEME_ACCENT=#0dbd8b +# VITE_THEME_ACCENT_20=#0dbd8b33 +# VITE_THEME_ALERT=#ff5b55 +# VITE_THEME_ALERT_20=#ff5b5533 +# VITE_THEME_LINKS=#0086e6 +# VITE_THEME_PRIMARY_CONTENT=#ffffff +# VITE_THEME_SECONDARY_CONTENT=#a9b2bc +# VITE_THEME_TERTIARY_CONTENT=#8e99a4 +# VITE_THEME_TERTIARY_CONTENT_20=#8e99a433 +# VITE_THEME_QUATERNARY_CONTENT=#6f7882 +# VITE_THEME_QUINARY_CONTENT=#394049 +# VITE_THEME_SYSTEM=#21262c +# VITE_THEME_BACKGROUND=#15191e From d097223d41aad16e55cb989ebc0c1d2e9227285c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 15 Jul 2022 11:29:25 +0200 Subject: [PATCH 07/17] Add `.env` to `.gitignore` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6a87b455..9bafa524 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules .DS_Store +.env dist dist-ssr *.local From f876df6acce6f1243f2a037de4346c03c30d6318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 15 Jul 2022 11:30:52 +0200 Subject: [PATCH 08/17] Remove `.env` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .env | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 2f62d615..00000000 --- a/.env +++ /dev/null @@ -1,29 +0,0 @@ -#### -# App Config -# Environment files are documented here: -# https://vitejs.dev/guide/env-and-mode.html#env-files -#### - -# Used for determining the homeserver to use for short urls etc. -# VITE_DEFAULT_HOMESERVER=http://localhost:8008 - -# Used for submitting debug logs to an external rageshake server -# VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit - -# The Sentry DSN to use for error reporting. Leave undefined to disable. -# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 - -# VITE_CUSTOM_THEME=true -# VITE_THEME_ACCENT=#0dbd8b -# VITE_THEME_ACCENT_20=#0dbd8b33 -# VITE_THEME_ALERT=#ff5b55 -# VITE_THEME_ALERT_20=#ff5b5533 -# VITE_THEME_LINKS=#0086e6 -# VITE_THEME_PRIMARY_CONTENT=#ffffff -# VITE_THEME_SECONDARY_CONTENT=#a9b2bc -# VITE_THEME_TERTIARY_CONTENT=#8e99a4 -# VITE_THEME_TERTIARY_CONTENT_20=#8e99a433 -# VITE_THEME_QUATERNARY_CONTENT=#6f7882 -# VITE_THEME_QUINARY_CONTENT=#394049 -# VITE_THEME_SYSTEM=#21262c -# VITE_THEME_BACKGROUND=#15191e From bb505273f4f7fd0ae510619ae578690d2b2ce99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 15 Jul 2022 11:32:07 +0200 Subject: [PATCH 09/17] Add `.env` instruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 75c9a5e6..076cb2c5 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ git clone https://github.com/vector-im/element-call.git cd element-call yarn yarn link matrix-js-sdk +cp .env.example .env yarn dev ``` From b4a56f6dd74438367f989d79610674380575473d Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 11:25:09 -0400 Subject: [PATCH 10/17] Wait for the created room to come down sync before placing a group call --- src/matrix-utils.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 3e576e78..81da90c2 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -8,6 +8,7 @@ import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto- import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; import { ICreateClientOpts } from "matrix-js-sdk/src/matrix"; import { ClientEvent } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials"; import { GroupCallIntent, @@ -226,6 +227,23 @@ export async function createRoom( name: string, isPtt = false ): Promise { + let setExpectedRoomId: (roomId: string) => void; + const expectedRoomId = new Promise( + (resolve) => (setExpectedRoomId = resolve) + ); + + // There's no telling whether the new room will come down sync in the middle + // of the createRoom request, or after it, so start watching for it beforehand + const roomReceived = new Promise((resolve) => { + const onRoom = async (room: Room) => { + if (room.roomId === (await expectedRoomId)) { + resolve(); + client.off(ClientEvent.Room, onRoom); + } + }; + client.on(ClientEvent.Room, onRoom); + }); + const createRoomResult = await client.createRoom({ visibility: Visibility.Private, preset: Preset.PublicChat, @@ -256,6 +274,10 @@ export async function createRoom( }, }); + // Wait for the room to come down sync before doing anything with it + setExpectedRoomId(createRoomResult.room_id); + await roomReceived; + console.log(`Creating ${isPtt ? "PTT" : "video"} group call room`); await client.createGroupCall( From fae4c504c9ee0d060b1cc56f979befde755c921e Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 12:58:53 -0400 Subject: [PATCH 11/17] Consolidate all group call creation into useLoadGroupCall This enables us to automatically create a group call in rooms that exist, but contain no calls. --- src/matrix-utils.ts | 16 +---- src/room/GroupCallLoader.jsx | 1 - src/room/useLoadGroupCall.js | 114 -------------------------------- src/room/useLoadGroupCall.ts | 123 +++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 130 deletions(-) delete mode 100644 src/room/useLoadGroupCall.js create mode 100644 src/room/useLoadGroupCall.ts diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 81da90c2..e7bf120d 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -10,10 +10,6 @@ import { ICreateClientOpts } from "matrix-js-sdk/src/matrix"; import { ClientEvent } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials"; -import { - GroupCallIntent, - GroupCallType, -} from "matrix-js-sdk/src/webrtc/groupCall"; import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; import { logger } from "matrix-js-sdk/src/logger"; @@ -224,8 +220,7 @@ export function isLocalRoomId(roomId: string): boolean { export async function createRoom( client: MatrixClient, - name: string, - isPtt = false + name: string ): Promise { let setExpectedRoomId: (roomId: string) => void; const expectedRoomId = new Promise( @@ -278,15 +273,6 @@ export async function createRoom( setExpectedRoomId(createRoomResult.room_id); await roomReceived; - console.log(`Creating ${isPtt ? "PTT" : "video"} group call room`); - - await client.createGroupCall( - createRoomResult.room_id, - isPtt ? GroupCallType.Voice : GroupCallType.Video, - isPtt, - GroupCallIntent.Prompt - ); - return fullAliasFromRoomName(name, client); } diff --git a/src/room/GroupCallLoader.jsx b/src/room/GroupCallLoader.jsx index f0741284..70791a20 100644 --- a/src/room/GroupCallLoader.jsx +++ b/src/room/GroupCallLoader.jsx @@ -30,7 +30,6 @@ export function GroupCallLoader({ client, roomId, viaServers, - true, createPtt ); diff --git a/src/room/useLoadGroupCall.js b/src/room/useLoadGroupCall.js deleted file mode 100644 index b4ec628f..00000000 --- a/src/room/useLoadGroupCall.js +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2022 Matrix.org Foundation C.I.C. - -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 { useState, useEffect } from "react"; -import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils"; - -async function fetchGroupCall( - client, - roomIdOrAlias, - viaServers = undefined, - timeout = 5000 -) { - const { roomId } = await client.joinRoom(roomIdOrAlias, { viaServers }); - - return new Promise((resolve, reject) => { - let timeoutId; - - function onGroupCallIncoming(groupCall) { - if (groupCall && groupCall.room.roomId === roomId) { - clearTimeout(timeoutId); - client.removeListener("GroupCall.incoming", onGroupCallIncoming); - resolve(groupCall); - } - } - - const groupCall = client.getGroupCallForRoom(roomId); - - if (groupCall) { - resolve(groupCall); - } - - client.on("GroupCall.incoming", onGroupCallIncoming); - - if (timeout) { - timeoutId = setTimeout(() => { - client.removeListener("GroupCall.incoming", onGroupCallIncoming); - reject(new Error("Fetching group call timed out.")); - }, timeout); - } - }); -} - -export function useLoadGroupCall( - client, - roomId, - viaServers, - createIfNotFound, - createPtt -) { - const [state, setState] = useState({ - loading: true, - error: undefined, - groupCall: undefined, - }); - - useEffect(() => { - async function fetchOrCreateGroupCall() { - try { - const groupCall = await fetchGroupCall( - client, - roomId, - viaServers, - 30000 - ); - return groupCall; - } catch (error) { - if ( - createIfNotFound && - (error.errcode === "M_NOT_FOUND" || - (error.message && - error.message.indexOf("Failed to fetch alias") !== -1)) && - isLocalRoomId(roomId) - ) { - const roomName = roomNameFromRoomId(roomId); - await createRoom(client, roomName, createPtt); - const groupCall = await fetchGroupCall( - client, - roomId, - viaServers, - 30000 - ); - return groupCall; - } - - throw error; - } - } - - setState({ loading: true }); - - fetchOrCreateGroupCall() - .then((groupCall) => - setState((prevState) => ({ ...prevState, loading: false, groupCall })) - ) - .catch((error) => - setState((prevState) => ({ ...prevState, loading: false, error })) - ); - }, [client, roomId, state.reloadId, createIfNotFound, viaServers, createPtt]); - - return state; -} diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts new file mode 100644 index 00000000..27744d7b --- /dev/null +++ b/src/room/useLoadGroupCall.ts @@ -0,0 +1,123 @@ +/* +Copyright 2022 Matrix.org Foundation C.I.C. + +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 { useState, useEffect } from "react"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { + GroupCallType, + GroupCallIntent, +} from "matrix-js-sdk/src/webrtc/groupCall"; +import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler"; + +import type { MatrixClient } from "matrix-js-sdk/src/client"; +import type { Room } from "matrix-js-sdk/src/models/room"; +import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; +import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils"; + +export interface GroupCallLoadState { + loading: boolean; + error?: Error; + groupCall?: GroupCall; +} + +export const useLoadGroupCall = ( + client: MatrixClient, + roomIdOrAlias: string, + viaServers: string[], + createPtt: boolean +) => { + const [state, setState] = useState({ loading: true }); + + useEffect(() => { + setState({ loading: true }); + + const fetchOrCreateRoom = async (): Promise => { + try { + return await client.joinRoom(roomIdOrAlias, { viaServers }); + } catch (error) { + if ( + isLocalRoomId(roomIdOrAlias) && + (error.errcode === "M_NOT_FOUND" || + (error.message && + error.message.indexOf("Failed to fetch alias") !== -1)) + ) { + // The room doesn't exist, but we can create it + await createRoom(client, roomNameFromRoomId(roomIdOrAlias)); + return await client.joinRoom(roomIdOrAlias, { viaServers }); + } else { + throw error; + } + } + }; + + const fetchOrCreateGroupCall = async (): Promise => { + const room = await fetchOrCreateRoom(); + const groupCall = client.getGroupCallForRoom(room.roomId); + + if (groupCall) { + return groupCall; + } else if ( + room.currentState.mayClientSendStateEvent( + EventType.GroupCallPrefix, + client + ) + ) { + // The call doesn't exist, but we can create it + console.log(`Creating ${createPtt ? "PTT" : "video"} group call room`); + return await client.createGroupCall( + room.roomId, + createPtt ? GroupCallType.Voice : GroupCallType.Video, + createPtt, + GroupCallIntent.Room + ); + } else { + // We don't have permission to create the call, so all we can do is wait + // for one to come in + return new Promise((resolve, reject) => { + const onGroupCallIncoming = (groupCall: GroupCall) => { + if (groupCall?.room.roomId === room.roomId) { + clearTimeout(timeout); + client.off( + GroupCallEventHandlerEvent.Incoming, + onGroupCallIncoming + ); + resolve(groupCall); + } + }; + client.on(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming); + + const timeout = setTimeout(() => { + client.off( + GroupCallEventHandlerEvent.Incoming, + onGroupCallIncoming + ); + reject(new Error("Fetching group call timed out.")); + }, 30000); + }); + } + }; + + fetchOrCreateGroupCall() + .then((groupCall) => + setState((prevState) => ({ ...prevState, loading: false, groupCall })) + ) + .catch((error) => + setState((prevState) => ({ ...prevState, loading: false, error })) + ); + }, [client, roomIdOrAlias, viaServers, createPtt]); + + return state; +}; From 982398b32fcc240f66e7f98d1a424a4249b50f54 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 13:05:06 -0400 Subject: [PATCH 12/17] Remove unnecessary complexity from createRoom With fae4c504c9ee0d060b1cc56f979befde755c921e, the changes from b4a56f6dd74438367f989d79610674380575473d are no longer necessary. --- src/matrix-utils.ts | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index e7bf120d..9b6a005b 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -8,7 +8,6 @@ import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto- import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; import { ICreateClientOpts } from "matrix-js-sdk/src/matrix"; import { ClientEvent } from "matrix-js-sdk/src/client"; -import { Room } from "matrix-js-sdk/src/models/room"; import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials"; import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; import { logger } from "matrix-js-sdk/src/logger"; @@ -222,24 +221,7 @@ export async function createRoom( client: MatrixClient, name: string ): Promise { - let setExpectedRoomId: (roomId: string) => void; - const expectedRoomId = new Promise( - (resolve) => (setExpectedRoomId = resolve) - ); - - // There's no telling whether the new room will come down sync in the middle - // of the createRoom request, or after it, so start watching for it beforehand - const roomReceived = new Promise((resolve) => { - const onRoom = async (room: Room) => { - if (room.roomId === (await expectedRoomId)) { - resolve(); - client.off(ClientEvent.Room, onRoom); - } - }; - client.on(ClientEvent.Room, onRoom); - }); - - const createRoomResult = await client.createRoom({ + await client.createRoom({ visibility: Visibility.Private, preset: Preset.PublicChat, name, @@ -269,10 +251,6 @@ export async function createRoom( }, }); - // Wait for the room to come down sync before doing anything with it - setExpectedRoomId(createRoomResult.room_id); - await roomReceived; - return fullAliasFromRoomName(name, client); } From daeecc9b689f367ef393c9b5e8c805ae5937db17 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 13:07:19 -0400 Subject: [PATCH 13/17] Add a missing type --- src/room/useLoadGroupCall.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 27744d7b..bb108036 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -38,7 +38,7 @@ export const useLoadGroupCall = ( roomIdOrAlias: string, viaServers: string[], createPtt: boolean -) => { +): GroupCallLoadState => { const [state, setState] = useState({ loading: true }); useEffect(() => { From 996c5f86c16f9cb1184b6766cbf35d0658be9962 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 16:08:26 -0400 Subject: [PATCH 14/17] Refactor to use fewer else's --- src/room/useLoadGroupCall.ts | 43 +++++++++++++++++------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index bb108036..0d7fb7c7 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -67,9 +67,9 @@ export const useLoadGroupCall = ( const room = await fetchOrCreateRoom(); const groupCall = client.getGroupCallForRoom(room.roomId); - if (groupCall) { - return groupCall; - } else if ( + if (groupCall) return groupCall; + + if ( room.currentState.mayClientSendStateEvent( EventType.GroupCallPrefix, client @@ -83,31 +83,28 @@ export const useLoadGroupCall = ( createPtt, GroupCallIntent.Room ); - } else { - // We don't have permission to create the call, so all we can do is wait - // for one to come in - return new Promise((resolve, reject) => { - const onGroupCallIncoming = (groupCall: GroupCall) => { - if (groupCall?.room.roomId === room.roomId) { - clearTimeout(timeout); - client.off( - GroupCallEventHandlerEvent.Incoming, - onGroupCallIncoming - ); - resolve(groupCall); - } - }; - client.on(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming); + } - const timeout = setTimeout(() => { + // We don't have permission to create the call, so all we can do is wait + // for one to come in + return new Promise((resolve, reject) => { + const onGroupCallIncoming = (groupCall: GroupCall) => { + if (groupCall?.room.roomId === room.roomId) { + clearTimeout(timeout); client.off( GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming ); - reject(new Error("Fetching group call timed out.")); - }, 30000); - }); - } + resolve(groupCall); + } + }; + client.on(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming); + + const timeout = setTimeout(() => { + client.off(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming); + reject(new Error("Fetching group call timed out.")); + }, 30000); + }); }; fetchOrCreateGroupCall() From d77d953f84448f6796d6669cae1b00ecd21457f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 16 Jul 2022 08:27:24 +0200 Subject: [PATCH 15/17] Be more explicit in `.vscode/settings.json` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .vscode/settings.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b43834a..fe488f68 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,21 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.insertSpaces": true, - "editor.tabSize": 2 + "editor.tabSize": 2, + "[typescriptreact]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + }, + "[javascriptreact]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + }, + "[typescript]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + }, + "[javascript]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + } } From 32b37ed8f08f16448a8ee5d23de0efcc5dd0c651 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Jul 2022 16:01:29 +0100 Subject: [PATCH 16/17] Fix 'cannot find room' error We weren't waiting for rooms to arrive down the sync stream after joining them but before trying to use them. More regression details in linked issue. Fixes https://github.com/vector-im/element-call/issues/477 --- src/home/RegisteredView.jsx | 2 +- src/home/UnauthenticatedView.jsx | 2 +- src/matrix-utils.ts | 6 ++--- src/room/useLoadGroupCall.ts | 40 +++++++++++++++++++++++++++++--- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/home/RegisteredView.jsx b/src/home/RegisteredView.jsx index 1d07d1c7..b5fef745 100644 --- a/src/home/RegisteredView.jsx +++ b/src/home/RegisteredView.jsx @@ -47,7 +47,7 @@ export function RegisteredView({ client }) { setError(undefined); setLoading(true); - const roomIdOrAlias = await createRoom(client, roomName, ptt); + const [roomIdOrAlias] = await createRoom(client, roomName, ptt); if (roomIdOrAlias) { history.push(`/room/${roomIdOrAlias}`); diff --git a/src/home/UnauthenticatedView.jsx b/src/home/UnauthenticatedView.jsx index f324d504..a6d7a6ed 100644 --- a/src/home/UnauthenticatedView.jsx +++ b/src/home/UnauthenticatedView.jsx @@ -70,7 +70,7 @@ export function UnauthenticatedView() { let roomIdOrAlias; try { - roomIdOrAlias = await createRoom(client, roomName, ptt); + [roomIdOrAlias] = await createRoom(client, roomName, ptt); } catch (error) { if (error.errcode === "M_ROOM_IN_USE") { setOnFinished(() => () => { diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 9b6a005b..5986954b 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -220,8 +220,8 @@ export function isLocalRoomId(roomId: string): boolean { export async function createRoom( client: MatrixClient, name: string -): Promise { - await client.createRoom({ +): Promise<[string, string]> { + const result = await client.createRoom({ visibility: Visibility.Private, preset: Preset.PublicChat, name, @@ -251,7 +251,7 @@ export async function createRoom( }, }); - return fullAliasFromRoomName(name, client); + return [fullAliasFromRoomName(name, client), result.room_id]; } export function getRoomUrl(roomId: string): string { diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 0d7fb7c7..301ba54f 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -21,6 +21,7 @@ import { GroupCallIntent, } from "matrix-js-sdk/src/webrtc/groupCall"; import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler"; +import { ClientEvent } from "matrix-js-sdk/src/client"; import type { MatrixClient } from "matrix-js-sdk/src/client"; import type { Room } from "matrix-js-sdk/src/models/room"; @@ -44,9 +45,38 @@ export const useLoadGroupCall = ( useEffect(() => { setState({ loading: true }); + const waitForRoom = async (roomId: string): Promise => { + const room = client.getRoom(roomId); + if (room) return room; + console.log(`Room ${roomId} hasn't arrived yet: waiting`); + + const waitPromise = new Promise((resolve) => { + const onRoomEvent = async (room: Room) => { + if (room.roomId === roomId) { + client.removeListener(ClientEvent.Room, onRoomEvent); + resolve(room); + } + }; + client.on(ClientEvent.Room, onRoomEvent); + }); + + // race the promise with a timeout so we don't + // wait forever for the room + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error("Timed out trying to join room")); + }, 30000); + }); + + return Promise.race([waitPromise, timeoutPromise]); + }; + const fetchOrCreateRoom = async (): Promise => { try { - return await client.joinRoom(roomIdOrAlias, { viaServers }); + const room = await client.joinRoom(roomIdOrAlias, { viaServers }); + // wait for the room to come down the sync stream, otherwise + // client.getRoom() won't return the room. + return waitForRoom(room.roomId); } catch (error) { if ( isLocalRoomId(roomIdOrAlias) && @@ -55,8 +85,12 @@ export const useLoadGroupCall = ( error.message.indexOf("Failed to fetch alias") !== -1)) ) { // The room doesn't exist, but we can create it - await createRoom(client, roomNameFromRoomId(roomIdOrAlias)); - return await client.joinRoom(roomIdOrAlias, { viaServers }); + const [, roomId] = await createRoom( + client, + roomNameFromRoomId(roomIdOrAlias) + ); + // likewise, wait for the room + return await waitForRoom(roomId); } else { throw error; } From 56afbe6eb15a3819460cf41c367567dd2bae9ab8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Jul 2022 20:49:07 +0100 Subject: [PATCH 17/17] Fix crash on screen share Don't try to wire up audio nodes if the stream has no audio track, 'cos it'll crash. Fixes https://github.com/vector-im/element-call/issues/421 --- src/video-grid/useMediaStream.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/video-grid/useMediaStream.ts b/src/video-grid/useMediaStream.ts index 79e8292b..5fbf57d0 100644 --- a/src/video-grid/useMediaStream.ts +++ b/src/video-grid/useMediaStream.ts @@ -202,7 +202,12 @@ export const useSpatialMediaStream = ( const sourceRef = useRef(); useEffect(() => { - if (spatialAudio && tileRef.current && !mute) { + if ( + spatialAudio && + tileRef.current && + !mute && + stream.getAudioTracks().length > 0 + ) { if (!pannerNodeRef.current) { pannerNodeRef.current = new PannerNode(audioContext, { panningModel: "HRTF",