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 {
 | 
			
		||||
  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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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