Refactor LegacyCallHandler event emitter to use TypedEventEmitter (#29008)

* Switch LegacyCallHandler over to TypedEventEmitter and use emits to notify consumers of protocol support updates

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test for dialpad

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2025-01-16 10:44:20 +00:00 committed by GitHub
parent 13913ba8b2
commit e5ca7954c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 66 additions and 30 deletions

View File

@ -0,0 +1,31 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
test.describe("PSTN", () => {
test.beforeEach(async ({ page }) => {
// Mock the third party protocols endpoint to look like the HS has PSTN support
await page.route("**/_matrix/client/v3/thirdparty/protocols", async (route) => {
await route.fulfill({
status: 200,
json: {
"im.vector.protocol.pstn": {},
},
});
});
});
test("should render dialpad as expected", { tag: "@screenshot" }, async ({ page, user, toasts }) => {
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();
await expect(page.locator(".mx_LeftPanel_filterContainer")).toMatchScreenshot("dialpad-trigger.png");
await page.getByLabel("Open dial pad").click();
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("dialpad.png");
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { MatrixError, RuleId, TweakName, SyncState } from "matrix-js-sdk/src/matrix";
import { MatrixError, RuleId, TweakName, SyncState, TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import {
CallError,
CallErrorCode,
@ -22,7 +22,6 @@ import {
MatrixCall,
} from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger";
import EventEmitter from "events";
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler";
@ -137,14 +136,23 @@ export enum LegacyCallHandlerEvent {
CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed",
CallState = "call_state",
ProtocolSupport = "protocol_support",
}
type EventEmitterMap = {
[LegacyCallHandlerEvent.CallsChanged]: (calls: Map<string, MatrixCall>) => void;
[LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void;
[LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set<string>) => void;
[LegacyCallHandlerEvent.CallState]: (mappedRoomId: string | null, status: CallState) => void;
[LegacyCallHandlerEvent.ProtocolSupport]: () => void;
};
/**
* LegacyCallHandler manages all currently active calls. It should be used for
* placing, answering, rejecting and hanging up calls. It also handles ringing,
* PSTN support and other things.
*/
export default class LegacyCallHandler extends EventEmitter {
export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandlerEvent, EventEmitterMap> {
private calls = new Map<string, MatrixCall>(); // roomId -> call
// Calls started as an attended transfer, ie. with the intention of transferring another
// call with a different party to this one.
@ -271,15 +279,13 @@ export default class LegacyCallHandler extends EventEmitter {
this.supportsPstnProtocol = null;
}
dis.dispatch({ action: Action.PstnSupportUpdated });
if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) {
this.supportsSipNativeVirtual = Boolean(
protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL],
);
}
dis.dispatch({ action: Action.VirtualRoomSupportUpdated });
this.emit(LegacyCallHandlerEvent.ProtocolSupport);
} catch (e) {
if (maxTries === 1) {
logger.log("Failed to check for protocol support and no retries remain: assuming no support", e);
@ -296,8 +302,8 @@ export default class LegacyCallHandler extends EventEmitter {
return !!SdkConfig.getObject("voip")?.get("obey_asserted_identity");
}
public getSupportsPstnProtocol(): boolean | null {
return this.supportsPstnProtocol;
public getSupportsPstnProtocol(): boolean {
return this.supportsPstnProtocol ?? false;
}
public getSupportsVirtualRooms(): boolean | null {
@ -568,6 +574,7 @@ export default class LegacyCallHandler extends EventEmitter {
if (!mappedRoomId || !this.matchesCallForThisRoom(call)) return;
this.setCallState(call, newState);
// XXX: this is used by the IPC into Electron to keep device awake
dis.dispatch({
action: "call_state",
room_id: mappedRoomId,

View File

@ -13,7 +13,7 @@ import classNames from "classnames";
import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
import RoomList from "../views/rooms/RoomList";
import LegacyCallHandler from "../../LegacyCallHandler";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
import { Action } from "../../dispatcher/actions";
import RoomSearch from "./RoomSearch";
@ -51,6 +51,7 @@ enum BreadcrumbsMode {
interface IState {
showBreadcrumbs: BreadcrumbsMode;
activeSpace: SpaceKey;
supportsPstnProtocol: boolean;
}
export default class LeftPanel extends React.Component<IProps, IState> {
@ -65,6 +66,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.state = {
activeSpace: SpaceStore.instance.activeSpace,
showBreadcrumbs: LeftPanel.breadcrumbsMode,
supportsPstnProtocol: LegacyCallHandler.instance.getSupportsPstnProtocol(),
};
}
@ -76,6 +78,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
if (this.listContainerRef.current) {
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
@ -90,6 +93,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
LegacyCallHandler.instance.off(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
UIStore.instance.stopTrackingElementDimensions("ListContainer");
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
this.listContainerRef.current?.removeEventListener("scroll", this.onScroll);
@ -101,6 +105,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
}
}
private updateProtocolSupport = (): void => {
this.setState({ supportsPstnProtocol: LegacyCallHandler.instance.getSupportsPstnProtocol() });
};
private updateActiveSpace = (activeSpace: SpaceKey): void => {
this.setState({ activeSpace });
};
@ -330,9 +338,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private renderSearchDialExplore(): React.ReactNode {
let dialPadButton: JSX.Element | undefined;
// If we have dialer support, show a button to bring up the dial pad
// to start a new call
if (LegacyCallHandler.instance.getSupportsPstnProtocol()) {
// If we have dialer support, show a button to bring up the dial pad to start a new call
if (this.state.supportsPstnProtocol) {
dialPadButton = (
<AccessibleButton
className={classNames("mx_LeftPanel_dialPadButton", {})}

View File

@ -1082,7 +1082,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
};
private onCallState = (roomId: string): void => {
private onCallState = (roomId: string | null): void => {
// don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { EventType, RoomType, Room } from "matrix-js-sdk/src/matrix";
import { EventType, Room, RoomType } from "matrix-js-sdk/src/matrix";
import React, { ComponentType, createRef, ReactComponentElement, SyntheticEvent } from "react";
import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
@ -56,6 +56,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import AccessibleButton from "../elements/AccessibleButton";
import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler.tsx";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
@ -440,6 +441,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
this.updateLists(); // trigger the first update
}
@ -448,8 +450,13 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
defaultDispatcher.unregister(this.dispatcherRef);
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
LegacyCallHandler.instance.off(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
}
private updateProtocolSupport = (): void => {
this.updateLists();
};
private onRoomViewStoreUpdate = (): void => {
this.setState({
currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId() ?? undefined,
@ -471,8 +478,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
metricsViaKeyboard: true,
});
}
} else if (payload.action === Action.PstnSupportUpdated) {
this.updateLists();
}
};

View File

@ -135,20 +135,6 @@ export enum Action {
*/
OpenDialPad = "open_dial_pad",
/**
* Fired when CallHandler has checked for PSTN protocol support
* payload: none
* XXX: Is an action the right thing for this?
*/
PstnSupportUpdated = "pstn_support_updated",
/**
* Similar to PstnSupportUpdated, fired when CallHandler has checked for virtual room support
* payload: none
* XXX: Ditto
*/
VirtualRoomSupportUpdated = "virtual_room_support_updated",
/**
* Fired when an upload has started. Should be used with UploadStartedPayload.
*/