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

View File

@ -13,7 +13,7 @@ import classNames from "classnames";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import RoomList from "../views/rooms/RoomList"; import RoomList from "../views/rooms/RoomList";
import LegacyCallHandler from "../../LegacyCallHandler"; import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist"; import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import RoomSearch from "./RoomSearch"; import RoomSearch from "./RoomSearch";
@ -51,6 +51,7 @@ enum BreadcrumbsMode {
interface IState { interface IState {
showBreadcrumbs: BreadcrumbsMode; showBreadcrumbs: BreadcrumbsMode;
activeSpace: SpaceKey; activeSpace: SpaceKey;
supportsPstnProtocol: boolean;
} }
export default class LeftPanel extends React.Component<IProps, IState> { export default class LeftPanel extends React.Component<IProps, IState> {
@ -65,6 +66,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.state = { this.state = {
activeSpace: SpaceStore.instance.activeSpace, activeSpace: SpaceStore.instance.activeSpace,
showBreadcrumbs: LeftPanel.breadcrumbsMode, 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); BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
if (this.listContainerRef.current) { if (this.listContainerRef.current) {
UIStore.instance.trackElementDimensions("ListContainer", 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); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
LegacyCallHandler.instance.off(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
UIStore.instance.stopTrackingElementDimensions("ListContainer"); UIStore.instance.stopTrackingElementDimensions("ListContainer");
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders); UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
this.listContainerRef.current?.removeEventListener("scroll", this.onScroll); 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 => { private updateActiveSpace = (activeSpace: SpaceKey): void => {
this.setState({ activeSpace }); this.setState({ activeSpace });
}; };
@ -330,9 +338,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private renderSearchDialExplore(): React.ReactNode { private renderSearchDialExplore(): React.ReactNode {
let dialPadButton: JSX.Element | undefined; let dialPadButton: JSX.Element | undefined;
// If we have dialer support, show a button to bring up the dial pad // If we have dialer support, show a button to bring up the dial pad to start a new call
// to start a new call if (this.state.supportsPstnProtocol) {
if (LegacyCallHandler.instance.getSupportsPstnProtocol()) {
dialPadButton = ( dialPadButton = (
<AccessibleButton <AccessibleButton
className={classNames("mx_LeftPanel_dialPadButton", {})} 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 // don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room // 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. 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 React, { ComponentType, createRef, ReactComponentElement, SyntheticEvent } from "react";
import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
@ -56,6 +56,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation"; import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler.tsx";
interface IProps { interface IProps {
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void; 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); SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms); SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
this.updateLists(); // trigger the first update 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); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate); SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
LegacyCallHandler.instance.off(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
} }
private updateProtocolSupport = (): void => {
this.updateLists();
};
private onRoomViewStoreUpdate = (): void => { private onRoomViewStoreUpdate = (): void => {
this.setState({ this.setState({
currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId() ?? undefined, currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId() ?? undefined,
@ -471,8 +478,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
metricsViaKeyboard: true, metricsViaKeyboard: true,
}); });
} }
} else if (payload.action === Action.PstnSupportUpdated) {
this.updateLists();
} }
}; };

View File

@ -135,20 +135,6 @@ export enum Action {
*/ */
OpenDialPad = "open_dial_pad", 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. * Fired when an upload has started. Should be used with UploadStartedPayload.
*/ */