Merge remote-tracking branch 'upstream/main' into SimonBrandner/feat/local-volume
This commit is contained in:
commit
b47d633727
18 changed files with 357 additions and 192 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
.env
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
|
18
.vscode/settings.json
vendored
18
.vscode/settings.json
vendored
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -101,12 +101,15 @@ export const ClientProvider: FC<Props> = ({ 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 };
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
59
src/auth/useRegisterPasswordlessUser.ts
Normal file
59
src/auth/useRegisterPasswordlessUser.ts
Normal 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 };
|
||||
}
|
|
@ -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}`);
|
||||
|
|
|
@ -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(() => () => {
|
||||
|
|
|
@ -9,10 +9,6 @@ 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 { 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";
|
||||
|
||||
|
@ -24,6 +20,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 = (
|
||||
|
@ -43,8 +52,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<MatrixClient> {
|
||||
// TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10
|
||||
window.OLM_OPTIONS = {};
|
||||
|
@ -62,17 +81,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);
|
||||
|
@ -172,10 +219,9 @@ export function isLocalRoomId(roomId: string): boolean {
|
|||
|
||||
export async function createRoom(
|
||||
client: MatrixClient,
|
||||
name: string,
|
||||
isPtt = false
|
||||
): Promise<string> {
|
||||
const createRoomResult = await client.createRoom({
|
||||
name: string
|
||||
): Promise<[string, string]> {
|
||||
const result = await client.createRoom({
|
||||
visibility: Visibility.Private,
|
||||
preset: Preset.PublicChat,
|
||||
name,
|
||||
|
@ -205,16 +251,7 @@ export async function createRoom(
|
|||
},
|
||||
});
|
||||
|
||||
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);
|
||||
return [fullAliasFromRoomName(name, client), result.room_id];
|
||||
}
|
||||
|
||||
export function getRoomUrl(roomId: string): string {
|
||||
|
|
|
@ -30,7 +30,6 @@ export function GroupCallLoader({
|
|||
client,
|
||||
roomId,
|
||||
viaServers,
|
||||
true,
|
||||
createPtt
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 />;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
154
src/room/useLoadGroupCall.ts
Normal file
154
src/room/useLoadGroupCall.ts
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
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 { 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";
|
||||
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
|
||||
): GroupCallLoadState => {
|
||||
const [state, setState] = useState<GroupCallLoadState>({ loading: true });
|
||||
|
||||
useEffect(() => {
|
||||
setState({ loading: true });
|
||||
|
||||
const waitForRoom = async (roomId: string): Promise<Room> => {
|
||||
const room = client.getRoom(roomId);
|
||||
if (room) return room;
|
||||
console.log(`Room ${roomId} hasn't arrived yet: waiting`);
|
||||
|
||||
const waitPromise = new Promise<Room>((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<Room>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error("Timed out trying to join room"));
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
return Promise.race([waitPromise, timeoutPromise]);
|
||||
};
|
||||
|
||||
const fetchOrCreateRoom = async (): Promise<Room> => {
|
||||
try {
|
||||
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) &&
|
||||
(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
|
||||
const [, roomId] = await createRoom(
|
||||
client,
|
||||
roomNameFromRoomId(roomIdOrAlias)
|
||||
);
|
||||
// likewise, wait for the room
|
||||
return await waitForRoom(roomId);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchOrCreateGroupCall = async (): Promise<GroupCall> => {
|
||||
const room = await fetchOrCreateRoom();
|
||||
const groupCall = client.getGroupCallForRoom(room.roomId);
|
||||
|
||||
if (groupCall) return groupCall;
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
|
@ -212,7 +212,12 @@ export const useSpatialMediaStream = (
|
|||
const sourceRef = useRef<MediaStreamAudioSourceNode>();
|
||||
|
||||
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",
|
||||
|
|
Loading…
Add table
Reference in a new issue