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:
parent
50f6986f6c
commit
0d6a550c33
107 changed files with 2573 additions and 2157 deletions
185
src/stores/CallStore.ts
Normal file
185
src/stores/CallStore.ts
Normal file
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue