Initialize all widget-related things at the top level
This commit is contained in:
parent
3186b5f24b
commit
f0045c9406
4 changed files with 237 additions and 83 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
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
|
||||||
|
|
|
||||||
140
src/widget.ts
Normal file
140
src/widget.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
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,
|
||||||
|
"org.matrix.call_duplicate_session",
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
})();
|
||||||
Loading…
Add table
Add a link
Reference in a new issue