Prepare for Element Call integration (#9224)
* Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
This commit is contained in:
@@ -44,6 +44,10 @@ export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<
|
||||
})(dispatcher);
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
await this.readyStore.start();
|
||||
}
|
||||
|
||||
get matrixClient(): MatrixClient {
|
||||
return this.readyStore.mxClient;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,11 @@ interface IState {
|
||||
* reported.
|
||||
*/
|
||||
export default class AutoRageshakeStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new AutoRageshakeStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new AutoRageshakeStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {
|
||||
|
||||
@@ -37,7 +37,11 @@ interface IState {
|
||||
}
|
||||
|
||||
export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new BreadcrumbsStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new BreadcrumbsStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private waitingRooms: { roomId: string, addedTs: number }[] = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
Copyright 2022 The 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 { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
|
||||
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { UPDATE_EVENT } from "./AsyncStore";
|
||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
import WidgetStore from "./WidgetStore";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import { Call, CallEvent, ConnectionState } from "../models/Call";
|
||||
|
||||
export enum CallStoreEvent {
|
||||
// Signals a change in the call associated with a given room
|
||||
Call = "call",
|
||||
// Signals a change in the active calls
|
||||
ActiveCalls = "active_calls",
|
||||
}
|
||||
|
||||
export class CallStore extends AsyncStoreWithClient<{}> {
|
||||
private static _instance: CallStore;
|
||||
public static get instance(): CallStore {
|
||||
if (!this._instance) {
|
||||
this._instance = new CallStore();
|
||||
this._instance.start();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher);
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<void> {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
protected async onReady(): Promise<any> {
|
||||
// We assume that the calls present in a room are a function of room
|
||||
// state and room widgets, so we initialize the room map here and then
|
||||
// update it whenever those change
|
||||
for (const room of this.matrixClient.getRooms()) {
|
||||
this.updateRoom(room);
|
||||
}
|
||||
this.matrixClient.on(ClientEvent.Room, this.onRoom);
|
||||
this.matrixClient.on(RoomStateEvent.Events, this.onRoomState);
|
||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgets);
|
||||
|
||||
// If the room ID of a previously connected call is still in settings at
|
||||
// this time, that's a sign that we failed to disconnect from it
|
||||
// properly, and need to clean up after ourselves
|
||||
const uncleanlyDisconnectedRoomIds = SettingsStore.getValue<string[]>("activeCallRoomIds");
|
||||
if (uncleanlyDisconnectedRoomIds.length) {
|
||||
await Promise.all([
|
||||
...uncleanlyDisconnectedRoomIds.map(async uncleanlyDisconnectedRoomId => {
|
||||
logger.log(`Cleaning up call state for room ${uncleanlyDisconnectedRoomId}`);
|
||||
await this.get(uncleanlyDisconnectedRoomId)?.clean();
|
||||
}),
|
||||
SettingsStore.setValue("activeCallRoomIds", null, SettingLevel.DEVICE, []),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onNotReady(): Promise<any> {
|
||||
for (const [call, listenerMap] of this.callListeners) {
|
||||
// It's important that we remove the listeners before destroying the
|
||||
// call, because otherwise the call's onDestroy callback would fire
|
||||
// and immediately repopulate the map
|
||||
for (const [event, listener] of listenerMap) call.off(event, listener);
|
||||
call.destroy();
|
||||
}
|
||||
this.callListeners.clear();
|
||||
this.calls.clear();
|
||||
this.activeCalls = new Set();
|
||||
|
||||
this.matrixClient.off(ClientEvent.Room, this.onRoom);
|
||||
this.matrixClient.off(RoomStateEvent.Events, this.onRoomState);
|
||||
WidgetStore.instance.off(UPDATE_EVENT, this.onWidgets);
|
||||
}
|
||||
|
||||
private _activeCalls: Set<Call> = new Set();
|
||||
/**
|
||||
* The calls to which the user is currently connected.
|
||||
*/
|
||||
public get activeCalls(): Set<Call> {
|
||||
return this._activeCalls;
|
||||
}
|
||||
private set activeCalls(value: Set<Call>) {
|
||||
this._activeCalls = value;
|
||||
this.emit(CallStoreEvent.ActiveCalls, value);
|
||||
|
||||
// The room IDs are persisted to settings so we can detect unclean disconnects
|
||||
SettingsStore.setValue("activeCallRoomIds", null, SettingLevel.DEVICE, [...value].map(call => call.roomId));
|
||||
}
|
||||
|
||||
private calls = new Map<string, Call>(); // Key is room ID
|
||||
private callListeners = new Map<Call, Map<CallEvent, (...args: unknown[]) => unknown>>();
|
||||
|
||||
private updateRoom(room: Room) {
|
||||
if (!this.calls.has(room.roomId)) {
|
||||
const call = Call.get(room);
|
||||
|
||||
if (call) {
|
||||
const onConnectionState = (state: ConnectionState) => {
|
||||
if (state === ConnectionState.Connected) {
|
||||
this.activeCalls = new Set([...this.activeCalls, call]);
|
||||
} else if (state === ConnectionState.Disconnected) {
|
||||
this.activeCalls = new Set([...this.activeCalls].filter(c => c !== call));
|
||||
}
|
||||
};
|
||||
const onDestroy = () => {
|
||||
this.calls.delete(room.roomId);
|
||||
for (const [event, listener] of this.callListeners.get(call)!) call.off(event, listener);
|
||||
this.updateRoom(room);
|
||||
};
|
||||
|
||||
call.on(CallEvent.ConnectionState, onConnectionState);
|
||||
call.on(CallEvent.Destroy, onDestroy);
|
||||
|
||||
this.calls.set(room.roomId, call);
|
||||
this.callListeners.set(call, new Map<CallEvent, (...args: unknown[]) => unknown>([
|
||||
[CallEvent.ConnectionState, onConnectionState],
|
||||
[CallEvent.Destroy, onDestroy],
|
||||
]));
|
||||
}
|
||||
|
||||
this.emit(CallStoreEvent.Call, call, room.roomId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the call associated with the given room, if any.
|
||||
* @param {string} roomId The room's ID.
|
||||
* @returns {Call | null} The call.
|
||||
*/
|
||||
public get(roomId: string): Call | null {
|
||||
return this.calls.get(roomId) ?? null;
|
||||
}
|
||||
|
||||
private onRoom = (room: Room) => this.updateRoom(room);
|
||||
|
||||
private onRoomState = (event: MatrixEvent, state: RoomState) => {
|
||||
// If there's already a call stored for this room, it's understood to
|
||||
// still be valid until destroyed
|
||||
if (!this.calls.has(state.roomId)) {
|
||||
const room = this.matrixClient.getRoom(state.roomId);
|
||||
// State events can arrive before the room does, when creating a room
|
||||
if (room !== null) this.updateRoom(room);
|
||||
}
|
||||
};
|
||||
|
||||
private onWidgets = (roomId: string | null) => {
|
||||
if (roomId === null) {
|
||||
// This store happened to start before the widget store was done
|
||||
// loading all rooms, so we need to initialize each room again
|
||||
for (const room of this.matrixClient.getRooms()) {
|
||||
this.updateRoom(room);
|
||||
}
|
||||
} else {
|
||||
const room = this.matrixClient.getRoom(roomId);
|
||||
// Widget updates can arrive before the room does, empirically
|
||||
if (room !== null) this.updateRoom(room);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -30,10 +30,14 @@ interface IState {
|
||||
}
|
||||
|
||||
export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new ModalWidgetStore();
|
||||
private modalInstance: IHandle<void[]> = null;
|
||||
private openSourceWidgetId: string = null;
|
||||
private openSourceWidgetRoomId: string = null;
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new ModalWidgetStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
private modalInstance: IHandle<void[]> | null = null;
|
||||
private openSourceWidgetId: string | null = null;
|
||||
private openSourceWidgetRoomId: string | null = null;
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
|
||||
@@ -92,7 +92,11 @@ const getLocallyCreatedBeaconEventIds = (): string[] => {
|
||||
return ids;
|
||||
};
|
||||
export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||
private static internalInstance = new OwnBeaconStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new OwnBeaconStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
// users beacons, keyed by event type
|
||||
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<BeaconIdentifier>>();
|
||||
|
||||
@@ -37,7 +37,11 @@ const KEY_DISPLAY_NAME = "mx_profile_displayname";
|
||||
const KEY_AVATAR_URL = "mx_profile_avatar_url";
|
||||
|
||||
export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new OwnProfileStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new OwnProfileStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private monitoredUser: User;
|
||||
|
||||
|
||||
@@ -26,18 +26,19 @@ import { Action } from "../dispatcher/actions";
|
||||
|
||||
export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable {
|
||||
protected matrixClient: MatrixClient;
|
||||
private readonly dispatcherRef: string;
|
||||
private dispatcherRef: string | null = null;
|
||||
|
||||
constructor(protected readonly dispatcher: Dispatcher<ActionPayload>) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
this.dispatcherRef = this.dispatcher.register(this.onAction);
|
||||
|
||||
if (MatrixClientPeg.get()) {
|
||||
this.matrixClient = MatrixClientPeg.get();
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.onReady();
|
||||
const matrixClient = MatrixClientPeg.get();
|
||||
if (matrixClient) {
|
||||
this.matrixClient = matrixClient;
|
||||
await this.onReady();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.dispatcher.unregister(this.dispatcherRef);
|
||||
if (this.dispatcherRef !== null) this.dispatcher.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
protected async onReady() {
|
||||
|
||||
@@ -1,355 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The 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";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { ClientWidgetApi, IWidgetApiRequest } from "matrix-widget-api";
|
||||
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { ElementWidgetActions } from "./widgets/ElementWidgetActions";
|
||||
import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "./widgets/WidgetMessagingStore";
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "./ActiveWidgetStore";
|
||||
import { STUCK_DEVICE_TIMEOUT_MS, getVideoChannel, addOurDevice, removeOurDevice } from "../utils/VideoChannelUtils";
|
||||
import { timeout } from "../utils/promise";
|
||||
import WidgetUtils from "../utils/WidgetUtils";
|
||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
|
||||
export enum VideoChannelEvent {
|
||||
StartConnect = "start_connect",
|
||||
Connect = "connect",
|
||||
Disconnect = "disconnect",
|
||||
Participants = "participants",
|
||||
}
|
||||
|
||||
export interface IJitsiParticipant {
|
||||
avatarURL: string;
|
||||
displayName: string;
|
||||
formattedDisplayName: string;
|
||||
participantId: string;
|
||||
}
|
||||
|
||||
const TIMEOUT_MS = 16000;
|
||||
|
||||
// Wait until an event is emitted satisfying the given predicate
|
||||
const waitForEvent = async (emitter: EventEmitter, event: string, pred: (...args) => boolean = () => true) => {
|
||||
let listener;
|
||||
const wait = new Promise<void>(resolve => {
|
||||
listener = (...args) => { if (pred(...args)) resolve(); };
|
||||
emitter.on(event, listener);
|
||||
});
|
||||
|
||||
const timedOut = await timeout(wait, false, TIMEOUT_MS) === false;
|
||||
emitter.off(event, listener);
|
||||
if (timedOut) throw new Error("Timed out");
|
||||
};
|
||||
|
||||
/*
|
||||
* Holds information about the currently active video channel.
|
||||
*/
|
||||
export default class VideoChannelStore extends AsyncStoreWithClient<null> {
|
||||
private static _instance: VideoChannelStore;
|
||||
|
||||
public static get instance(): VideoChannelStore {
|
||||
if (!VideoChannelStore._instance) {
|
||||
VideoChannelStore._instance = new VideoChannelStore();
|
||||
}
|
||||
return VideoChannelStore._instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher);
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<void> {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
private activeChannel: ClientWidgetApi;
|
||||
private resendDevicesTimer: number;
|
||||
|
||||
// This is persisted to settings so we can detect unclean disconnects
|
||||
public get roomId(): string | null { return SettingsStore.getValue("videoChannelRoomId"); }
|
||||
private set roomId(value: string | null) {
|
||||
SettingsStore.setValue("videoChannelRoomId", null, SettingLevel.DEVICE, value);
|
||||
}
|
||||
|
||||
private get room(): Room { return this.matrixClient.getRoom(this.roomId); }
|
||||
|
||||
private _connected = false;
|
||||
public get connected(): boolean { return this._connected; }
|
||||
private set connected(value: boolean) { this._connected = value; }
|
||||
|
||||
private _participants: IJitsiParticipant[] = [];
|
||||
public get participants(): IJitsiParticipant[] { return this._participants; }
|
||||
private set participants(value: IJitsiParticipant[]) { this._participants = value; }
|
||||
|
||||
public get audioMuted(): boolean { return SettingsStore.getValue("audioInputMuted"); }
|
||||
public set audioMuted(value: boolean) {
|
||||
SettingsStore.setValue("audioInputMuted", null, SettingLevel.DEVICE, value);
|
||||
}
|
||||
|
||||
public get videoMuted(): boolean { return SettingsStore.getValue("videoInputMuted"); }
|
||||
public set videoMuted(value: boolean) {
|
||||
SettingsStore.setValue("videoInputMuted", null, SettingLevel.DEVICE, value);
|
||||
}
|
||||
|
||||
public connect = async (
|
||||
roomId: string,
|
||||
audioDevice: MediaDeviceInfo | null,
|
||||
videoDevice: MediaDeviceInfo | null,
|
||||
) => {
|
||||
if (this.activeChannel) await this.disconnect();
|
||||
|
||||
const jitsi = getVideoChannel(roomId);
|
||||
if (!jitsi) throw new Error(`No video channel in room ${roomId}`);
|
||||
|
||||
const jitsiUid = WidgetUtils.getWidgetUid(jitsi);
|
||||
const messagingStore = WidgetMessagingStore.instance;
|
||||
|
||||
let messaging = messagingStore.getMessagingForUid(jitsiUid);
|
||||
if (!messaging) {
|
||||
// The widget might still be initializing, so wait for it
|
||||
try {
|
||||
await waitForEvent(
|
||||
messagingStore,
|
||||
WidgetMessagingStoreEvent.StoreMessaging,
|
||||
(uid: string, widgetApi: ClientWidgetApi) => {
|
||||
if (uid === jitsiUid) {
|
||||
messaging = widgetApi;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to bind video channel in room ${roomId}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we got the messaging, we need a way to ensure that it doesn't get stopped
|
||||
const dontStopMessaging = new Promise<void>((resolve, reject) => {
|
||||
const listener = (uid: string) => {
|
||||
if (uid === jitsiUid) {
|
||||
cleanup();
|
||||
reject(new Error("Messaging stopped"));
|
||||
}
|
||||
};
|
||||
const done = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const cleanup = () => {
|
||||
messagingStore.off(WidgetMessagingStoreEvent.StopMessaging, listener);
|
||||
this.off(VideoChannelEvent.Connect, done);
|
||||
this.off(VideoChannelEvent.Disconnect, done);
|
||||
};
|
||||
|
||||
messagingStore.on(WidgetMessagingStoreEvent.StopMessaging, listener);
|
||||
this.on(VideoChannelEvent.Connect, done);
|
||||
this.on(VideoChannelEvent.Disconnect, done);
|
||||
});
|
||||
|
||||
if (!messagingStore.isWidgetReady(jitsiUid)) {
|
||||
// Wait for the widget to be ready to receive our join event
|
||||
try {
|
||||
await Promise.race([
|
||||
waitForEvent(
|
||||
messagingStore,
|
||||
WidgetMessagingStoreEvent.WidgetReady,
|
||||
(uid: string) => uid === jitsiUid,
|
||||
),
|
||||
dontStopMessaging,
|
||||
]);
|
||||
} catch (e) {
|
||||
throw new Error(`Video channel in room ${roomId} never became ready: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Participant data and mute state will come down the event pipeline quickly, so prepare in advance
|
||||
this.activeChannel = messaging;
|
||||
this.roomId = roomId;
|
||||
messaging.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
messaging.on(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
|
||||
messaging.on(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
messaging.on(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
messaging.on(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
// Empirically, it's possible for Jitsi Meet to crash instantly at startup,
|
||||
// sending a hangup event that races with the rest of this method, so we also
|
||||
// need to add the hangup listener now rather than later
|
||||
messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
this.emit(VideoChannelEvent.StartConnect, roomId);
|
||||
|
||||
// Actually perform the join
|
||||
const waitForJoin = waitForEvent(
|
||||
messaging,
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.ack(ev);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
messaging.transport.send(ElementWidgetActions.JoinCall, {
|
||||
audioDevice: audioDevice?.label ?? null,
|
||||
videoDevice: videoDevice?.label ?? null,
|
||||
});
|
||||
try {
|
||||
await Promise.race([waitForJoin, dontStopMessaging]);
|
||||
} catch (e) {
|
||||
// If it timed out, clean up our advance preparations
|
||||
this.activeChannel = null;
|
||||
this.roomId = null;
|
||||
|
||||
messaging.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
messaging.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
|
||||
messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
messaging.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
messaging.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
messaging.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
if (messaging.transport.ready) {
|
||||
// The messaging still exists, which means Jitsi might still be going in the background
|
||||
messaging.transport.send(ElementWidgetActions.ForceHangupCall, {});
|
||||
}
|
||||
|
||||
this.emit(VideoChannelEvent.Disconnect, roomId);
|
||||
|
||||
throw new Error(`Failed to join call in room ${roomId}: ${e}`);
|
||||
}
|
||||
|
||||
this.connected = true;
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.addEventListener("beforeunload", this.setDisconnected);
|
||||
|
||||
this.emit(VideoChannelEvent.Connect, roomId);
|
||||
|
||||
// Tell others that we're connected, by adding our device to room state
|
||||
await addOurDevice(this.room);
|
||||
// Re-add this device every so often so our video member event doesn't become stale
|
||||
this.resendDevicesTimer = setInterval(async () => {
|
||||
logger.log(`Resending video member event for ${this.roomId}`);
|
||||
await addOurDevice(this.room);
|
||||
}, (STUCK_DEVICE_TIMEOUT_MS * 3) / 4);
|
||||
};
|
||||
|
||||
public disconnect = async () => {
|
||||
if (!this.activeChannel) throw new Error("Not connected to any video channel");
|
||||
|
||||
const waitForDisconnect = waitForEvent(this, VideoChannelEvent.Disconnect);
|
||||
this.activeChannel.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
try {
|
||||
await waitForDisconnect; // onHangup cleans up for us
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to hangup call in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
public setDisconnected = async () => {
|
||||
const roomId = this.roomId;
|
||||
const room = this.room;
|
||||
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
room.off(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.removeEventListener("beforeunload", this.setDisconnected);
|
||||
clearInterval(this.resendDevicesTimer);
|
||||
|
||||
this.activeChannel = null;
|
||||
this.roomId = null;
|
||||
this.connected = false;
|
||||
this.participants = [];
|
||||
|
||||
this.emit(VideoChannelEvent.Disconnect, roomId);
|
||||
|
||||
// Tell others that we're disconnected, by removing our device from room state
|
||||
await removeOurDevice(room);
|
||||
};
|
||||
|
||||
private ack = (ev: CustomEvent<IWidgetApiRequest>, messaging = this.activeChannel) => {
|
||||
// Even if we don't have a reply to a given widget action, we still need
|
||||
// to give the widget API something to acknowledge receipt
|
||||
messaging.transport.reply(ev.detail, {});
|
||||
};
|
||||
|
||||
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
const messaging = this.activeChannel;
|
||||
// In case this hangup is caused by Jitsi Meet crashing at startup,
|
||||
// wait for the connection event in order to avoid racing
|
||||
if (!this.connected) await waitForEvent(this, VideoChannelEvent.Connect);
|
||||
await this.setDisconnected();
|
||||
this.ack(ev, messaging);
|
||||
};
|
||||
|
||||
private onParticipants = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.participants = ev.detail.data.participants as IJitsiParticipant[];
|
||||
this.emit(VideoChannelEvent.Participants, this.roomId, ev.detail.data.participants);
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onMuteAudio = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.audioMuted = true;
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onUnmuteAudio = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.audioMuted = false;
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onMuteVideo = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.videoMuted = true;
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onUnmuteVideo = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.videoMuted = false;
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onMyMembership = (room: Room, membership: string) => {
|
||||
if (membership !== "join") this.setDisconnected();
|
||||
};
|
||||
|
||||
private onDock = async () => {
|
||||
// The widget is no longer a PiP, so let's restore the default layout
|
||||
await this.activeChannel.transport.send(ElementWidgetActions.TileLayout, {});
|
||||
};
|
||||
|
||||
private onUndock = async () => {
|
||||
// The widget has become a PiP, so let's switch Jitsi to spotlight mode
|
||||
// to only show the active speaker and economize on space
|
||||
await this.activeChannel.transport.send(ElementWidgetActions.SpotlightLayout, {});
|
||||
};
|
||||
}
|
||||
@@ -33,10 +33,11 @@ export class VoiceRecordingStore extends AsyncStoreWithClient<IState> {
|
||||
}
|
||||
|
||||
public static get instance(): VoiceRecordingStore {
|
||||
if (!VoiceRecordingStore.internalInstance) {
|
||||
VoiceRecordingStore.internalInstance = new VoiceRecordingStore();
|
||||
if (!this.internalInstance) {
|
||||
this.internalInstance = new VoiceRecordingStore();
|
||||
this.internalInstance.start();
|
||||
}
|
||||
return VoiceRecordingStore.internalInstance;
|
||||
return this.internalInstance;
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<void> {
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface IApp extends IWidget {
|
||||
roomId: string;
|
||||
eventId: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
|
||||
avatar_url?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
|
||||
}
|
||||
|
||||
interface IRoomWidgets {
|
||||
@@ -46,7 +46,11 @@ interface IRoomWidgets {
|
||||
// TODO consolidate WidgetEchoStore into this
|
||||
// TODO consolidate ActiveWidgetStore into this
|
||||
export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new WidgetStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new WidgetStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private widgetMap = new Map<string, IApp>(); // Key is widget Unique ID (UID)
|
||||
private roomMap = new Map<string, IRoomWidgets>(); // Key is room ID
|
||||
|
||||
@@ -44,10 +44,11 @@ export class EchoStore extends AsyncStoreWithClient<IState> {
|
||||
}
|
||||
|
||||
public static get instance(): EchoStore {
|
||||
if (!EchoStore._instance) {
|
||||
EchoStore._instance = new EchoStore();
|
||||
if (!this._instance) {
|
||||
this._instance = new EchoStore();
|
||||
this._instance.start();
|
||||
}
|
||||
return EchoStore._instance;
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
public get contexts(): EchoContext[] {
|
||||
|
||||
@@ -34,7 +34,11 @@ interface IState {}
|
||||
export const UPDATE_STATUS_INDICATOR = Symbol("update-status-indicator");
|
||||
|
||||
export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new RoomNotificationStateStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new RoomNotificationStateStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private roomMap = new Map<Room, RoomNotificationState>();
|
||||
private roomThreadsMap = new Map<Room, ThreadsRoomNotificationState>();
|
||||
|
||||
@@ -394,10 +394,11 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
||||
}
|
||||
|
||||
public static get instance(): RightPanelStore {
|
||||
if (!RightPanelStore.internalInstance) {
|
||||
RightPanelStore.internalInstance = new RightPanelStore();
|
||||
if (!this.internalInstance) {
|
||||
this.internalInstance = new RightPanelStore();
|
||||
this.internalInstance.start();
|
||||
}
|
||||
return RightPanelStore.internalInstance;
|
||||
return this.internalInstance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { MessageEventPreview } from "./previews/MessageEventPreview";
|
||||
import { PollStartEventPreview } from "./previews/PollStartEventPreview";
|
||||
import { TagID } from "./models";
|
||||
import { CallInviteEventPreview } from "./previews/CallInviteEventPreview";
|
||||
import { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview";
|
||||
import { CallHangupEvent } from "./previews/CallHangupEvent";
|
||||
import { LegacyCallInviteEventPreview } from "./previews/LegacyCallInviteEventPreview";
|
||||
import { LegacyCallAnswerEventPreview } from "./previews/LegacyCallAnswerEventPreview";
|
||||
import { LegacyCallHangupEvent } from "./previews/LegacyCallHangupEvent";
|
||||
import { StickerEventPreview } from "./previews/StickerEventPreview";
|
||||
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
||||
import { UPDATE_EVENT } from "../AsyncStore";
|
||||
@@ -47,15 +47,15 @@ const PREVIEWS: Record<string, {
|
||||
},
|
||||
'm.call.invite': {
|
||||
isState: false,
|
||||
previewer: new CallInviteEventPreview(),
|
||||
previewer: new LegacyCallInviteEventPreview(),
|
||||
},
|
||||
'm.call.answer': {
|
||||
isState: false,
|
||||
previewer: new CallAnswerEventPreview(),
|
||||
previewer: new LegacyCallAnswerEventPreview(),
|
||||
},
|
||||
'm.call.hangup': {
|
||||
isState: false,
|
||||
previewer: new CallHangupEvent(),
|
||||
previewer: new LegacyCallHangupEvent(),
|
||||
},
|
||||
'm.sticker': {
|
||||
isState: false,
|
||||
@@ -87,7 +87,11 @@ interface IState {
|
||||
}
|
||||
|
||||
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new MessagePreviewStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new MessagePreviewStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
// null indicates the preview is empty / irrelevant
|
||||
private previews = new Map<string, Map<TagID|TAG_ANY, string|null>>();
|
||||
|
||||
@@ -34,8 +34,9 @@ export default class RoomListLayoutStore extends AsyncStoreWithClient<IState> {
|
||||
}
|
||||
|
||||
public static get instance(): RoomListLayoutStore {
|
||||
if (!RoomListLayoutStore.internalInstance) {
|
||||
RoomListLayoutStore.internalInstance = new RoomListLayoutStore();
|
||||
if (!this.internalInstance) {
|
||||
this.internalInstance = new RoomListLayoutStore();
|
||||
this.internalInstance.start();
|
||||
}
|
||||
return RoomListLayoutStore.internalInstance;
|
||||
}
|
||||
|
||||
@@ -602,11 +602,13 @@ export default class RoomListStore {
|
||||
private static internalInstance: Interface;
|
||||
|
||||
public static get instance(): Interface {
|
||||
if (!RoomListStore.internalInstance) {
|
||||
RoomListStore.internalInstance = new RoomListStoreClass();
|
||||
if (!this.internalInstance) {
|
||||
const instance = new RoomListStoreClass();
|
||||
instance.start();
|
||||
this.internalInstance = instance;
|
||||
}
|
||||
|
||||
return RoomListStore.internalInstance;
|
||||
return this.internalInstance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
|
||||
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
||||
import { getListAlgorithmInstance } from "./list-ordering";
|
||||
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
||||
import VideoChannelStore, { VideoChannelEvent } from "../../VideoChannelStore";
|
||||
import { CallStore, CallStoreEvent } from "../../CallStore";
|
||||
|
||||
/**
|
||||
* Fired when the Algorithm has determined a list has been updated.
|
||||
@@ -82,13 +82,11 @@ export class Algorithm extends EventEmitter {
|
||||
public updatesInhibited = false;
|
||||
|
||||
public start() {
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Connect, this.updateVideoRoom);
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Disconnect, this.updateVideoRoom);
|
||||
CallStore.instance.on(CallStoreEvent.ActiveCalls, this.onActiveCalls);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Connect, this.updateVideoRoom);
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Disconnect, this.updateVideoRoom);
|
||||
CallStore.instance.off(CallStoreEvent.ActiveCalls, this.onActiveCalls);
|
||||
}
|
||||
|
||||
public get stickyRoom(): Room {
|
||||
@@ -106,7 +104,7 @@ export class Algorithm extends EventEmitter {
|
||||
protected set cachedRooms(val: ITagMap) {
|
||||
this._cachedRooms = val;
|
||||
this.recalculateStickyRoom();
|
||||
this.recalculateVideoRoom();
|
||||
this.recalculateActiveCallRooms();
|
||||
}
|
||||
|
||||
protected get cachedRooms(): ITagMap {
|
||||
@@ -143,7 +141,7 @@ export class Algorithm extends EventEmitter {
|
||||
algorithm.setSortAlgorithm(sort);
|
||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||
this.recalculateVideoRoom(tagId);
|
||||
this.recalculateActiveCallRooms(tagId);
|
||||
}
|
||||
|
||||
public getListOrdering(tagId: TagID): ListAlgorithm {
|
||||
@@ -162,7 +160,7 @@ export class Algorithm extends EventEmitter {
|
||||
algorithm.setRooms(this._cachedRooms[tagId]);
|
||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||
this.recalculateVideoRoom(tagId);
|
||||
this.recalculateActiveCallRooms(tagId);
|
||||
}
|
||||
|
||||
private updateStickyRoom(val: Room) {
|
||||
@@ -279,22 +277,20 @@ export class Algorithm extends EventEmitter {
|
||||
// a room while filtering and it'll disappear. We don't update the filter earlier in
|
||||
// this function simply because we don't have to.
|
||||
this.recalculateStickyRoom();
|
||||
this.recalculateVideoRoom(tag);
|
||||
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateVideoRoom(lastStickyRoom.tag);
|
||||
this.recalculateActiveCallRooms(tag);
|
||||
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateActiveCallRooms(lastStickyRoom.tag);
|
||||
|
||||
// Finally, trigger an update
|
||||
if (this.updatesInhibited) return;
|
||||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the stickiness of video rooms.
|
||||
*/
|
||||
public updateVideoRoom = () => {
|
||||
// In case we're unsticking a video room, sort it back into natural order
|
||||
private onActiveCalls = () => {
|
||||
// In case we're unsticking a room, sort it back into natural order
|
||||
this.recalculateStickyRoom();
|
||||
|
||||
this.recalculateVideoRoom();
|
||||
// Update the stickiness of rooms with calls
|
||||
this.recalculateActiveCallRooms();
|
||||
|
||||
if (this.updatesInhibited) return;
|
||||
// This isn't in response to any particular RoomListStore update,
|
||||
@@ -358,16 +354,16 @@ export class Algorithm extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate the position of any video rooms. If this is being called in relation to
|
||||
* a specific tag being updated, it should be given to this function to optimize
|
||||
* the call.
|
||||
* Recalculate the position of any rooms with calls. If this is being called in
|
||||
* relation to a specific tag being updated, it should be given to this function to
|
||||
* optimize the call.
|
||||
*
|
||||
* This expects to be called *after* the sticky rooms are updated, and sticks the
|
||||
* currently connected video room to the top of its tag.
|
||||
* room with the currently active call to the top of its tag.
|
||||
*
|
||||
* @param updatedTag The tag that was updated, if possible.
|
||||
*/
|
||||
protected recalculateVideoRoom(updatedTag: TagID = null): void {
|
||||
protected recalculateActiveCallRooms(updatedTag: TagID = null): void {
|
||||
if (!updatedTag) {
|
||||
// Assume all tags need updating
|
||||
// We're not modifying the map here, so can safely rely on the cached values
|
||||
@@ -376,24 +372,26 @@ export class Algorithm extends EventEmitter {
|
||||
if (!tagId) {
|
||||
throw new Error("Unexpected recursion: falsy tag");
|
||||
}
|
||||
this.recalculateVideoRoom(tagId);
|
||||
this.recalculateActiveCallRooms(tagId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const videoRoomId = VideoChannelStore.instance.connected ? VideoChannelStore.instance.roomId : null;
|
||||
|
||||
if (videoRoomId) {
|
||||
// We operate directly on the sticky rooms map
|
||||
if (CallStore.instance.activeCalls.size) {
|
||||
// We operate on the sticky rooms map
|
||||
if (!this._cachedStickyRooms) this.initCachedStickyRooms();
|
||||
const rooms = this._cachedStickyRooms[updatedTag];
|
||||
const videoRoomIdxInTag = rooms.findIndex(r => r.roomId === videoRoomId);
|
||||
if (videoRoomIdxInTag < 0) return; // no-op
|
||||
|
||||
const videoRoom = rooms[videoRoomIdxInTag];
|
||||
rooms.splice(videoRoomIdxInTag, 1);
|
||||
rooms.unshift(videoRoom);
|
||||
this._cachedStickyRooms[updatedTag] = rooms; // re-set because references aren't always safe
|
||||
const activeRoomIds = new Set([...CallStore.instance.activeCalls].map(call => call.roomId));
|
||||
const activeRooms: Room[] = [];
|
||||
const inactiveRooms: Room[] = [];
|
||||
|
||||
for (const room of rooms) {
|
||||
(activeRoomIds.has(room.roomId) ? activeRooms : inactiveRooms).push(room);
|
||||
}
|
||||
|
||||
// Stick rooms with active calls to the top
|
||||
this._cachedStickyRooms[updatedTag] = [...activeRooms, ...inactiveRooms];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,7 +664,7 @@ export class Algorithm extends EventEmitter {
|
||||
algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||
this._cachedRooms[rmTag] = algorithm.orderedRooms;
|
||||
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
|
||||
this.recalculateVideoRoom(rmTag);
|
||||
this.recalculateActiveCallRooms(rmTag);
|
||||
}
|
||||
for (const addTag of diff.added) {
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
||||
@@ -742,7 +740,7 @@ export class Algorithm extends EventEmitter {
|
||||
|
||||
// Flag that we've done something
|
||||
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
|
||||
this.recalculateVideoRoom(tag);
|
||||
this.recalculateActiveCallRooms(tag);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
import { RoomListCustomisations } from "../../../customisations/RoomList";
|
||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||
import VoipUserMapper from "../../../VoipUserMapper";
|
||||
@@ -44,7 +44,7 @@ export class VisibilityProvider {
|
||||
}
|
||||
|
||||
if (
|
||||
CallHandler.instance.getSupportsVirtualRooms() &&
|
||||
LegacyCallHandler.instance.getSupportsVirtualRooms() &&
|
||||
VoipUserMapper.sharedInstance().isVirtualRoom(room)
|
||||
) {
|
||||
return false;
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import { TagID } from "../models";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallAnswerEventPreview implements IPreview {
|
||||
export class LegacyCallAnswerEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
||||
+1
-1
@@ -21,7 +21,7 @@ import { TagID } from "../models";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallHangupEvent implements IPreview {
|
||||
export class LegacyCallHangupEvent implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
||||
+1
-1
@@ -21,7 +21,7 @@ import { TagID } from "../models";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallInviteEventPreview implements IPreview {
|
||||
export class LegacyCallInviteEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
||||
@@ -1284,7 +1284,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
}
|
||||
|
||||
export default class SpaceStore {
|
||||
private static internalInstance = new SpaceStoreClass();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new SpaceStoreClass();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
public static get instance(): SpaceStoreClass {
|
||||
return SpaceStore.internalInstance;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
* Copyright 2020-2022 The 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.
|
||||
@@ -17,13 +17,9 @@
|
||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||
|
||||
export enum ElementWidgetActions {
|
||||
ClientReady = "im.vector.ready",
|
||||
WidgetReady = "io.element.widget_ready",
|
||||
|
||||
// All of these actions are currently specific to Jitsi
|
||||
JoinCall = "io.element.join",
|
||||
HangupCall = "im.vector.hangup",
|
||||
ForceHangupCall = "io.element.force_hangup",
|
||||
CallParticipants = "io.element.participants",
|
||||
MuteAudio = "io.element.mute_audio",
|
||||
UnmuteAudio = "io.element.unmute_audio",
|
||||
|
||||
@@ -114,10 +114,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
||||
}
|
||||
|
||||
public static get instance(): WidgetLayoutStore {
|
||||
if (!WidgetLayoutStore.internalInstance) {
|
||||
WidgetLayoutStore.internalInstance = new WidgetLayoutStore();
|
||||
if (!this.internalInstance) {
|
||||
this.internalInstance = new WidgetLayoutStore();
|
||||
this.internalInstance.start();
|
||||
}
|
||||
return WidgetLayoutStore.internalInstance;
|
||||
return this.internalInstance;
|
||||
}
|
||||
|
||||
public static emissionForRoom(room: Room): string {
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClientWidgetApi, Widget, IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { ClientWidgetApi, Widget } from "matrix-widget-api";
|
||||
|
||||
import { ElementWidgetActions } from "./ElementWidgetActions";
|
||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
@@ -26,7 +25,6 @@ import WidgetUtils from "../../utils/WidgetUtils";
|
||||
export enum WidgetMessagingStoreEvent {
|
||||
StoreMessaging = "store_messaging",
|
||||
StopMessaging = "stop_messaging",
|
||||
WidgetReady = "widget_ready",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,11 +32,14 @@ export enum WidgetMessagingStoreEvent {
|
||||
* going to be merged with a more complete WidgetStore, but for now it's
|
||||
* easiest to split this into a single place.
|
||||
*/
|
||||
export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||
private static internalInstance = new WidgetMessagingStore();
|
||||
export class WidgetMessagingStore extends AsyncStoreWithClient<{}> {
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new WidgetMessagingStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget UID, ClientWidgetAPi>
|
||||
private readyWidgets = new Set<string>(); // widgets that have sent a WidgetReady event
|
||||
|
||||
public constructor() {
|
||||
super(defaultDispatcher);
|
||||
@@ -62,12 +63,6 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||
const uid = WidgetUtils.calcWidgetUid(widget.id, roomId);
|
||||
this.widgetMap.set(uid, widgetApi);
|
||||
|
||||
widgetApi.once(`action:${ElementWidgetActions.WidgetReady}`, (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this.readyWidgets.add(uid);
|
||||
this.emit(WidgetMessagingStoreEvent.WidgetReady, uid);
|
||||
widgetApi.transport.reply(ev.detail, {}); // ack
|
||||
});
|
||||
|
||||
this.emit(WidgetMessagingStoreEvent.StoreMessaging, uid, widgetApi);
|
||||
}
|
||||
|
||||
@@ -85,7 +80,6 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||
*/
|
||||
public stopMessagingByUid(widgetUid: string) {
|
||||
this.widgetMap.remove(widgetUid)?.stop();
|
||||
this.readyWidgets.delete(widgetUid);
|
||||
this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid);
|
||||
}
|
||||
|
||||
@@ -97,12 +91,4 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||
public getMessagingForUid(widgetUid: string): ClientWidgetApi {
|
||||
return this.widgetMap.get(widgetUid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} widgetUid The widget UID.
|
||||
* @returns {boolean} Whether the widget has issued an ElementWidgetActions.WidgetReady event.
|
||||
*/
|
||||
public isWidgetReady(widgetUid: string): boolean {
|
||||
return this.readyWidgets.has(widgetUid);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user