Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f7e0e5a0 | ||
|
|
25388a77aa | ||
|
|
2155d9bb80 | ||
|
|
46ab10f733 | ||
|
|
6e91ec3a0e | ||
|
|
b55aa12100 | ||
|
|
ded6a80b58 | ||
|
|
7435f1101a | ||
|
|
7720770c67 | ||
|
|
d9fc9e82ab | ||
|
|
ae66e4b3f8 | ||
|
|
1e65f10d3f | ||
|
|
a76f27152b | ||
|
|
de0df4b534 | ||
|
|
f78cf6e79a | ||
|
|
b84c36eb2e | ||
|
|
6355aa863c | ||
|
|
80cc10e8b9 | ||
|
|
10c37d205a | ||
|
|
a9e94c341c | ||
|
|
3b181224fd | ||
|
|
89fa9dfd64 | ||
|
|
4a08ae75b3 | ||
|
|
d9b0f45c6a | ||
|
|
c5a3fb72e1 | ||
|
|
f0d7d8fac6 | ||
|
|
1f485bfd55 | ||
|
|
9e367db324 | ||
|
|
a2fdab8eb9 | ||
|
|
2c052c162f | ||
|
|
b1c9e8c07a | ||
|
|
f71817b0a2 | ||
|
|
73d09bc99c | ||
|
|
5ebb54a857 | ||
|
|
8725b2c230 | ||
|
|
fd18f2acdf | ||
|
|
3bffe58549 | ||
|
|
e8bc22370b | ||
|
|
b7be3011da | ||
|
|
f0045c9406 | ||
|
|
3186b5f24b | ||
|
|
ca5ce7d468 | ||
|
|
a05f6a64a8 | ||
|
|
70dffe95ff | ||
|
|
0360889fd6 | ||
|
|
7304411c5d | ||
|
|
22dd095ea9 | ||
|
|
30a270193f | ||
|
|
ee1dd2293e | ||
|
|
34d5e88def | ||
|
|
30c9dfce02 | ||
|
|
48ad4d040d | ||
|
|
1b4f097b1c |
@@ -38,7 +38,7 @@
|
|||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"color-hash": "^2.0.1",
|
"color-hash": "^2.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#965f4fb13b4b36b26a3f4d7214cc7630d9f579a5",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#f41b7706e489cc1b83e6b25dd50091be2b5a9083",
|
||||||
"matrix-widget-api": "^1.0.0",
|
"matrix-widget-api": "^1.0.0",
|
||||||
"mermaid": "^8.13.8",
|
"mermaid": "^8.13.8",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
"sass": "^1.42.1",
|
"sass": "^1.42.1",
|
||||||
"storybook-builder-vite": "^0.1.12",
|
"storybook-builder-vite": "^0.1.12",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.6.4",
|
||||||
|
"typescript-strict-plugin": "^2.0.1",
|
||||||
"vite": "^2.4.2",
|
"vite": "^2.4.2",
|
||||||
"vite-plugin-html-template": "^1.1.0",
|
"vite-plugin-html-template": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^0.4.0"
|
"vite-plugin-svgr": "^0.4.0"
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||||||
import { ErrorView } from "./FullScreenView";
|
import { ErrorView } from "./FullScreenView";
|
||||||
import {
|
import {
|
||||||
initClient,
|
initClient,
|
||||||
initMatroskaClient,
|
|
||||||
defaultHomeserver,
|
defaultHomeserver,
|
||||||
CryptoStoreIntegrityError,
|
CryptoStoreIntegrityError,
|
||||||
} from "./matrix-utils";
|
} from "./matrix-utils";
|
||||||
|
import { widget } from "./widget";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -100,16 +100,12 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
const init = async (): Promise<
|
const init = async (): Promise<
|
||||||
Pick<ClientProviderState, "client" | "isPasswordlessUser">
|
Pick<ClientProviderState, "client" | "isPasswordlessUser">
|
||||||
> => {
|
> => {
|
||||||
const query = new URLSearchParams(window.location.search);
|
if (widget) {
|
||||||
const widgetId = query.get("widgetId");
|
// We're inside a widget, so let's engage *matryoshka mode*
|
||||||
const parentUrl = query.get("parentUrl");
|
logger.log("Using a matryoshka client");
|
||||||
|
|
||||||
if (widgetId && parentUrl) {
|
|
||||||
// We're inside a widget, so let's engage *Matroska mode*
|
|
||||||
logger.log("Using a Matroska client");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
client: await initMatroskaClient(widgetId, parentUrl),
|
client: await widget.client,
|
||||||
isPasswordlessUser: false,
|
isPasswordlessUser: false,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -256,13 +252,27 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
[client]
|
[client]
|
||||||
);
|
);
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(async () => {
|
||||||
|
await client.logout(undefined, true);
|
||||||
|
await client.clearStores();
|
||||||
clearSession();
|
clearSession();
|
||||||
|
setState({
|
||||||
|
client: undefined,
|
||||||
|
loading: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isPasswordlessUser: true,
|
||||||
|
userName: "",
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}, [history]);
|
}, [history, client]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (client) {
|
// To protect against multiple sessions writing to the same storage
|
||||||
|
// simultaneously, we send a to-device message that shuts down all other
|
||||||
|
// running instances of the app. This isn't necessary if the app is running
|
||||||
|
// in a widget though, since then it'll be mostly stateless.
|
||||||
|
if (!widget && client) {
|
||||||
const loadTime = Date.now();
|
const loadTime = Date.now();
|
||||||
|
|
||||||
const onToDeviceEvent = (event: MatrixEvent) => {
|
const onToDeviceEvent = (event: MatrixEvent) => {
|
||||||
|
|||||||
90
src/LazyEventEmitter.ts
Normal file
90
src/LazyEventEmitter.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
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 EventEmitter from "events";
|
||||||
|
|
||||||
|
type NonEmptyArray<T> = [T, ...T[]];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event emitter that lets events pile up in a backlog until a listener is
|
||||||
|
* present, at which point any events that were missed are re-emitted.
|
||||||
|
*/
|
||||||
|
export class LazyEventEmitter extends EventEmitter {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
private eventBacklogs = new Map<string | symbol, NonEmptyArray<any[]>>();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public emit(type: string | symbol, ...args: any[]): boolean {
|
||||||
|
const hasListeners = super.emit(type, ...args);
|
||||||
|
|
||||||
|
if (!hasListeners) {
|
||||||
|
// The event was missed, so add it to the backlog
|
||||||
|
const backlog = this.eventBacklogs.get(type);
|
||||||
|
if (backlog) {
|
||||||
|
backlog.push(args);
|
||||||
|
} else {
|
||||||
|
// Start a new backlog
|
||||||
|
this.eventBacklogs.set(type, [args]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public on(type: string | symbol, listener: (...args: any[]) => void): this {
|
||||||
|
super.on(type, listener);
|
||||||
|
|
||||||
|
const backlog = this.eventBacklogs.get(type);
|
||||||
|
if (backlog) {
|
||||||
|
// That was the first listener for this type, so let's send it all the
|
||||||
|
// events that have piled up
|
||||||
|
for (const args of backlog) super.emit(type, ...args);
|
||||||
|
// Backlog is now clear
|
||||||
|
this.eventBacklogs.delete(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener(
|
||||||
|
type: string | symbol,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
listener: (...args: any[]) => void
|
||||||
|
): this {
|
||||||
|
return this.on(type, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public once(type: string | symbol, listener: (...args: any[]) => void): this {
|
||||||
|
super.once(type, listener);
|
||||||
|
|
||||||
|
const backlog = this.eventBacklogs.get(type);
|
||||||
|
if (backlog) {
|
||||||
|
// That was the first listener for this type, so let's send it the first
|
||||||
|
// of the events that have piled up
|
||||||
|
super.emit(type, ...backlog[0]);
|
||||||
|
// Clear the event from the backlog
|
||||||
|
if (backlog.length === 1) {
|
||||||
|
this.eventBacklogs.delete(type);
|
||||||
|
} else {
|
||||||
|
backlog.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,23 +5,18 @@ import { MemoryStore } from "matrix-js-sdk/src/store/memory";
|
|||||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||||
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
|
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
|
||||||
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
|
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
|
||||||
import {
|
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||||
createClient,
|
|
||||||
createRoomWidgetClient,
|
|
||||||
MatrixClient,
|
|
||||||
} from "matrix-js-sdk/src/matrix";
|
|
||||||
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
||||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
|
||||||
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||||
import { WidgetApi } from "matrix-widget-api";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import {
|
import {
|
||||||
GroupCallIntent,
|
GroupCallIntent,
|
||||||
GroupCallType,
|
GroupCallType,
|
||||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
|
||||||
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
||||||
import { getRoomParams } from "./room/useRoomParams";
|
import { getRoomParams } from "./room/useRoomParams";
|
||||||
@@ -64,73 +59,6 @@ function waitForSync(client: MatrixClient) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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> {
|
|
||||||
// In this mode, we use a special client which routes all requests through
|
|
||||||
// the host application via the widget API
|
|
||||||
|
|
||||||
const { roomId, userId, deviceId } = getRoomParams();
|
|
||||||
if (!roomId) throw new Error("Room ID must be supplied");
|
|
||||||
if (!userId) throw new Error("User ID must be supplied");
|
|
||||||
if (!deviceId) throw new Error("Device ID must be supplied");
|
|
||||||
|
|
||||||
// These are all the event types the app uses
|
|
||||||
const sendState = [
|
|
||||||
{ eventType: EventType.GroupCallPrefix },
|
|
||||||
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
|
|
||||||
];
|
|
||||||
const receiveState = [
|
|
||||||
{ eventType: EventType.RoomMember },
|
|
||||||
{ eventType: EventType.GroupCallPrefix },
|
|
||||||
{ eventType: EventType.GroupCallMemberPrefix },
|
|
||||||
];
|
|
||||||
const sendRecvToDevice = [
|
|
||||||
EventType.CallInvite,
|
|
||||||
EventType.CallCandidates,
|
|
||||||
EventType.CallAnswer,
|
|
||||||
EventType.CallHangup,
|
|
||||||
EventType.CallReject,
|
|
||||||
EventType.CallSelectAnswer,
|
|
||||||
EventType.CallNegotiate,
|
|
||||||
EventType.CallSDPStreamMetadataChanged,
|
|
||||||
EventType.CallSDPStreamMetadataChangedPrefix,
|
|
||||||
EventType.CallReplaces,
|
|
||||||
"org.matrix.call_duplicate_session",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Since all data should be coming from the host application, there's no
|
|
||||||
// need to persist anything, and therefore we can use the default stores
|
|
||||||
// We don't even need to set up crypto
|
|
||||||
const client = createRoomWidgetClient(
|
|
||||||
new WidgetApi(widgetId, new URL(parentUrl).origin),
|
|
||||||
{
|
|
||||||
sendState,
|
|
||||||
receiveState,
|
|
||||||
sendToDevice: sendRecvToDevice,
|
|
||||||
receiveToDevice: sendRecvToDevice,
|
|
||||||
turnServers: true,
|
|
||||||
},
|
|
||||||
roomId,
|
|
||||||
{
|
|
||||||
baseUrl: "",
|
|
||||||
userId,
|
|
||||||
deviceId,
|
|
||||||
timelineSupport: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await client.startClient();
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialises and returns a new standalone Matrix Client.
|
* Initialises and returns a new standalone Matrix Client.
|
||||||
* If true is passed for the 'restore' parameter, a check will be made
|
* If true is passed for the 'restore' parameter, a check will be made
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { useHistory } from "react-router-dom";
|
|||||||
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
|
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
|
||||||
import { useGroupCall } from "./useGroupCall";
|
import { useGroupCall } from "./useGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
import { LobbyView } from "./LobbyView";
|
import { LobbyView } from "./LobbyView";
|
||||||
@@ -28,22 +30,30 @@ import { CallEndedView } from "./CallEndedView";
|
|||||||
import { useRoomAvatar } from "./useRoomAvatar";
|
import { useRoomAvatar } from "./useRoomAvatar";
|
||||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
groupCall: GroupCall;
|
groupCall?: GroupCall;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
isPasswordlessUser: boolean;
|
isPasswordlessUser: boolean;
|
||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
|
preload: boolean;
|
||||||
|
hideHeader: boolean;
|
||||||
roomIdOrAlias: string;
|
roomIdOrAlias: string;
|
||||||
groupCall: GroupCall;
|
groupCall: GroupCall;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupCallView({
|
export function GroupCallView({
|
||||||
client,
|
client,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
|
preload,
|
||||||
|
hideHeader,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
groupCall,
|
groupCall,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
@@ -69,14 +79,50 @@ export function GroupCallView({
|
|||||||
unencryptedEventsFromUsers,
|
unencryptedEventsFromUsers,
|
||||||
} = useGroupCall(groupCall);
|
} = useGroupCall(groupCall);
|
||||||
|
|
||||||
|
const { setAudioInput, setVideoInput } = useMediaHandler();
|
||||||
|
|
||||||
const avatarUrl = useRoomAvatar(groupCall.room);
|
const avatarUrl = useRoomAvatar(groupCall.room);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.groupCall = groupCall;
|
window.groupCall = groupCall;
|
||||||
|
return () => {
|
||||||
|
delete window.groupCall;
|
||||||
|
};
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
// In embedded mode, bypass the lobby and just enter the call straight away
|
useEffect(() => {
|
||||||
if (isEmbedded) groupCall.enter();
|
if (widget && preload) {
|
||||||
}, [groupCall, isEmbedded]);
|
// In preload mode, wait for a join action before entering
|
||||||
|
const onJoin = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
const { audioInput, videoInput } = ev.detail
|
||||||
|
.data as unknown as JoinCallData;
|
||||||
|
if (audioInput !== null) setAudioInput(audioInput);
|
||||||
|
if (videoInput !== null) setVideoInput(videoInput);
|
||||||
|
await Promise.all([
|
||||||
|
groupCall.setMicrophoneMuted(audioInput === null),
|
||||||
|
groupCall.setLocalVideoMuted(videoInput === null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await groupCall.enter();
|
||||||
|
await Promise.all([
|
||||||
|
widget.api.setAlwaysOnScreen(true),
|
||||||
|
widget.api.transport.reply(ev.detail, {}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||||
|
return () => {
|
||||||
|
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [groupCall, preload, setAudioInput, setVideoInput]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEmbedded && !preload) {
|
||||||
|
// In embedded mode, bypass the lobby and just enter the call straight away
|
||||||
|
groupCall.enter();
|
||||||
|
}
|
||||||
|
}, [groupCall, isEmbedded, preload]);
|
||||||
|
|
||||||
useSentryGroupCallHandler(groupCall);
|
useSentryGroupCallHandler(groupCall);
|
||||||
|
|
||||||
@@ -88,11 +134,29 @@ export function GroupCallView({
|
|||||||
const onLeave = useCallback(() => {
|
const onLeave = useCallback(() => {
|
||||||
setLeft(true);
|
setLeft(true);
|
||||||
leave();
|
leave();
|
||||||
|
if (widget) {
|
||||||
|
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
|
||||||
|
widget.api.setAlwaysOnScreen(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isPasswordlessUser) {
|
if (!isPasswordlessUser && !isEmbedded) {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
}, [leave, isPasswordlessUser, history]);
|
}, [leave, isPasswordlessUser, isEmbedded, history]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (widget && state === GroupCallState.Entered) {
|
||||||
|
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
leave();
|
||||||
|
await widget.api.transport.reply(ev.detail, {});
|
||||||
|
widget.api.setAlwaysOnScreen(false);
|
||||||
|
};
|
||||||
|
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
||||||
|
return () => {
|
||||||
|
widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [groupCall, state, leave]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorView error={error} />;
|
return <ErrorView error={error} />;
|
||||||
@@ -109,6 +173,7 @@ export function GroupCallView({
|
|||||||
userMediaFeeds={userMediaFeeds}
|
userMediaFeeds={userMediaFeeds}
|
||||||
onLeave={onLeave}
|
onLeave={onLeave}
|
||||||
isEmbedded={isEmbedded}
|
isEmbedded={isEmbedded}
|
||||||
|
hideHeader={hideHeader}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -131,6 +196,7 @@ export function GroupCallView({
|
|||||||
screenshareFeeds={screenshareFeeds}
|
screenshareFeeds={screenshareFeeds}
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
roomIdOrAlias={roomIdOrAlias}
|
||||||
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
||||||
|
hideHeader={hideHeader}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -141,33 +207,41 @@ export function GroupCallView({
|
|||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
} else if (left) {
|
} else if (left) {
|
||||||
return <CallEndedView client={client} />;
|
if (isPasswordlessUser) {
|
||||||
} else {
|
return <CallEndedView client={client} />;
|
||||||
if (isEmbedded) {
|
|
||||||
return (
|
|
||||||
<FullScreenView>
|
|
||||||
<h1>Loading room...</h1>
|
|
||||||
</FullScreenView>
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return (
|
// If the user is a regular user, we'll have sent them back to the homepage,
|
||||||
<LobbyView
|
// so just sit here & do nothing: otherwise we would (briefly) mount the
|
||||||
client={client}
|
// LobbyView again which would open capture devices again.
|
||||||
groupCall={groupCall}
|
return null;
|
||||||
roomName={groupCall.room.name}
|
|
||||||
avatarUrl={avatarUrl}
|
|
||||||
state={state}
|
|
||||||
onInitLocalCallFeed={initLocalCallFeed}
|
|
||||||
localCallFeed={localCallFeed}
|
|
||||||
onEnter={enter}
|
|
||||||
microphoneMuted={microphoneMuted}
|
|
||||||
localVideoMuted={localVideoMuted}
|
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
|
||||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
} else if (preload) {
|
||||||
|
return null;
|
||||||
|
} else if (isEmbedded) {
|
||||||
|
return (
|
||||||
|
<FullScreenView>
|
||||||
|
<h1>Loading room...</h1>
|
||||||
|
</FullScreenView>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<LobbyView
|
||||||
|
client={client}
|
||||||
|
groupCall={groupCall}
|
||||||
|
roomName={groupCall.room.name}
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
state={state}
|
||||||
|
onInitLocalCallFeed={initLocalCallFeed}
|
||||||
|
localCallFeed={localCallFeed}
|
||||||
|
onEnter={enter}
|
||||||
|
microphoneMuted={microphoneMuted}
|
||||||
|
localVideoMuted={localVideoMuted}
|
||||||
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||||
|
roomIdOrAlias={roomIdOrAlias}
|
||||||
|
isEmbedded={isEmbedded}
|
||||||
|
hideHeader={hideHeader}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ limitations under the License.
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 64px;
|
height: calc(50px + 2 * 8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer > * {
|
.footer > * {
|
||||||
@@ -54,7 +54,7 @@ limitations under the License.
|
|||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footerFullscreen {
|
.maximised .footer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -67,8 +67,14 @@ limitations under the License.
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-height: 300px) {
|
||||||
.footer {
|
.footer {
|
||||||
height: 118px;
|
height: calc(50px + 2 * 24px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.footer {
|
||||||
|
height: calc(50px + 2 * 32px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,17 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useMemo, useRef } from "react";
|
import React, { useEffect, useCallback, useMemo, useRef } from "react";
|
||||||
import { usePreventScroll } from "@react-aria/overlays";
|
import { usePreventScroll } from "@react-aria/overlays";
|
||||||
|
import useMeasure from "react-use-measure";
|
||||||
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import styles from "./InCallView.module.css";
|
import styles from "./InCallView.module.css";
|
||||||
import {
|
import {
|
||||||
HangupButton,
|
HangupButton,
|
||||||
@@ -52,6 +55,7 @@ import { useAudioContext } from "../video-grid/useMediaStream";
|
|||||||
import { useFullscreen } from "../video-grid/useFullscreen";
|
import { useFullscreen } from "../video-grid/useFullscreen";
|
||||||
import { AudioContainer } from "../video-grid/AudioContainer";
|
import { AudioContainer } from "../video-grid/AudioContainer";
|
||||||
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
|
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
|
||||||
|
import { widget, ElementWidgetActions } from "../widget";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
@@ -77,6 +81,7 @@ interface Props {
|
|||||||
localScreenshareFeed: CallFeed;
|
localScreenshareFeed: CallFeed;
|
||||||
roomIdOrAlias: string;
|
roomIdOrAlias: string;
|
||||||
unencryptedEventsFromUsers: Set<string>;
|
unencryptedEventsFromUsers: Set<string>;
|
||||||
|
hideHeader: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Participant {
|
export interface Participant {
|
||||||
@@ -105,11 +110,23 @@ export function InCallView({
|
|||||||
localScreenshareFeed,
|
localScreenshareFeed,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
unencryptedEventsFromUsers,
|
unencryptedEventsFromUsers,
|
||||||
|
hideHeader,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
usePreventScroll();
|
usePreventScroll();
|
||||||
const elementRef = useRef<HTMLDivElement>();
|
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
|
// Merge the refs so they can attach to the same element
|
||||||
|
const containerRef = useCallback(
|
||||||
|
(el: HTMLDivElement) => {
|
||||||
|
containerRef1.current = el;
|
||||||
|
containerRef2(el);
|
||||||
|
},
|
||||||
|
[containerRef1, containerRef2]
|
||||||
|
);
|
||||||
|
|
||||||
const { layout, setLayout } = useVideoGridLayout(screenshareFeeds.length > 0);
|
const { layout, setLayout } = useVideoGridLayout(screenshareFeeds.length > 0);
|
||||||
const { toggleFullscreen, fullscreenParticipant } = useFullscreen(elementRef);
|
const { toggleFullscreen, fullscreenParticipant } =
|
||||||
|
useFullscreen(containerRef1);
|
||||||
|
|
||||||
const [spatialAudio] = useSpatialAudio();
|
const [spatialAudio] = useSpatialAudio();
|
||||||
|
|
||||||
@@ -122,6 +139,42 @@ export function InCallView({
|
|||||||
|
|
||||||
useAudioOutputDevice(audioRef, audioOutput);
|
useAudioOutputDevice(audioRef, audioOutput);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
widget?.api.transport.send(
|
||||||
|
layout === "freedom"
|
||||||
|
? ElementWidgetActions.TileLayout
|
||||||
|
: ElementWidgetActions.SpotlightLayout,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}, [layout]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (widget) {
|
||||||
|
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
setLayout("freedom");
|
||||||
|
await widget.api.transport.reply(ev.detail, {});
|
||||||
|
};
|
||||||
|
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
setLayout("spotlight");
|
||||||
|
await widget.api.transport.reply(ev.detail, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
|
||||||
|
widget.lazyActions.on(
|
||||||
|
ElementWidgetActions.SpotlightLayout,
|
||||||
|
onSpotlightLayout
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
widget.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
|
||||||
|
widget.lazyActions.off(
|
||||||
|
ElementWidgetActions.SpotlightLayout,
|
||||||
|
onSpotlightLayout
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [setLayout]);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const participants: Participant[] = [];
|
const participants: Participant[] = [];
|
||||||
|
|
||||||
@@ -130,9 +183,7 @@ export function InCallView({
|
|||||||
id: callFeed.stream.id,
|
id: callFeed.stream.id,
|
||||||
callFeed,
|
callFeed,
|
||||||
focused:
|
focused:
|
||||||
screenshareFeeds.length === 0 && layout === "spotlight"
|
screenshareFeeds.length === 0 && callFeed.userId === activeSpeaker,
|
||||||
? callFeed.userId === activeSpeaker
|
|
||||||
: false,
|
|
||||||
isLocal: callFeed.isLocal(),
|
isLocal: callFeed.isLocal(),
|
||||||
presenter: false,
|
presenter: false,
|
||||||
});
|
});
|
||||||
@@ -157,7 +208,20 @@ export function InCallView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return participants;
|
return participants;
|
||||||
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
|
}, [userMediaFeeds, activeSpeaker, screenshareFeeds]);
|
||||||
|
|
||||||
|
// The maximised participant: either the participant that the user has
|
||||||
|
// manually put in fullscreen, or the focused (active) participant if the
|
||||||
|
// window is too small to show everyone
|
||||||
|
const maximisedParticipant = useMemo(
|
||||||
|
() =>
|
||||||
|
fullscreenParticipant ?? (bounds.height <= 500 && bounds.width <= 500)
|
||||||
|
? items.find((item) => item.focused) ??
|
||||||
|
items.find((item) => item.callFeed) ??
|
||||||
|
null
|
||||||
|
: null,
|
||||||
|
[fullscreenParticipant, bounds, items]
|
||||||
|
);
|
||||||
|
|
||||||
const renderAvatar = useCallback(
|
const renderAvatar = useCallback(
|
||||||
(roomMember: RoomMember, width: number, height: number) => {
|
(roomMember: RoomMember, width: number, height: number) => {
|
||||||
@@ -177,7 +241,7 @@ export function InCallView({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderContent = useCallback((): JSX.Element => {
|
const renderContent = (): JSX.Element => {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.centerMessage}>
|
<div className={styles.centerMessage}>
|
||||||
@@ -185,16 +249,19 @@ export function InCallView({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (fullscreenParticipant) {
|
if (maximisedParticipant) {
|
||||||
return (
|
return (
|
||||||
<VideoTileContainer
|
<VideoTileContainer
|
||||||
key={fullscreenParticipant.id}
|
height={bounds.height}
|
||||||
item={fullscreenParticipant}
|
width={bounds.width}
|
||||||
|
key={maximisedParticipant.id}
|
||||||
|
item={maximisedParticipant}
|
||||||
getAvatar={renderAvatar}
|
getAvatar={renderAvatar}
|
||||||
audioContext={audioContext}
|
audioContext={audioContext}
|
||||||
audioDestination={audioDestination}
|
audioDestination={audioDestination}
|
||||||
disableSpeakingIndicator={true}
|
disableSpeakingIndicator={true}
|
||||||
isFullscreen={!!fullscreenParticipant}
|
maximised={Boolean(maximisedParticipant)}
|
||||||
|
fullscreen={maximisedParticipant === fullscreenParticipant}
|
||||||
onFullscreen={toggleFullscreen}
|
onFullscreen={toggleFullscreen}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -210,43 +277,36 @@ export function InCallView({
|
|||||||
audioContext={audioContext}
|
audioContext={audioContext}
|
||||||
audioDestination={audioDestination}
|
audioDestination={audioDestination}
|
||||||
disableSpeakingIndicator={items.length < 3}
|
disableSpeakingIndicator={items.length < 3}
|
||||||
isFullscreen={!!fullscreenParticipant}
|
maximised={false}
|
||||||
|
fullscreen={false}
|
||||||
onFullscreen={toggleFullscreen}
|
onFullscreen={toggleFullscreen}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</VideoGrid>
|
</VideoGrid>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
fullscreenParticipant,
|
|
||||||
items,
|
|
||||||
audioContext,
|
|
||||||
audioDestination,
|
|
||||||
layout,
|
|
||||||
renderAvatar,
|
|
||||||
toggleFullscreen,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
modalState: rageshakeRequestModalState,
|
modalState: rageshakeRequestModalState,
|
||||||
modalProps: rageshakeRequestModalProps,
|
modalProps: rageshakeRequestModalProps,
|
||||||
} = useRageshakeRequestModal(groupCall.room.roomId);
|
} = useRageshakeRequestModal(groupCall.room.roomId);
|
||||||
|
|
||||||
const footerClassNames = classNames(styles.footer, {
|
const containerClasses = classNames(styles.inRoom, {
|
||||||
[styles.footerFullscreen]: fullscreenParticipant,
|
[styles.maximised]: maximisedParticipant,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.inRoom} ref={elementRef}>
|
<div className={containerClasses} ref={containerRef}>
|
||||||
<audio ref={audioRef} />
|
<audio ref={audioRef} />
|
||||||
{(!spatialAudio || fullscreenParticipant) && (
|
{(!spatialAudio || maximisedParticipant) && (
|
||||||
<AudioContainer
|
<AudioContainer
|
||||||
items={items}
|
items={items}
|
||||||
audioContext={audioContext}
|
audioContext={audioContext}
|
||||||
audioDestination={audioDestination}
|
audioDestination={audioDestination}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!fullscreenParticipant && (
|
{!hideHeader && !maximisedParticipant && (
|
||||||
<Header>
|
<Header>
|
||||||
<LeftNav>
|
<LeftNav>
|
||||||
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||||
@@ -262,16 +322,16 @@ export function InCallView({
|
|||||||
</Header>
|
</Header>
|
||||||
)}
|
)}
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
<div className={footerClassNames}>
|
<div className={styles.footer}>
|
||||||
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
|
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
|
||||||
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
|
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
|
||||||
{canScreenshare && !isSafari && !fullscreenParticipant && (
|
{canScreenshare && !isSafari && !maximisedParticipant && (
|
||||||
<ScreenshareButton
|
<ScreenshareButton
|
||||||
enabled={isScreensharing}
|
enabled={isScreensharing}
|
||||||
onPress={toggleScreensharing}
|
onPress={toggleScreensharing}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!fullscreenParticipant && (
|
{!maximisedParticipant && (
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
inCall
|
inCall
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
roomIdOrAlias={roomIdOrAlias}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ interface Props {
|
|||||||
localVideoMuted: boolean;
|
localVideoMuted: boolean;
|
||||||
roomIdOrAlias: string;
|
roomIdOrAlias: string;
|
||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
|
hideHeader: boolean;
|
||||||
}
|
}
|
||||||
export function LobbyView({
|
export function LobbyView({
|
||||||
client,
|
client,
|
||||||
@@ -63,6 +64,7 @@ export function LobbyView({
|
|||||||
toggleMicrophoneMuted,
|
toggleMicrophoneMuted,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
|
hideHeader,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { stream } = useCallFeed(localCallFeed);
|
const { stream } = useCallFeed(localCallFeed);
|
||||||
const {
|
const {
|
||||||
@@ -90,14 +92,16 @@ export function LobbyView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.room}>
|
<div className={styles.room}>
|
||||||
<Header>
|
{!hideHeader && (
|
||||||
<LeftNav>
|
<Header>
|
||||||
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
<LeftNav>
|
||||||
</LeftNav>
|
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||||
<RightNav>
|
</LeftNav>
|
||||||
<UserMenuContainer />
|
<RightNav>
|
||||||
</RightNav>
|
<UserMenuContainer />
|
||||||
</Header>
|
</RightNav>
|
||||||
|
</Header>
|
||||||
|
)}
|
||||||
<div className={styles.joinRoom}>
|
<div className={styles.joinRoom}>
|
||||||
<div className={styles.joinRoomContent}>
|
<div className={styles.joinRoomContent}>
|
||||||
{groupCall.isPtt ? (
|
{groupCall.isPtt ? (
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ interface Props {
|
|||||||
userMediaFeeds: CallFeed[];
|
userMediaFeeds: CallFeed[];
|
||||||
onLeave: () => void;
|
onLeave: () => void;
|
||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
|
hideHeader: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PTTCallView: React.FC<Props> = ({
|
export const PTTCallView: React.FC<Props> = ({
|
||||||
@@ -109,6 +110,7 @@ export const PTTCallView: React.FC<Props> = ({
|
|||||||
userMediaFeeds,
|
userMediaFeeds,
|
||||||
onLeave,
|
onLeave,
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
|
hideHeader,
|
||||||
}) => {
|
}) => {
|
||||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||||
useModalTriggerState();
|
useModalTriggerState();
|
||||||
@@ -176,7 +178,7 @@ export const PTTCallView: React.FC<Props> = ({
|
|||||||
// https://github.com/vector-im/element-call/issues/328
|
// https://github.com/vector-im/element-call/issues/328
|
||||||
show={false}
|
show={false}
|
||||||
/>
|
/>
|
||||||
{showControls && (
|
{!hideHeader && showControls && (
|
||||||
<Header className={styles.header}>
|
<Header className={styles.header}>
|
||||||
<LeftNav>
|
<LeftNav>
|
||||||
<RoomSetupHeaderInfo
|
<RoomSetupHeaderInfo
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useEffect, useState } from "react";
|
import React, { FC, useEffect, useState, useCallback } from "react";
|
||||||
|
|
||||||
|
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||||
import { RoomAuthView } from "./RoomAuthView";
|
import { RoomAuthView } from "./RoomAuthView";
|
||||||
@@ -29,8 +30,16 @@ export const RoomPage: FC = () => {
|
|||||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||||
useClient();
|
useClient();
|
||||||
|
|
||||||
const { roomAlias, roomId, viaServers, isEmbedded, isPtt, displayName } =
|
const {
|
||||||
useRoomParams();
|
roomAlias,
|
||||||
|
roomId,
|
||||||
|
viaServers,
|
||||||
|
isEmbedded,
|
||||||
|
preload,
|
||||||
|
hideHeader,
|
||||||
|
isPtt,
|
||||||
|
displayName,
|
||||||
|
} = useRoomParams();
|
||||||
const roomIdOrAlias = roomId ?? roomAlias;
|
const roomIdOrAlias = roomId ?? roomAlias;
|
||||||
if (!roomIdOrAlias) throw new Error("No room specified");
|
if (!roomIdOrAlias) throw new Error("No room specified");
|
||||||
|
|
||||||
@@ -53,6 +62,21 @@ export const RoomPage: FC = () => {
|
|||||||
registerPasswordlessUser,
|
registerPasswordlessUser,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const groupCallView = useCallback(
|
||||||
|
(groupCall: GroupCall) => (
|
||||||
|
<GroupCallView
|
||||||
|
client={client}
|
||||||
|
roomIdOrAlias={roomIdOrAlias}
|
||||||
|
groupCall={groupCall}
|
||||||
|
isPasswordlessUser={isPasswordlessUser}
|
||||||
|
isEmbedded={isEmbedded}
|
||||||
|
preload={preload}
|
||||||
|
hideHeader={hideHeader}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[client, roomIdOrAlias, isPasswordlessUser, isEmbedded, preload, hideHeader]
|
||||||
|
);
|
||||||
|
|
||||||
if (loading || isRegistering) {
|
if (loading || isRegistering) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
}
|
}
|
||||||
@@ -73,15 +97,7 @@ export const RoomPage: FC = () => {
|
|||||||
viaServers={viaServers}
|
viaServers={viaServers}
|
||||||
createPtt={isPtt}
|
createPtt={isPtt}
|
||||||
>
|
>
|
||||||
{(groupCall) => (
|
{groupCallView}
|
||||||
<GroupCallView
|
|
||||||
client={client}
|
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
|
||||||
groupCall={groupCall}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</GroupCallLoader>
|
</GroupCallLoader>
|
||||||
</MediaHandlerProvider>
|
</MediaHandlerProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import {
|
|||||||
GroupCallIntent,
|
GroupCallIntent,
|
||||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
|
import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
|
||||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||||
|
|
||||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
||||||
@@ -45,38 +46,15 @@ export const useLoadGroupCall = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState({ loading: true });
|
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> => {
|
const fetchOrCreateRoom = async (): Promise<Room> => {
|
||||||
try {
|
try {
|
||||||
const room = await client.joinRoom(roomIdOrAlias, { viaServers });
|
const room = await client.joinRoom(roomIdOrAlias, { viaServers });
|
||||||
// wait for the room to come down the sync stream, otherwise
|
logger.info(
|
||||||
// client.getRoom() won't return the room.
|
`Joined ${roomIdOrAlias}, waiting room to be ready for group calls`
|
||||||
return waitForRoom(room.roomId);
|
);
|
||||||
|
await client.waitUntilRoomReadyForGroupCalls(room.roomId);
|
||||||
|
logger.info(`${roomIdOrAlias}, is ready for group calls`);
|
||||||
|
return room;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
if (
|
||||||
isLocalRoomId(roomIdOrAlias) &&
|
isLocalRoomId(roomIdOrAlias) &&
|
||||||
@@ -91,7 +69,8 @@ export const useLoadGroupCall = (
|
|||||||
createPtt
|
createPtt
|
||||||
);
|
);
|
||||||
// likewise, wait for the room
|
// likewise, wait for the room
|
||||||
return await waitForRoom(roomId);
|
await client.waitUntilRoomReadyForGroupCalls(roomId);
|
||||||
|
return client.getRoom(roomId);
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -100,7 +79,9 @@ export const useLoadGroupCall = (
|
|||||||
|
|
||||||
const fetchOrCreateGroupCall = async (): Promise<GroupCall> => {
|
const fetchOrCreateGroupCall = async (): Promise<GroupCall> => {
|
||||||
const room = await fetchOrCreateRoom();
|
const room = await fetchOrCreateRoom();
|
||||||
|
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
|
||||||
const groupCall = client.getGroupCallForRoom(room.roomId);
|
const groupCall = client.getGroupCallForRoom(room.roomId);
|
||||||
|
logger.debug("Got group call", groupCall?.groupCallId);
|
||||||
|
|
||||||
if (groupCall) return groupCall;
|
if (groupCall) return groupCall;
|
||||||
|
|
||||||
@@ -111,7 +92,11 @@ export const useLoadGroupCall = (
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// The call doesn't exist, but we can create it
|
// The call doesn't exist, but we can create it
|
||||||
console.log(`Creating ${createPtt ? "PTT" : "video"} group call room`);
|
console.log(
|
||||||
|
`No call found in ${roomIdOrAlias}: creating ${
|
||||||
|
createPtt ? "PTT" : "video"
|
||||||
|
} call`
|
||||||
|
);
|
||||||
return await client.createGroupCall(
|
return await client.createGroupCall(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
createPtt ? GroupCallType.Voice : GroupCallType.Video,
|
createPtt ? GroupCallType.Voice : GroupCallType.Video,
|
||||||
@@ -142,7 +127,26 @@ export const useLoadGroupCall = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchOrCreateGroupCall()
|
const waitForClientSyncing = async () => {
|
||||||
|
if (client.getSyncState() !== SyncState.Syncing) {
|
||||||
|
logger.debug(
|
||||||
|
"useLoadGroupCall: waiting for client to start syncing..."
|
||||||
|
);
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const onSync = () => {
|
||||||
|
if (client.getSyncState() === SyncState.Syncing) {
|
||||||
|
client.off(ClientEvent.Sync, onSync);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
client.on(ClientEvent.Sync, onSync);
|
||||||
|
});
|
||||||
|
logger.debug("useLoadGroupCall: client is now syncing.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
waitForClientSyncing()
|
||||||
|
.then(fetchOrCreateGroupCall)
|
||||||
.then((groupCall) =>
|
.then((groupCall) =>
|
||||||
setState((prevState) => ({ ...prevState, loading: false, groupCall }))
|
setState((prevState) => ({ ...prevState, loading: false, groupCall }))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ export interface RoomParams {
|
|||||||
// Whether the app is running in embedded mode, and should keep the user
|
// Whether the app is running in embedded mode, and should keep the user
|
||||||
// confined to the current room
|
// confined to the current room
|
||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
|
// Whether the app should pause before joining the call until it sees an
|
||||||
|
// io.element.join widget action, allowing it to be preloaded
|
||||||
|
preload: boolean;
|
||||||
|
// Whether to hide the room header when in a call
|
||||||
|
hideHeader: boolean;
|
||||||
// Whether to start a walkie-talkie call instead of a video call
|
// Whether to start a walkie-talkie call instead of a video call
|
||||||
isPtt: boolean;
|
isPtt: boolean;
|
||||||
// Whether to use end-to-end encryption
|
// Whether to use end-to-end encryption
|
||||||
@@ -75,6 +80,8 @@ export const getRoomParams = (
|
|||||||
roomId: getParam("roomId"),
|
roomId: getParam("roomId"),
|
||||||
viaServers: getAllParams("via"),
|
viaServers: getAllParams("via"),
|
||||||
isEmbedded: hasParam("embed"),
|
isEmbedded: hasParam("embed"),
|
||||||
|
preload: hasParam("preload"),
|
||||||
|
hideHeader: hasParam("hideHeader"),
|
||||||
isPtt: hasParam("ptt"),
|
isPtt: hasParam("ptt"),
|
||||||
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
||||||
userId: getParam("userId"),
|
userId: getParam("userId"),
|
||||||
|
|||||||
@@ -660,6 +660,8 @@ function getSubGridPositions(
|
|||||||
return newTilePositions;
|
return newTilePositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets the 'order' property on tiles based on the layout param and
|
||||||
|
// other properties of the tiles, eg. 'focused' and 'presenter'
|
||||||
function reorderTiles(tiles: Tile[], layout: Layout) {
|
function reorderTiles(tiles: Tile[], layout: Layout) {
|
||||||
if (layout === "freedom" && tiles.length === 2) {
|
if (layout === "freedom" && tiles.length === 2) {
|
||||||
// 1:1 layout
|
// 1:1 layout
|
||||||
@@ -904,12 +906,12 @@ export function VideoGrid({
|
|||||||
return {
|
return {
|
||||||
x:
|
x:
|
||||||
tilePosition.x +
|
tilePosition.x +
|
||||||
(layout === "spotlight" && tileIndex !== 0 && isMobile
|
(layout === "spotlight" && tile.order !== 0 && isMobile
|
||||||
? scrollPosition
|
? scrollPosition
|
||||||
: 0),
|
: 0),
|
||||||
y:
|
y:
|
||||||
tilePosition.y +
|
tilePosition.y +
|
||||||
(layout === "spotlight" && tileIndex !== 0 && !isMobile
|
(layout === "spotlight" && tile.order !== 0 && !isMobile
|
||||||
? scrollPosition
|
? scrollPosition
|
||||||
: 0),
|
: 0),
|
||||||
width: tilePosition.width,
|
width: tilePosition.width,
|
||||||
|
|||||||
@@ -40,9 +40,11 @@
|
|||||||
box-shadow: inset 0 0 0 4px var(--accent) !important;
|
box-shadow: inset 0 0 0 4px var(--accent) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoTile.fullscreen {
|
.videoTile.maximised {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoTile.screenshare > video {
|
.videoTile.screenshare > video {
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ interface Props {
|
|||||||
mediaRef?: React.RefObject<MediaElement>;
|
mediaRef?: React.RefObject<MediaElement>;
|
||||||
onOptionsPress?: () => void;
|
onOptionsPress?: () => void;
|
||||||
localVolume?: number;
|
localVolume?: number;
|
||||||
isFullscreen?: boolean;
|
maximised?: boolean;
|
||||||
|
fullscreen?: boolean;
|
||||||
onFullscreen?: () => void;
|
onFullscreen?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
showOptions?: boolean;
|
showOptions?: boolean;
|
||||||
@@ -53,7 +54,8 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
mediaRef,
|
mediaRef,
|
||||||
onOptionsPress,
|
onOptionsPress,
|
||||||
localVolume,
|
localVolume,
|
||||||
isFullscreen,
|
maximised,
|
||||||
|
fullscreen,
|
||||||
onFullscreen,
|
onFullscreen,
|
||||||
className,
|
className,
|
||||||
showOptions,
|
showOptions,
|
||||||
@@ -64,6 +66,27 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
|
const toolbarButtons: JSX.Element[] = [];
|
||||||
|
if (!isLocal) {
|
||||||
|
toolbarButtons.push(
|
||||||
|
<AudioButton
|
||||||
|
className={styles.button}
|
||||||
|
volume={localVolume}
|
||||||
|
onPress={onOptionsPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (screenshare) {
|
||||||
|
toolbarButtons.push(
|
||||||
|
<FullscreenButton
|
||||||
|
className={styles.button}
|
||||||
|
fullscreen={fullscreen}
|
||||||
|
onPress={onFullscreen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<animated.div
|
<animated.div
|
||||||
className={classNames(styles.videoTile, className, {
|
className={classNames(styles.videoTile, className, {
|
||||||
@@ -71,28 +94,13 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
[styles.speaking]: speaking,
|
[styles.speaking]: speaking,
|
||||||
[styles.muted]: audioMuted,
|
[styles.muted]: audioMuted,
|
||||||
[styles.screenshare]: screenshare,
|
[styles.screenshare]: screenshare,
|
||||||
[styles.fullscreen]: isFullscreen,
|
[styles.maximised]: maximised,
|
||||||
})}
|
})}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{(!isLocal || screenshare) && (
|
{toolbarButtons.length > 0 && !maximised && (
|
||||||
<div className={classNames(styles.toolbar)}>
|
<div className={classNames(styles.toolbar)}>{toolbarButtons}</div>
|
||||||
{!isLocal && (
|
|
||||||
<AudioButton
|
|
||||||
className={styles.button}
|
|
||||||
volume={localVolume}
|
|
||||||
onPress={onOptionsPress}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{screenshare && (
|
|
||||||
<FullscreenButton
|
|
||||||
className={styles.button}
|
|
||||||
fullscreen={isFullscreen}
|
|
||||||
onPress={onFullscreen}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{videoMuted && (
|
{videoMuted && (
|
||||||
<>
|
<>
|
||||||
@@ -100,17 +108,18 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
{avatar}
|
{avatar}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{screenshare ? (
|
{!maximised &&
|
||||||
<div className={styles.presenterLabel}>
|
(screenshare ? (
|
||||||
<span>{`${name} is presenting`}</span>
|
<div className={styles.presenterLabel}>
|
||||||
</div>
|
<span>{`${name} is presenting`}</span>
|
||||||
) : (
|
</div>
|
||||||
<div className={classNames(styles.infoBubble, styles.memberName)}>
|
) : (
|
||||||
{audioMuted && !videoMuted && <MicMutedIcon />}
|
<div className={classNames(styles.infoBubble, styles.memberName)}>
|
||||||
{videoMuted && <VideoMutedIcon />}
|
{audioMuted && !videoMuted && <MicMutedIcon />}
|
||||||
<span title={name}>{name}</span>
|
{videoMuted && <VideoMutedIcon />}
|
||||||
</div>
|
<span title={name}>{name}</span>
|
||||||
)}
|
</div>
|
||||||
|
))}
|
||||||
<video ref={mediaRef} playsInline disablePictureInPicture />
|
<video ref={mediaRef} playsInline disablePictureInPicture />
|
||||||
</animated.div>
|
</animated.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ interface Props {
|
|||||||
audioContext: AudioContext;
|
audioContext: AudioContext;
|
||||||
audioDestination: AudioNode;
|
audioDestination: AudioNode;
|
||||||
disableSpeakingIndicator: boolean;
|
disableSpeakingIndicator: boolean;
|
||||||
isFullscreen: boolean;
|
maximised: boolean;
|
||||||
|
fullscreen: boolean;
|
||||||
onFullscreen: (item: Participant) => void;
|
onFullscreen: (item: Participant) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoTileContainer({
|
export function VideoTileContainer({
|
||||||
item,
|
item,
|
||||||
width,
|
width,
|
||||||
@@ -50,7 +52,8 @@ export function VideoTileContainer({
|
|||||||
audioContext,
|
audioContext,
|
||||||
audioDestination,
|
audioDestination,
|
||||||
disableSpeakingIndicator,
|
disableSpeakingIndicator,
|
||||||
isFullscreen,
|
maximised,
|
||||||
|
fullscreen,
|
||||||
onFullscreen,
|
onFullscreen,
|
||||||
...rest
|
...rest
|
||||||
}: Props) {
|
}: Props) {
|
||||||
@@ -101,11 +104,12 @@ export function VideoTileContainer({
|
|||||||
avatar={getAvatar && getAvatar(member, width, height)}
|
avatar={getAvatar && getAvatar(member, width, height)}
|
||||||
onOptionsPress={onOptionsPress}
|
onOptionsPress={onOptionsPress}
|
||||||
localVolume={localVolume}
|
localVolume={localVolume}
|
||||||
isFullscreen={isFullscreen}
|
maximised={maximised}
|
||||||
|
fullscreen={fullscreen}
|
||||||
onFullscreen={onFullscreenCallback}
|
onFullscreen={onFullscreenCallback}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{videoTileSettingsModalState.isOpen && (
|
{videoTileSettingsModalState.isOpen && !maximised && (
|
||||||
<VideoTileSettingsModal
|
<VideoTileSettingsModal
|
||||||
{...videoTileSettingsModalProps}
|
{...videoTileSettingsModalProps}
|
||||||
feed={item.callFeed}
|
feed={item.callFeed}
|
||||||
|
|||||||
139
src/widget.ts
Normal file
139
src/widget.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { createRoomWidgetClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { WidgetApi, MatrixCapabilities } from "matrix-widget-api";
|
||||||
|
|
||||||
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
|
import { LazyEventEmitter } from "./LazyEventEmitter";
|
||||||
|
import { getRoomParams } from "./room/useRoomParams";
|
||||||
|
|
||||||
|
// Subset of the actions in matrix-react-sdk
|
||||||
|
export enum ElementWidgetActions {
|
||||||
|
JoinCall = "io.element.join",
|
||||||
|
HangupCall = "im.vector.hangup",
|
||||||
|
TileLayout = "io.element.tile_layout",
|
||||||
|
SpotlightLayout = "io.element.spotlight_layout",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JoinCallData {
|
||||||
|
audioInput: string | null;
|
||||||
|
videoInput: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WidgetHelpers {
|
||||||
|
api: WidgetApi;
|
||||||
|
lazyActions: LazyEventEmitter;
|
||||||
|
client: Promise<MatrixClient>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A point of access to the widget API, if the app is running as a widget. This
|
||||||
|
* is declared and initialized on the top level because the widget messaging
|
||||||
|
* needs to be set up ASAP on load to ensure it doesn't miss any requests.
|
||||||
|
*/
|
||||||
|
export const widget: WidgetHelpers | null = (() => {
|
||||||
|
try {
|
||||||
|
const query = new URLSearchParams(window.location.search);
|
||||||
|
const widgetId = query.get("widgetId");
|
||||||
|
const parentUrl = query.get("parentUrl");
|
||||||
|
|
||||||
|
if (widgetId && parentUrl) {
|
||||||
|
const parentOrigin = new URL(parentUrl).origin;
|
||||||
|
logger.info("Widget API is available");
|
||||||
|
const api = new WidgetApi(widgetId, parentOrigin);
|
||||||
|
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
|
||||||
|
|
||||||
|
// Set up the lazy action emitter, but only for select actions that we
|
||||||
|
// intend for the app to handle
|
||||||
|
const lazyActions = new LazyEventEmitter();
|
||||||
|
[
|
||||||
|
ElementWidgetActions.JoinCall,
|
||||||
|
ElementWidgetActions.HangupCall,
|
||||||
|
ElementWidgetActions.TileLayout,
|
||||||
|
ElementWidgetActions.SpotlightLayout,
|
||||||
|
].forEach((action) => {
|
||||||
|
api.on(`action:${action}`, (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
lazyActions.emit(action, ev);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now, initialize the matryoshka MatrixClient (so named because it routes
|
||||||
|
// all requests through the host client via the widget API)
|
||||||
|
// We need to do this now rather than later because it has capabilities to
|
||||||
|
// request, and is responsible for starting the transport (should it be?)
|
||||||
|
|
||||||
|
const { roomId, userId, deviceId } = getRoomParams();
|
||||||
|
if (!roomId) throw new Error("Room ID must be supplied");
|
||||||
|
if (!userId) throw new Error("User ID must be supplied");
|
||||||
|
if (!deviceId) throw new Error("Device ID must be supplied");
|
||||||
|
|
||||||
|
// These are all the event types the app uses
|
||||||
|
const sendState = [
|
||||||
|
{ eventType: EventType.GroupCallPrefix },
|
||||||
|
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
|
||||||
|
];
|
||||||
|
const receiveState = [
|
||||||
|
{ eventType: EventType.RoomMember },
|
||||||
|
{ eventType: EventType.GroupCallPrefix },
|
||||||
|
{ eventType: EventType.GroupCallMemberPrefix },
|
||||||
|
];
|
||||||
|
const sendRecvToDevice = [
|
||||||
|
EventType.CallInvite,
|
||||||
|
EventType.CallCandidates,
|
||||||
|
EventType.CallAnswer,
|
||||||
|
EventType.CallHangup,
|
||||||
|
EventType.CallReject,
|
||||||
|
EventType.CallSelectAnswer,
|
||||||
|
EventType.CallNegotiate,
|
||||||
|
EventType.CallSDPStreamMetadataChanged,
|
||||||
|
EventType.CallSDPStreamMetadataChangedPrefix,
|
||||||
|
EventType.CallReplaces,
|
||||||
|
];
|
||||||
|
|
||||||
|
const client = createRoomWidgetClient(
|
||||||
|
api,
|
||||||
|
{
|
||||||
|
sendState,
|
||||||
|
receiveState,
|
||||||
|
sendToDevice: sendRecvToDevice,
|
||||||
|
receiveToDevice: sendRecvToDevice,
|
||||||
|
turnServers: true,
|
||||||
|
},
|
||||||
|
roomId,
|
||||||
|
{
|
||||||
|
baseUrl: "",
|
||||||
|
userId,
|
||||||
|
deviceId,
|
||||||
|
timelineSupport: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const clientPromise = client.startClient().then(() => client);
|
||||||
|
|
||||||
|
return { api, lazyActions, client: clientPromise };
|
||||||
|
} else {
|
||||||
|
logger.info("No widget API available");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("Continuing without the widget API", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -8,7 +8,14 @@
|
|||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"lib": ["es2020", "dom", "dom.iterable"]
|
"lib": ["es2020", "dom", "dom.iterable"],
|
||||||
|
"strict": false,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "typescript-strict-plugin",
|
||||||
|
"paths": ["src"]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
|
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
|
||||||
}
|
}
|
||||||
|
|||||||
156
yarn.lock
156
yarn.lock
@@ -3878,7 +3878,7 @@ base16@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70"
|
resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70"
|
||||||
integrity sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==
|
integrity sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==
|
||||||
|
|
||||||
base64-js@^1.0.2:
|
base64-js@^1.0.2, base64-js@^1.3.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||||
@@ -3937,6 +3937,15 @@ bindings@^1.5.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
file-uri-to-path "1.0.0"
|
file-uri-to-path "1.0.0"
|
||||||
|
|
||||||
|
bl@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
||||||
|
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
|
||||||
|
dependencies:
|
||||||
|
buffer "^5.5.0"
|
||||||
|
inherits "^2.0.4"
|
||||||
|
readable-stream "^3.4.0"
|
||||||
|
|
||||||
bluebird@^3.5.5:
|
bluebird@^3.5.5:
|
||||||
version "3.7.2"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
@@ -4134,6 +4143,14 @@ buffer@^4.3.0:
|
|||||||
ieee754 "^1.1.4"
|
ieee754 "^1.1.4"
|
||||||
isarray "^1.0.0"
|
isarray "^1.0.0"
|
||||||
|
|
||||||
|
buffer@^5.5.0:
|
||||||
|
version "5.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||||
|
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||||
|
dependencies:
|
||||||
|
base64-js "^1.3.1"
|
||||||
|
ieee754 "^1.1.13"
|
||||||
|
|
||||||
builtin-status-codes@^3.0.0:
|
builtin-status-codes@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
||||||
@@ -4310,6 +4327,14 @@ chalk@^2.0.0, chalk@^2.4.1:
|
|||||||
escape-string-regexp "^1.0.5"
|
escape-string-regexp "^1.0.5"
|
||||||
supports-color "^5.3.0"
|
supports-color "^5.3.0"
|
||||||
|
|
||||||
|
chalk@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
|
||||||
|
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.1.0"
|
||||||
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
chalk@^4.0.0, chalk@^4.1.0:
|
chalk@^4.0.0, chalk@^4.1.0:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||||
@@ -4422,6 +4447,18 @@ cli-boxes@^2.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
|
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
|
||||||
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
|
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
|
||||||
|
|
||||||
|
cli-cursor@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
|
||||||
|
integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
|
||||||
|
dependencies:
|
||||||
|
restore-cursor "^3.1.0"
|
||||||
|
|
||||||
|
cli-spinners@^2.5.0:
|
||||||
|
version "2.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a"
|
||||||
|
integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==
|
||||||
|
|
||||||
cli-table3@^0.6.1:
|
cli-table3@^0.6.1:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a"
|
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a"
|
||||||
@@ -4449,6 +4486,11 @@ clone-deep@^4.0.1:
|
|||||||
kind-of "^6.0.2"
|
kind-of "^6.0.2"
|
||||||
shallow-clone "^3.0.0"
|
shallow-clone "^3.0.0"
|
||||||
|
|
||||||
|
clone@^1.0.2:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||||
|
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
|
||||||
|
|
||||||
clsx@^1.1.1:
|
clsx@^1.1.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||||
@@ -5453,6 +5495,13 @@ default-browser-id@^1.0.4:
|
|||||||
meow "^3.1.0"
|
meow "^3.1.0"
|
||||||
untildify "^2.0.0"
|
untildify "^2.0.0"
|
||||||
|
|
||||||
|
defaults@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
|
||||||
|
integrity sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==
|
||||||
|
dependencies:
|
||||||
|
clone "^1.0.2"
|
||||||
|
|
||||||
define-lazy-prop@^2.0.0:
|
define-lazy-prop@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
|
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
|
||||||
@@ -6376,6 +6425,21 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
|
|||||||
md5.js "^1.3.4"
|
md5.js "^1.3.4"
|
||||||
safe-buffer "^5.1.1"
|
safe-buffer "^5.1.1"
|
||||||
|
|
||||||
|
execa@^4.0.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
|
||||||
|
integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
|
||||||
|
dependencies:
|
||||||
|
cross-spawn "^7.0.0"
|
||||||
|
get-stream "^5.0.0"
|
||||||
|
human-signals "^1.1.1"
|
||||||
|
is-stream "^2.0.0"
|
||||||
|
merge-stream "^2.0.0"
|
||||||
|
npm-run-path "^4.0.0"
|
||||||
|
onetime "^5.1.0"
|
||||||
|
signal-exit "^3.0.2"
|
||||||
|
strip-final-newline "^2.0.0"
|
||||||
|
|
||||||
execa@^5.1.1:
|
execa@^5.1.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
|
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
|
||||||
@@ -6955,6 +7019,13 @@ get-stdin@^4.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
||||||
integrity sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==
|
integrity sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==
|
||||||
|
|
||||||
|
get-stream@^5.0.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
|
||||||
|
integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
|
||||||
|
dependencies:
|
||||||
|
pump "^3.0.0"
|
||||||
|
|
||||||
get-stream@^6.0.0:
|
get-stream@^6.0.0:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
|
||||||
@@ -7430,6 +7501,11 @@ https-browserify@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||||
integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==
|
integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==
|
||||||
|
|
||||||
|
human-signals@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
||||||
|
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
|
||||||
|
|
||||||
human-signals@^2.1.0:
|
human-signals@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
|
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
|
||||||
@@ -7456,7 +7532,7 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.14"
|
postcss "^7.0.14"
|
||||||
|
|
||||||
ieee754@^1.1.4:
|
ieee754@^1.1.13, ieee754@^1.1.4:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||||
@@ -7774,6 +7850,11 @@ is-hexadecimal@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
|
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
|
||||||
integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
|
integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
|
||||||
|
|
||||||
|
is-interactive@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
|
||||||
|
integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
|
||||||
|
|
||||||
is-map@^2.0.2:
|
is-map@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
|
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
|
||||||
@@ -7864,6 +7945,11 @@ is-typedarray@~1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||||
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
|
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
|
||||||
|
|
||||||
|
is-unicode-supported@^0.1.0:
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
|
||||||
|
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
|
||||||
|
|
||||||
is-utf8@^0.2.0:
|
is-utf8@^0.2.0:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
||||||
@@ -8294,6 +8380,14 @@ lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
|
log-symbols@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
|
||||||
|
integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
|
||||||
|
dependencies:
|
||||||
|
chalk "^4.1.0"
|
||||||
|
is-unicode-supported "^0.1.0"
|
||||||
|
|
||||||
loglevel@^1.7.1:
|
loglevel@^1.7.1:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114"
|
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114"
|
||||||
@@ -8390,9 +8484,9 @@ matrix-events-sdk@^0.0.1-beta.7:
|
|||||||
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
|
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
|
||||||
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
|
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#965f4fb13b4b36b26a3f4d7214cc7630d9f579a5":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#f41b7706e489cc1b83e6b25dd50091be2b5a9083":
|
||||||
version "19.3.0"
|
version "19.5.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/965f4fb13b4b36b26a3f4d7214cc7630d9f579a5"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f41b7706e489cc1b83e6b25dd50091be2b5a9083"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@types/sdp-transform" "^2.4.5"
|
"@types/sdp-transform" "^2.4.5"
|
||||||
@@ -8410,9 +8504,9 @@ matrix-events-sdk@^0.0.1-beta.7:
|
|||||||
unhomoglyph "^1.0.6"
|
unhomoglyph "^1.0.6"
|
||||||
|
|
||||||
matrix-widget-api@^1.0.0:
|
matrix-widget-api@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.0.0.tgz#0cde6839cca66ad817ab12aca3490ccc8bac97d1"
|
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.1.1.tgz#d3fec45033d0cbc14387a38ba92dac4dbb1be962"
|
||||||
integrity sha512-cy8p/8EteRPTFIAw7Q9EgPUJc2jD19ZahMR8bMKf2NkILDcjuPMC0UWnsJyB3fSnlGw+VbGepttRpULM31zX8Q==
|
integrity sha512-gNSgmgSwvOsOcWK9k2+tOhEMYBiIMwX95vMZu0JqY7apkM02xrOzUBuPRProzN8CnbIALH7e3GAhatF6QCNvtA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/events" "^3.0.0"
|
"@types/events" "^3.0.0"
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
@@ -8898,7 +8992,7 @@ normalize.css@^8.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
|
resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
|
||||||
integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
|
integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
|
||||||
|
|
||||||
npm-run-path@^4.0.1:
|
npm-run-path@^4.0.0, npm-run-path@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
||||||
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
|
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
|
||||||
@@ -9049,7 +9143,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
onetime@^5.1.2:
|
onetime@^5.1.0, onetime@^5.1.2:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
|
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
|
||||||
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
|
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
|
||||||
@@ -9097,6 +9191,21 @@ optionator@^0.9.1:
|
|||||||
type-check "^0.4.0"
|
type-check "^0.4.0"
|
||||||
word-wrap "^1.2.3"
|
word-wrap "^1.2.3"
|
||||||
|
|
||||||
|
ora@^5.4.1:
|
||||||
|
version "5.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18"
|
||||||
|
integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==
|
||||||
|
dependencies:
|
||||||
|
bl "^4.1.0"
|
||||||
|
chalk "^4.1.0"
|
||||||
|
cli-cursor "^3.1.0"
|
||||||
|
cli-spinners "^2.5.0"
|
||||||
|
is-interactive "^1.0.0"
|
||||||
|
is-unicode-supported "^0.1.0"
|
||||||
|
log-symbols "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
wcwidth "^1.0.1"
|
||||||
|
|
||||||
os-browserify@^0.3.0:
|
os-browserify@^0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
||||||
@@ -10302,7 +10411,7 @@ read-pkg@^5.2.0:
|
|||||||
string_decoder "~1.1.1"
|
string_decoder "~1.1.1"
|
||||||
util-deprecate "~1.0.1"
|
util-deprecate "~1.0.1"
|
||||||
|
|
||||||
readable-stream@^3.6.0:
|
readable-stream@^3.4.0, readable-stream@^3.6.0:
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||||
@@ -10576,6 +10685,14 @@ resolve@^2.0.0-next.3:
|
|||||||
path-parse "^1.0.7"
|
path-parse "^1.0.7"
|
||||||
supports-preserve-symlinks-flag "^1.0.0"
|
supports-preserve-symlinks-flag "^1.0.0"
|
||||||
|
|
||||||
|
restore-cursor@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
|
||||||
|
integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
|
||||||
|
dependencies:
|
||||||
|
onetime "^5.1.0"
|
||||||
|
signal-exit "^3.0.2"
|
||||||
|
|
||||||
ret@~0.1.10:
|
ret@~0.1.10:
|
||||||
version "0.1.15"
|
version "0.1.15"
|
||||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||||
@@ -11634,6 +11751,16 @@ typedarray@^0.0.6:
|
|||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||||
|
|
||||||
|
typescript-strict-plugin@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript-strict-plugin/-/typescript-strict-plugin-2.0.1.tgz#4e676704818c4458a8b11125e9d32032e0513de4"
|
||||||
|
integrity sha512-8LHbwpkeQN12KZMK4BsmC6U1AyF+QisiLlaPH6GoCDV3xd52emyg6mOsL4I3C1Uy2n65HrnAdSkc8yi6bWb/6Q==
|
||||||
|
dependencies:
|
||||||
|
chalk "^3.0.0"
|
||||||
|
execa "^4.0.0"
|
||||||
|
ora "^5.4.1"
|
||||||
|
yargs "^16.2.0"
|
||||||
|
|
||||||
typescript@^4.6.4:
|
typescript@^4.6.4:
|
||||||
version "4.7.4"
|
version "4.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
||||||
@@ -12073,6 +12200,13 @@ watchpack@^2.2.0, watchpack@^2.3.1:
|
|||||||
glob-to-regexp "^0.4.1"
|
glob-to-regexp "^0.4.1"
|
||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.1.2"
|
||||||
|
|
||||||
|
wcwidth@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
|
||||||
|
integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==
|
||||||
|
dependencies:
|
||||||
|
defaults "^1.0.3"
|
||||||
|
|
||||||
web-namespaces@^1.0.0:
|
web-namespaces@^1.0.0:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
|
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
|
||||||
|
|||||||
Reference in New Issue
Block a user