mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-15 00:04:59 +08:00
Merge branch 'main' into ts_Form+Home
This commit is contained in:
commit
f26ab2f941
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
|
||||
```
|
||||
|
||||
|
@ -38,11 +38,11 @@
|
||||
"classnames": "^2.3.1",
|
||||
"color-hash": "^2.0.1",
|
||||
"events": "^3.3.0",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#9a15094374f52053ca9f833269d2b1c6c7f964d2",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#984dd26a138411ef73903ff4e635f2752e0829f2",
|
||||
"mermaid": "^8.13.8",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pako": "^2.0.4",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"postcss-preset-env": "^7",
|
||||
"re-resizable": "^6.9.0",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
|
@ -72,7 +72,11 @@ type ClientProviderState = Omit<
|
||||
"changePassword" | "logout" | "setClient"
|
||||
> & { error?: Error };
|
||||
|
||||
export const ClientProvider: FC = ({ children }) => {
|
||||
interface Props {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
const history = useHistory();
|
||||
const [
|
||||
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
||||
@ -98,12 +102,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 };
|
||||
|
@ -24,7 +24,11 @@ export function Facepile({
|
||||
<div
|
||||
className={classNames(styles.facepile, styles[size], className)}
|
||||
title={participants.map((member) => member.name).join(", ")}
|
||||
style={{ width: participants.length * (_size - _overlap) + _overlap }}
|
||||
style={{
|
||||
width:
|
||||
Math.min(participants.length, max + 1) * (_size - _overlap) +
|
||||
_overlap,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{participants.slice(0, max).map((member, i) => {
|
||||
|
@ -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 };
|
||||
}
|
@ -121,8 +121,10 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
|
||||
{...mergeProps(rest, filteredButtonProps)}
|
||||
ref={buttonRef}
|
||||
>
|
||||
{children}
|
||||
{variant === "dropdown" && <ArrowDownIcon />}
|
||||
<>
|
||||
{children}
|
||||
{variant === "dropdown" && <ArrowDownIcon />}
|
||||
</>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ export function RegisteredView({ client }: { client: MatrixClient }) {
|
||||
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 {
|
||||
|
@ -19,12 +19,18 @@ import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
||||
export function GroupCallLoader({
|
||||
client,
|
||||
roomId,
|
||||
viaServers,
|
||||
createPtt,
|
||||
children,
|
||||
}) {
|
||||
const { loading, error, groupCall } = useLoadGroupCall(
|
||||
client,
|
||||
roomId,
|
||||
viaServers,
|
||||
true
|
||||
createPtt
|
||||
);
|
||||
|
||||
usePageTitle(groupCall ? groupCall.room.name : "Loading...");
|
||||
|
@ -61,7 +61,10 @@ export function GroupCallView({
|
||||
|
||||
useEffect(() => {
|
||||
window.groupCall = groupCall;
|
||||
}, [groupCall]);
|
||||
|
||||
// In embedded mode, bypass the lobby and just enter the call straight away
|
||||
if (isEmbedded) groupCall.enter();
|
||||
}, [groupCall, isEmbedded]);
|
||||
|
||||
useSentryGroupCallHandler(groupCall);
|
||||
|
||||
@ -128,24 +131,32 @@ export function GroupCallView({
|
||||
} else if (left) {
|
||||
return <CallEndedView client={client} />;
|
||||
} else {
|
||||
return (
|
||||
<LobbyView
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
hasLocalParticipant={hasLocalParticipant}
|
||||
roomName={groupCall.room.name}
|
||||
avatarUrl={avatarUrl}
|
||||
state={state}
|
||||
onInitLocalCallFeed={initLocalCallFeed}
|
||||
localCallFeed={localCallFeed}
|
||||
onEnter={enter}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
roomId={roomId}
|
||||
isEmbedded={isEmbedded}
|
||||
/>
|
||||
);
|
||||
if (isEmbedded) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Loading room...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LobbyView
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
hasLocalParticipant={hasLocalParticipant}
|
||||
roomName={groupCall.room.name}
|
||||
avatarUrl={avatarUrl}
|
||||
state={state}
|
||||
onInitLocalCallFeed={initLocalCallFeed}
|
||||
localCallFeed={localCallFeed}
|
||||
onEnter={enter}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
roomId={roomId}
|
||||
isEmbedded={isEmbedded}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,13 +57,17 @@ export const PTTButton: React.FC<Props> = ({
|
||||
const buttonRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const [activeTouchId, setActiveTouchId] = useState<number | null>(null);
|
||||
const [buttonHeld, setButtonHeld] = useState(false);
|
||||
|
||||
const hold = useCallback(() => {
|
||||
// This update is delayed so the user only sees it if latency is significant
|
||||
if (buttonHeld) return;
|
||||
setButtonHeld(true);
|
||||
enqueueNetworkWaiting(true, 100);
|
||||
startTalking();
|
||||
}, [enqueueNetworkWaiting, startTalking]);
|
||||
}, [enqueueNetworkWaiting, startTalking, buttonHeld]);
|
||||
const unhold = useCallback(() => {
|
||||
setButtonHeld(false);
|
||||
setNetworkWaiting(false);
|
||||
stopTalking();
|
||||
}, [setNetworkWaiting, stopTalking]);
|
||||
|
@ -28,6 +28,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.participants > p {
|
||||
|
@ -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,13 +30,37 @@ export function RoomPage() {
|
||||
|
||||
const { roomId: maybeRoomId } = useParams();
|
||||
const { hash, search } = useLocation();
|
||||
const [viaServers, isEmbedded] = useMemo(() => {
|
||||
const [viaServers, isEmbedded, isPtt, displayName] = useMemo(() => {
|
||||
const params = new URLSearchParams(search);
|
||||
return [params.getAll("via"), params.has("embed")];
|
||||
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 />;
|
||||
}
|
||||
|
||||
@ -49,7 +74,12 @@ export function RoomPage() {
|
||||
|
||||
return (
|
||||
<MediaHandlerProvider client={client}>
|
||||
<GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}>
|
||||
<GroupCallLoader
|
||||
client={client}
|
||||
roomId={roomId}
|
||||
viaServers={viaServers}
|
||||
createPtt={isPtt}
|
||||
>
|
||||
{(groupCall) => (
|
||||
<GroupCallView
|
||||
client={client}
|
||||
|
@ -1,108 +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) {
|
||||
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);
|
||||
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]);
|
||||
|
||||
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;
|
||||
};
|
@ -130,7 +130,7 @@ export const usePTT = (
|
||||
const onMuteStateChanged = useCallback(() => {
|
||||
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
|
||||
|
||||
let blocked = false;
|
||||
let blocked = transmitBlocked;
|
||||
if (activeSpeakerUserId === null && activeSpeakerFeed !== null) {
|
||||
if (activeSpeakerFeed.userId === client.getUserId()) {
|
||||
playClip(PTTClipID.START_TALKING_LOCAL);
|
||||
@ -141,8 +141,8 @@ export const usePTT = (
|
||||
playClip(PTTClipID.END_TALKING);
|
||||
} else if (
|
||||
pttButtonHeld &&
|
||||
activeSpeakerUserId === client.getUserId() &&
|
||||
activeSpeakerFeed?.userId !== client.getUserId()
|
||||
activeSpeakerFeed?.userId !== client.getUserId() &&
|
||||
!transmitBlocked
|
||||
) {
|
||||
// We were talking but we've been cut off: mute our own mic
|
||||
// (this is the easier way of cutting other speakers off if an
|
||||
@ -167,6 +167,7 @@ export const usePTT = (
|
||||
client,
|
||||
userMediaFeeds,
|
||||
setMicMuteWrapper,
|
||||
transmitBlocked,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -202,7 +202,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…
Reference in New Issue
Block a user