From 4bf115583f6c69cf394a402b00854b04c450bca1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jan 2025 16:26:28 +0000 Subject: [PATCH 1/2] Switch LegacyCallHandler over to TypedEventEmitter and use emits to notify consumers of protocol support updates Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/LegacyCallHandler.tsx | 23 +++++++++++++++-------- src/components/structures/LeftPanel.tsx | 15 +++++++++++---- src/components/structures/RoomView.tsx | 2 +- src/components/views/rooms/RoomList.tsx | 11 ++++++++--- src/dispatcher/actions.ts | 14 -------------- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index c2df066fa23..8aaa32f4718 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -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) => void; + [LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void; + [LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set) => 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 { private calls = new Map(); // 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, diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 92e612a477b..de591a6b227 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -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 { @@ -65,6 +66,7 @@ export default class LeftPanel extends React.Component { this.state = { activeSpace: SpaceStore.instance.activeSpace, showBreadcrumbs: LeftPanel.breadcrumbsMode, + supportsPstnProtocol: LegacyCallHandler.instance.getSupportsPstnProtocol(), }; } @@ -76,6 +78,7 @@ export default class LeftPanel extends React.Component { 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 { 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 { } } + 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 { 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 = ( { } }; - 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 diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index fed1826bbb8..22e4f49469a 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -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 { 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 { 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 { metricsViaKeyboard: true, }); } - } else if (payload.action === Action.PstnSupportUpdated) { - this.updateLists(); } }; diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 5205e5badf3..cd8b7aea3dd 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -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. */ From 09f4fd824ac751e5e09e7dfc325ed204493584fd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 15 Jan 2025 16:26:34 +0000 Subject: [PATCH 2/2] Add test for dialpad Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/voip/pstn.spec.ts | 31 ++++++++++++++++++ .../voip/pstn.spec.ts/dialpad-linux.png | Bin 0 -> 13680 bytes .../pstn.spec.ts/dialpad-trigger-linux.png | Bin 0 -> 3236 bytes 3 files changed, 31 insertions(+) create mode 100644 playwright/e2e/voip/pstn.spec.ts create mode 100644 playwright/snapshots/voip/pstn.spec.ts/dialpad-linux.png create mode 100644 playwright/snapshots/voip/pstn.spec.ts/dialpad-trigger-linux.png diff --git a/playwright/e2e/voip/pstn.spec.ts b/playwright/e2e/voip/pstn.spec.ts new file mode 100644 index 00000000000..9a35d9b9c3d --- /dev/null +++ b/playwright/e2e/voip/pstn.spec.ts @@ -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"); + }); +}); diff --git a/playwright/snapshots/voip/pstn.spec.ts/dialpad-linux.png b/playwright/snapshots/voip/pstn.spec.ts/dialpad-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..3be63e2f506e4fb0c7d92e8cc0836f347bca21a6 GIT binary patch literal 13680 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 X;_F)AMe+9)DiBIiLEw$ literal 0 HcmV?d00001 diff --git a/playwright/snapshots/voip/pstn.spec.ts/dialpad-trigger-linux.png b/playwright/snapshots/voip/pstn.spec.ts/dialpad-trigger-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..17bea8979db806f0129cc83136cc4ea3a814f183 GIT binary patch literal 3236 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 literal 0 HcmV?d00001