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",
|
||||
"color-hash": "^2.0.1",
|
||||
"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",
|
||||
"mermaid": "^8.13.8",
|
||||
"normalize.css": "^8.0.1",
|
||||
@@ -75,6 +75,7 @@
|
||||
"sass": "^1.42.1",
|
||||
"storybook-builder-vite": "^0.1.12",
|
||||
"typescript": "^4.6.4",
|
||||
"typescript-strict-plugin": "^2.0.1",
|
||||
"vite": "^2.4.2",
|
||||
"vite-plugin-html-template": "^1.1.0",
|
||||
"vite-plugin-svgr": "^0.4.0"
|
||||
|
||||
@@ -31,10 +31,10 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ErrorView } from "./FullScreenView";
|
||||
import {
|
||||
initClient,
|
||||
initMatroskaClient,
|
||||
defaultHomeserver,
|
||||
CryptoStoreIntegrityError,
|
||||
} from "./matrix-utils";
|
||||
import { widget } from "./widget";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -100,16 +100,12 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
const init = async (): Promise<
|
||||
Pick<ClientProviderState, "client" | "isPasswordlessUser">
|
||||
> => {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const widgetId = query.get("widgetId");
|
||||
const parentUrl = query.get("parentUrl");
|
||||
|
||||
if (widgetId && parentUrl) {
|
||||
// We're inside a widget, so let's engage *Matroska mode*
|
||||
logger.log("Using a Matroska client");
|
||||
if (widget) {
|
||||
// We're inside a widget, so let's engage *matryoshka mode*
|
||||
logger.log("Using a matryoshka client");
|
||||
|
||||
return {
|
||||
client: await initMatroskaClient(widgetId, parentUrl),
|
||||
client: await widget.client,
|
||||
isPasswordlessUser: false,
|
||||
};
|
||||
} else {
|
||||
@@ -256,13 +252,27 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
[client]
|
||||
);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
const logout = useCallback(async () => {
|
||||
await client.logout(undefined, true);
|
||||
await client.clearStores();
|
||||
clearSession();
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: true,
|
||||
userName: "",
|
||||
error: undefined,
|
||||
});
|
||||
history.push("/");
|
||||
}, [history]);
|
||||
}, [history, client]);
|
||||
|
||||
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 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 { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
|
||||
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
|
||||
import {
|
||||
createClient,
|
||||
createRoomWidgetClient,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
||||
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 { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { WidgetApi } from "matrix-widget-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
} 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 IndexedDBWorker from "./IndexedDBWorker?worker";
|
||||
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.
|
||||
* 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 { 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 { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
import { LobbyView } from "./LobbyView";
|
||||
@@ -28,22 +30,30 @@ import { CallEndedView } from "./CallEndedView";
|
||||
import { useRoomAvatar } from "./useRoomAvatar";
|
||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||
import { useLocationNavigation } from "../useLocationNavigation";
|
||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
groupCall: GroupCall;
|
||||
groupCall?: GroupCall;
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
isPasswordlessUser: boolean;
|
||||
isEmbedded: boolean;
|
||||
preload: boolean;
|
||||
hideHeader: boolean;
|
||||
roomIdOrAlias: string;
|
||||
groupCall: GroupCall;
|
||||
}
|
||||
|
||||
export function GroupCallView({
|
||||
client,
|
||||
isPasswordlessUser,
|
||||
isEmbedded,
|
||||
preload,
|
||||
hideHeader,
|
||||
roomIdOrAlias,
|
||||
groupCall,
|
||||
}: Props) {
|
||||
@@ -69,14 +79,50 @@ export function GroupCallView({
|
||||
unencryptedEventsFromUsers,
|
||||
} = useGroupCall(groupCall);
|
||||
|
||||
const { setAudioInput, setVideoInput } = useMediaHandler();
|
||||
|
||||
const avatarUrl = useRoomAvatar(groupCall.room);
|
||||
|
||||
useEffect(() => {
|
||||
window.groupCall = groupCall;
|
||||
return () => {
|
||||
delete window.groupCall;
|
||||
};
|
||||
}, [groupCall]);
|
||||
|
||||
// In embedded mode, bypass the lobby and just enter the call straight away
|
||||
if (isEmbedded) groupCall.enter();
|
||||
}, [groupCall, isEmbedded]);
|
||||
useEffect(() => {
|
||||
if (widget && preload) {
|
||||
// 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);
|
||||
|
||||
@@ -88,11 +134,29 @@ export function GroupCallView({
|
||||
const onLeave = useCallback(() => {
|
||||
setLeft(true);
|
||||
leave();
|
||||
if (widget) {
|
||||
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
widget.api.setAlwaysOnScreen(false);
|
||||
}
|
||||
|
||||
if (!isPasswordlessUser) {
|
||||
if (!isPasswordlessUser && !isEmbedded) {
|
||||
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) {
|
||||
return <ErrorView error={error} />;
|
||||
@@ -109,6 +173,7 @@ export function GroupCallView({
|
||||
userMediaFeeds={userMediaFeeds}
|
||||
onLeave={onLeave}
|
||||
isEmbedded={isEmbedded}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -131,6 +196,7 @@ export function GroupCallView({
|
||||
screenshareFeeds={screenshareFeeds}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -141,33 +207,41 @@ export function GroupCallView({
|
||||
</FullScreenView>
|
||||
);
|
||||
} else if (left) {
|
||||
return <CallEndedView client={client} />;
|
||||
} else {
|
||||
if (isEmbedded) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Loading room...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
if (isPasswordlessUser) {
|
||||
return <CallEndedView client={client} />;
|
||||
} 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}
|
||||
/>
|
||||
);
|
||||
// If the user is a regular user, we'll have sent them back to the homepage,
|
||||
// so just sit here & do nothing: otherwise we would (briefly) mount the
|
||||
// LobbyView again which would open capture devices again.
|
||||
return null;
|
||||
}
|
||||
} 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;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
height: calc(50px + 2 * 8px);
|
||||
}
|
||||
|
||||
.footer > * {
|
||||
@@ -54,7 +54,7 @@ limitations under the License.
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.footerFullscreen {
|
||||
.maximised .footer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
@@ -67,8 +67,14 @@ limitations under the License.
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
@media (min-height: 300px) {
|
||||
.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.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useRef } from "react";
|
||||
import React, { useEffect, useCallback, useMemo, useRef } from "react";
|
||||
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 { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||
import classNames from "classnames";
|
||||
|
||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||
import styles from "./InCallView.module.css";
|
||||
import {
|
||||
HangupButton,
|
||||
@@ -52,6 +55,7 @@ import { useAudioContext } from "../video-grid/useMediaStream";
|
||||
import { useFullscreen } from "../video-grid/useFullscreen";
|
||||
import { AudioContainer } from "../video-grid/AudioContainer";
|
||||
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
|
||||
import { widget, ElementWidgetActions } from "../widget";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
@@ -77,6 +81,7 @@ interface Props {
|
||||
localScreenshareFeed: CallFeed;
|
||||
roomIdOrAlias: string;
|
||||
unencryptedEventsFromUsers: Set<string>;
|
||||
hideHeader: boolean;
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
@@ -105,11 +110,23 @@ export function InCallView({
|
||||
localScreenshareFeed,
|
||||
roomIdOrAlias,
|
||||
unencryptedEventsFromUsers,
|
||||
hideHeader,
|
||||
}: Props) {
|
||||
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 { toggleFullscreen, fullscreenParticipant } = useFullscreen(elementRef);
|
||||
const { toggleFullscreen, fullscreenParticipant } =
|
||||
useFullscreen(containerRef1);
|
||||
|
||||
const [spatialAudio] = useSpatialAudio();
|
||||
|
||||
@@ -122,6 +139,42 @@ export function InCallView({
|
||||
|
||||
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 participants: Participant[] = [];
|
||||
|
||||
@@ -130,9 +183,7 @@ export function InCallView({
|
||||
id: callFeed.stream.id,
|
||||
callFeed,
|
||||
focused:
|
||||
screenshareFeeds.length === 0 && layout === "spotlight"
|
||||
? callFeed.userId === activeSpeaker
|
||||
: false,
|
||||
screenshareFeeds.length === 0 && callFeed.userId === activeSpeaker,
|
||||
isLocal: callFeed.isLocal(),
|
||||
presenter: false,
|
||||
});
|
||||
@@ -157,7 +208,20 @@ export function InCallView({
|
||||
}
|
||||
|
||||
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(
|
||||
(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) {
|
||||
return (
|
||||
<div className={styles.centerMessage}>
|
||||
@@ -185,16 +249,19 @@ export function InCallView({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (fullscreenParticipant) {
|
||||
if (maximisedParticipant) {
|
||||
return (
|
||||
<VideoTileContainer
|
||||
key={fullscreenParticipant.id}
|
||||
item={fullscreenParticipant}
|
||||
height={bounds.height}
|
||||
width={bounds.width}
|
||||
key={maximisedParticipant.id}
|
||||
item={maximisedParticipant}
|
||||
getAvatar={renderAvatar}
|
||||
audioContext={audioContext}
|
||||
audioDestination={audioDestination}
|
||||
disableSpeakingIndicator={true}
|
||||
isFullscreen={!!fullscreenParticipant}
|
||||
maximised={Boolean(maximisedParticipant)}
|
||||
fullscreen={maximisedParticipant === fullscreenParticipant}
|
||||
onFullscreen={toggleFullscreen}
|
||||
/>
|
||||
);
|
||||
@@ -210,43 +277,36 @@ export function InCallView({
|
||||
audioContext={audioContext}
|
||||
audioDestination={audioDestination}
|
||||
disableSpeakingIndicator={items.length < 3}
|
||||
isFullscreen={!!fullscreenParticipant}
|
||||
maximised={false}
|
||||
fullscreen={false}
|
||||
onFullscreen={toggleFullscreen}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
</VideoGrid>
|
||||
);
|
||||
}, [
|
||||
fullscreenParticipant,
|
||||
items,
|
||||
audioContext,
|
||||
audioDestination,
|
||||
layout,
|
||||
renderAvatar,
|
||||
toggleFullscreen,
|
||||
]);
|
||||
};
|
||||
|
||||
const {
|
||||
modalState: rageshakeRequestModalState,
|
||||
modalProps: rageshakeRequestModalProps,
|
||||
} = useRageshakeRequestModal(groupCall.room.roomId);
|
||||
|
||||
const footerClassNames = classNames(styles.footer, {
|
||||
[styles.footerFullscreen]: fullscreenParticipant,
|
||||
const containerClasses = classNames(styles.inRoom, {
|
||||
[styles.maximised]: maximisedParticipant,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.inRoom} ref={elementRef}>
|
||||
<div className={containerClasses} ref={containerRef}>
|
||||
<audio ref={audioRef} />
|
||||
{(!spatialAudio || fullscreenParticipant) && (
|
||||
{(!spatialAudio || maximisedParticipant) && (
|
||||
<AudioContainer
|
||||
items={items}
|
||||
audioContext={audioContext}
|
||||
audioDestination={audioDestination}
|
||||
/>
|
||||
)}
|
||||
{!fullscreenParticipant && (
|
||||
{!hideHeader && !maximisedParticipant && (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||
@@ -262,16 +322,16 @@ export function InCallView({
|
||||
</Header>
|
||||
)}
|
||||
{renderContent()}
|
||||
<div className={footerClassNames}>
|
||||
<div className={styles.footer}>
|
||||
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
|
||||
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
|
||||
{canScreenshare && !isSafari && !fullscreenParticipant && (
|
||||
{canScreenshare && !isSafari && !maximisedParticipant && (
|
||||
<ScreenshareButton
|
||||
enabled={isScreensharing}
|
||||
onPress={toggleScreensharing}
|
||||
/>
|
||||
)}
|
||||
{!fullscreenParticipant && (
|
||||
{!maximisedParticipant && (
|
||||
<OverflowMenu
|
||||
inCall
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
|
||||
@@ -47,6 +47,7 @@ interface Props {
|
||||
localVideoMuted: boolean;
|
||||
roomIdOrAlias: string;
|
||||
isEmbedded: boolean;
|
||||
hideHeader: boolean;
|
||||
}
|
||||
export function LobbyView({
|
||||
client,
|
||||
@@ -63,6 +64,7 @@ export function LobbyView({
|
||||
toggleMicrophoneMuted,
|
||||
roomIdOrAlias,
|
||||
isEmbedded,
|
||||
hideHeader,
|
||||
}: Props) {
|
||||
const { stream } = useCallFeed(localCallFeed);
|
||||
const {
|
||||
@@ -90,14 +92,16 @@ export function LobbyView({
|
||||
|
||||
return (
|
||||
<div className={styles.room}>
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
{!hideHeader && (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
)}
|
||||
<div className={styles.joinRoom}>
|
||||
<div className={styles.joinRoomContent}>
|
||||
{groupCall.isPtt ? (
|
||||
|
||||
@@ -97,6 +97,7 @@ interface Props {
|
||||
userMediaFeeds: CallFeed[];
|
||||
onLeave: () => void;
|
||||
isEmbedded: boolean;
|
||||
hideHeader: boolean;
|
||||
}
|
||||
|
||||
export const PTTCallView: React.FC<Props> = ({
|
||||
@@ -109,6 +110,7 @@ export const PTTCallView: React.FC<Props> = ({
|
||||
userMediaFeeds,
|
||||
onLeave,
|
||||
isEmbedded,
|
||||
hideHeader,
|
||||
}) => {
|
||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||
useModalTriggerState();
|
||||
@@ -176,7 +178,7 @@ export const PTTCallView: React.FC<Props> = ({
|
||||
// https://github.com/vector-im/element-call/issues/328
|
||||
show={false}
|
||||
/>
|
||||
{showControls && (
|
||||
{!hideHeader && showControls && (
|
||||
<Header className={styles.header}>
|
||||
<LeftNav>
|
||||
<RoomSetupHeaderInfo
|
||||
|
||||
@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
||||
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 { ErrorView, LoadingView } from "../FullScreenView";
|
||||
import { RoomAuthView } from "./RoomAuthView";
|
||||
@@ -29,8 +30,16 @@ export const RoomPage: FC = () => {
|
||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||
useClient();
|
||||
|
||||
const { roomAlias, roomId, viaServers, isEmbedded, isPtt, displayName } =
|
||||
useRoomParams();
|
||||
const {
|
||||
roomAlias,
|
||||
roomId,
|
||||
viaServers,
|
||||
isEmbedded,
|
||||
preload,
|
||||
hideHeader,
|
||||
isPtt,
|
||||
displayName,
|
||||
} = useRoomParams();
|
||||
const roomIdOrAlias = roomId ?? roomAlias;
|
||||
if (!roomIdOrAlias) throw new Error("No room specified");
|
||||
|
||||
@@ -53,6 +62,21 @@ export const RoomPage: FC = () => {
|
||||
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) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
@@ -73,15 +97,7 @@ export const RoomPage: FC = () => {
|
||||
viaServers={viaServers}
|
||||
createPtt={isPtt}
|
||||
>
|
||||
{(groupCall) => (
|
||||
<GroupCallView
|
||||
client={client}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
groupCall={groupCall}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
isEmbedded={isEmbedded}
|
||||
/>
|
||||
)}
|
||||
{groupCallView}
|
||||
</GroupCallLoader>
|
||||
</MediaHandlerProvider>
|
||||
);
|
||||
|
||||
@@ -21,9 +21,10 @@ import {
|
||||
GroupCallIntent,
|
||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { 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 { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
||||
@@ -45,38 +46,15 @@ export const useLoadGroupCall = (
|
||||
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);
|
||||
logger.info(
|
||||
`Joined ${roomIdOrAlias}, waiting room to be ready for group calls`
|
||||
);
|
||||
await client.waitUntilRoomReadyForGroupCalls(room.roomId);
|
||||
logger.info(`${roomIdOrAlias}, is ready for group calls`);
|
||||
return room;
|
||||
} catch (error) {
|
||||
if (
|
||||
isLocalRoomId(roomIdOrAlias) &&
|
||||
@@ -91,7 +69,8 @@ export const useLoadGroupCall = (
|
||||
createPtt
|
||||
);
|
||||
// likewise, wait for the room
|
||||
return await waitForRoom(roomId);
|
||||
await client.waitUntilRoomReadyForGroupCalls(roomId);
|
||||
return client.getRoom(roomId);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -100,7 +79,9 @@ export const useLoadGroupCall = (
|
||||
|
||||
const fetchOrCreateGroupCall = async (): Promise<GroupCall> => {
|
||||
const room = await fetchOrCreateRoom();
|
||||
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
|
||||
const groupCall = client.getGroupCallForRoom(room.roomId);
|
||||
logger.debug("Got group call", groupCall?.groupCallId);
|
||||
|
||||
if (groupCall) return groupCall;
|
||||
|
||||
@@ -111,7 +92,11 @@ export const useLoadGroupCall = (
|
||||
)
|
||||
) {
|
||||
// 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(
|
||||
room.roomId,
|
||||
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) =>
|
||||
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
|
||||
// confined to the current room
|
||||
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
|
||||
isPtt: boolean;
|
||||
// Whether to use end-to-end encryption
|
||||
@@ -75,6 +80,8 @@ export const getRoomParams = (
|
||||
roomId: getParam("roomId"),
|
||||
viaServers: getAllParams("via"),
|
||||
isEmbedded: hasParam("embed"),
|
||||
preload: hasParam("preload"),
|
||||
hideHeader: hasParam("hideHeader"),
|
||||
isPtt: hasParam("ptt"),
|
||||
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
||||
userId: getParam("userId"),
|
||||
|
||||
@@ -660,6 +660,8 @@ function getSubGridPositions(
|
||||
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) {
|
||||
if (layout === "freedom" && tiles.length === 2) {
|
||||
// 1:1 layout
|
||||
@@ -904,12 +906,12 @@ export function VideoGrid({
|
||||
return {
|
||||
x:
|
||||
tilePosition.x +
|
||||
(layout === "spotlight" && tileIndex !== 0 && isMobile
|
||||
(layout === "spotlight" && tile.order !== 0 && isMobile
|
||||
? scrollPosition
|
||||
: 0),
|
||||
y:
|
||||
tilePosition.y +
|
||||
(layout === "spotlight" && tileIndex !== 0 && !isMobile
|
||||
(layout === "spotlight" && tile.order !== 0 && !isMobile
|
||||
? scrollPosition
|
||||
: 0),
|
||||
width: tilePosition.width,
|
||||
|
||||
@@ -40,9 +40,11 @@
|
||||
box-shadow: inset 0 0 0 4px var(--accent) !important;
|
||||
}
|
||||
|
||||
.videoTile.fullscreen {
|
||||
.videoTile.maximised {
|
||||
position: relative;
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.videoTile.screenshare > video {
|
||||
|
||||
@@ -33,7 +33,8 @@ interface Props {
|
||||
mediaRef?: React.RefObject<MediaElement>;
|
||||
onOptionsPress?: () => void;
|
||||
localVolume?: number;
|
||||
isFullscreen?: boolean;
|
||||
maximised?: boolean;
|
||||
fullscreen?: boolean;
|
||||
onFullscreen?: () => void;
|
||||
className?: string;
|
||||
showOptions?: boolean;
|
||||
@@ -53,7 +54,8 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
mediaRef,
|
||||
onOptionsPress,
|
||||
localVolume,
|
||||
isFullscreen,
|
||||
maximised,
|
||||
fullscreen,
|
||||
onFullscreen,
|
||||
className,
|
||||
showOptions,
|
||||
@@ -64,6 +66,27 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
},
|
||||
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 (
|
||||
<animated.div
|
||||
className={classNames(styles.videoTile, className, {
|
||||
@@ -71,28 +94,13 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
[styles.speaking]: speaking,
|
||||
[styles.muted]: audioMuted,
|
||||
[styles.screenshare]: screenshare,
|
||||
[styles.fullscreen]: isFullscreen,
|
||||
[styles.maximised]: maximised,
|
||||
})}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
>
|
||||
{(!isLocal || screenshare) && (
|
||||
<div className={classNames(styles.toolbar)}>
|
||||
{!isLocal && (
|
||||
<AudioButton
|
||||
className={styles.button}
|
||||
volume={localVolume}
|
||||
onPress={onOptionsPress}
|
||||
/>
|
||||
)}
|
||||
{screenshare && (
|
||||
<FullscreenButton
|
||||
className={styles.button}
|
||||
fullscreen={isFullscreen}
|
||||
onPress={onFullscreen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{toolbarButtons.length > 0 && !maximised && (
|
||||
<div className={classNames(styles.toolbar)}>{toolbarButtons}</div>
|
||||
)}
|
||||
{videoMuted && (
|
||||
<>
|
||||
@@ -100,17 +108,18 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
{avatar}
|
||||
</>
|
||||
)}
|
||||
{screenshare ? (
|
||||
<div className={styles.presenterLabel}>
|
||||
<span>{`${name} is presenting`}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={classNames(styles.infoBubble, styles.memberName)}>
|
||||
{audioMuted && !videoMuted && <MicMutedIcon />}
|
||||
{videoMuted && <VideoMutedIcon />}
|
||||
<span title={name}>{name}</span>
|
||||
</div>
|
||||
)}
|
||||
{!maximised &&
|
||||
(screenshare ? (
|
||||
<div className={styles.presenterLabel}>
|
||||
<span>{`${name} is presenting`}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={classNames(styles.infoBubble, styles.memberName)}>
|
||||
{audioMuted && !videoMuted && <MicMutedIcon />}
|
||||
{videoMuted && <VideoMutedIcon />}
|
||||
<span title={name}>{name}</span>
|
||||
</div>
|
||||
))}
|
||||
<video ref={mediaRef} playsInline disablePictureInPicture />
|
||||
</animated.div>
|
||||
);
|
||||
|
||||
@@ -39,9 +39,11 @@ interface Props {
|
||||
audioContext: AudioContext;
|
||||
audioDestination: AudioNode;
|
||||
disableSpeakingIndicator: boolean;
|
||||
isFullscreen: boolean;
|
||||
maximised: boolean;
|
||||
fullscreen: boolean;
|
||||
onFullscreen: (item: Participant) => void;
|
||||
}
|
||||
|
||||
export function VideoTileContainer({
|
||||
item,
|
||||
width,
|
||||
@@ -50,7 +52,8 @@ export function VideoTileContainer({
|
||||
audioContext,
|
||||
audioDestination,
|
||||
disableSpeakingIndicator,
|
||||
isFullscreen,
|
||||
maximised,
|
||||
fullscreen,
|
||||
onFullscreen,
|
||||
...rest
|
||||
}: Props) {
|
||||
@@ -101,11 +104,12 @@ export function VideoTileContainer({
|
||||
avatar={getAvatar && getAvatar(member, width, height)}
|
||||
onOptionsPress={onOptionsPress}
|
||||
localVolume={localVolume}
|
||||
isFullscreen={isFullscreen}
|
||||
maximised={maximised}
|
||||
fullscreen={fullscreen}
|
||||
onFullscreen={onFullscreenCallback}
|
||||
{...rest}
|
||||
/>
|
||||
{videoTileSettingsModalState.isOpen && (
|
||||
{videoTileSettingsModalState.isOpen && !maximised && (
|
||||
<VideoTileSettingsModal
|
||||
{...videoTileSettingsModalProps}
|
||||
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,
|
||||
"noUnusedLocals": true,
|
||||
"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"]
|
||||
}
|
||||
|
||||
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"
|
||||
integrity sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==
|
||||
|
||||
base64-js@^1.0.2:
|
||||
base64-js@^1.0.2, base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
@@ -3937,6 +3937,15 @@ bindings@^1.5.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
@@ -4134,6 +4143,14 @@ buffer@^4.3.0:
|
||||
ieee754 "^1.1.4"
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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:
|
||||
version "4.1.2"
|
||||
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"
|
||||
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:
|
||||
version "0.6.2"
|
||||
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"
|
||||
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:
|
||||
version "1.2.1"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
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:
|
||||
version "5.1.1"
|
||||
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"
|
||||
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:
|
||||
version "6.0.1"
|
||||
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"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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:
|
||||
postcss "^7.0.14"
|
||||
|
||||
ieee754@^1.1.4:
|
||||
ieee754@^1.1.13, ieee754@^1.1.4:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
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"
|
||||
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:
|
||||
version "2.0.2"
|
||||
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"
|
||||
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:
|
||||
version "0.2.1"
|
||||
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"
|
||||
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:
|
||||
version "1.8.0"
|
||||
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"
|
||||
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#965f4fb13b4b36b26a3f4d7214cc7630d9f579a5":
|
||||
version "19.3.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/965f4fb13b4b36b26a3f4d7214cc7630d9f579a5"
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#f41b7706e489cc1b83e6b25dd50091be2b5a9083":
|
||||
version "19.5.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f41b7706e489cc1b83e6b25dd50091be2b5a9083"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/sdp-transform" "^2.4.5"
|
||||
@@ -8410,9 +8504,9 @@ matrix-events-sdk@^0.0.1-beta.7:
|
||||
unhomoglyph "^1.0.6"
|
||||
|
||||
matrix-widget-api@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.0.0.tgz#0cde6839cca66ad817ab12aca3490ccc8bac97d1"
|
||||
integrity sha512-cy8p/8EteRPTFIAw7Q9EgPUJc2jD19ZahMR8bMKf2NkILDcjuPMC0UWnsJyB3fSnlGw+VbGepttRpULM31zX8Q==
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.1.1.tgz#d3fec45033d0cbc14387a38ba92dac4dbb1be962"
|
||||
integrity sha512-gNSgmgSwvOsOcWK9k2+tOhEMYBiIMwX95vMZu0JqY7apkM02xrOzUBuPRProzN8CnbIALH7e3GAhatF6QCNvtA==
|
||||
dependencies:
|
||||
"@types/events" "^3.0.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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
||||
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
|
||||
@@ -9049,7 +9143,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
onetime@^5.1.2:
|
||||
onetime@^5.1.0, onetime@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
|
||||
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
|
||||
@@ -9097,6 +9191,21 @@ optionator@^0.9.1:
|
||||
type-check "^0.4.0"
|
||||
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:
|
||||
version "0.3.0"
|
||||
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"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-stream@^3.6.0:
|
||||
readable-stream@^3.4.0, readable-stream@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||
@@ -10576,6 +10685,14 @@ resolve@^2.0.0-next.3:
|
||||
path-parse "^1.0.7"
|
||||
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:
|
||||
version "0.1.15"
|
||||
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"
|
||||
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:
|
||||
version "4.7.4"
|
||||
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"
|
||||
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:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
|
||||
|
||||
Reference in New Issue
Block a user