Merge branch 'main' into matroska

This commit is contained in:
Robin Townsend 2022-07-15 14:34:50 -04:00
commit 3fc8fe505b
11 changed files with 180 additions and 55 deletions

View file

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
node_modules
.DS_Store
.env
dist
dist-ssr
*.local

View file

@ -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
```

View file

@ -117,12 +117,15 @@ export const ClientProvider: FC<Props> = ({ children }) => {
session;
logger.log("Using a standalone client");
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 };

View file

@ -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,
});
}

View file

@ -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];

View file

@ -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);

View file

@ -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: (displayName: string) => Promise<void>;
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 };
}

View file

@ -26,6 +26,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<void>((resolve, reject) => {
const onSync = (
@ -66,6 +79,12 @@ const SEND_RECV_TO_DEVICE = [
"org.matrix.call_duplicate_session",
];
/**
* Initialises and returns a new widget-API-based Matrix Client.
* @param widgetId The ID of the widget that the app is running inside.
* @param parentUrl The URL of the parent client.
* @returns The MatrixClient instance
*/
export async function initMatroskaClient(
widgetId: string, parentUrl: string,
): Promise<MatrixClient> {
@ -102,8 +121,18 @@ export async function initMatroskaClient(
return client;
}
/**
* Initialises and returns a new standalone 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<MatrixClient> {
// TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10
window.OLM_OPTIONS = {};
@ -121,17 +150,45 @@ 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.
// 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(
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);

View file

@ -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();

View file

@ -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 <LoadingView />;
}