From 3a3ca258c519da83312f4c7ae9b6d59eb18dc663 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 14 Oct 2022 12:46:27 +0200 Subject: [PATCH 1/6] silence call ringers when local notifications are silenced --- src/LegacyCallHandler.tsx | 13 ++- src/toasts/IncomingLegacyCallToast.tsx | 2 + test/LegacyCallHandler-test.ts | 128 ++++++++++++++++++++++++- yarn.lock | 5 + 4 files changed, 144 insertions(+), 4 deletions(-) diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index a924388eadb..c49a25c7e3f 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -62,6 +62,7 @@ import { KIND_CALL_TRANSFER } from "./components/views/dialogs/InviteDialogTypes import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload"; import { findDMForUser } from './utils/dm/findDMForUser'; import { getJoinedNonFunctionalMembers } from './utils/room/getJoinedNonFunctionalMembers'; +import { localNotificationsAreSilenced } from './utils/notifications'; export const PROTOCOL_PSTN = 'm.protocol.pstn'; export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; @@ -184,6 +185,11 @@ export default class LegacyCallHandler extends EventEmitter { } } + public isForcedSilent(): boolean { + const cli = MatrixClientPeg.get(); + return localNotificationsAreSilenced(cli); + } + public silenceCall(callId: string): void { this.silencedCalls.add(callId); this.emit(LegacyCallHandlerEvent.SilencedCallsChanged, this.silencedCalls); @@ -194,13 +200,14 @@ export default class LegacyCallHandler extends EventEmitter { } public unSilenceCall(callId: string): void { + if (this.isForcedSilent) return; this.silencedCalls.delete(callId); this.emit(LegacyCallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.play(AudioID.Ring); } public isCallSilenced(callId: string): boolean { - return this.silencedCalls.has(callId); + return this.isForcedSilent() || this.silencedCalls.has(callId); } /** @@ -582,7 +589,9 @@ export default class LegacyCallHandler extends EventEmitter { action.value === "ring" )); - if (pushRuleEnabled && tweakSetToRing) { + console.log('hhhh', { pushRuleEnabled, tweakSetToRing }); + + if (pushRuleEnabled && tweakSetToRing && !this.isForcedSilent()) { this.play(AudioID.Ring); } else { this.silenceCall(call.callId); diff --git a/src/toasts/IncomingLegacyCallToast.tsx b/src/toasts/IncomingLegacyCallToast.tsx index ee640411ed9..839d49f94ab 100644 --- a/src/toasts/IncomingLegacyCallToast.tsx +++ b/src/toasts/IncomingLegacyCallToast.tsx @@ -85,6 +85,7 @@ export default class IncomingLegacyCallToast extends React.Component diff --git a/test/LegacyCallHandler-test.ts b/test/LegacyCallHandler-test.ts index 8743c4cdf6d..34a2978fccd 100644 --- a/test/LegacyCallHandler-test.ts +++ b/test/LegacyCallHandler-test.ts @@ -14,10 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IProtocol } from 'matrix-js-sdk/src/matrix'; -import { CallEvent, CallState, CallType } from 'matrix-js-sdk/src/webrtc/call'; +import { + IProtocol, + LOCAL_NOTIFICATION_SETTINGS_PREFIX, + MatrixEvent, + PushRuleKind, + RuleId, + TweakName, +} from 'matrix-js-sdk/src/matrix'; +import { CallEvent, CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import EventEmitter from 'events'; import { mocked } from 'jest-mock'; +import { CallEventHandlerEvent } from 'matrix-js-sdk/src/webrtc/callEventHandler'; import LegacyCallHandler, { LegacyCallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL, @@ -28,6 +36,8 @@ import DMRoomMap from '../src/utils/DMRoomMap'; import SdkConfig from '../src/SdkConfig'; import { Action } from "../src/dispatcher/actions"; import { getFunctionalMembers } from "../src/utils/room/getFunctionalMembers"; +import SettingsStore from '../src/settings/SettingsStore'; +import { UIFeature } from '../src/settings/UIFeature'; jest.mock("../src/utils/room/getFunctionalMembers", () => ({ getFunctionalMembers: jest.fn(), @@ -126,6 +136,7 @@ describe('LegacyCallHandler', () => { // what addresses the app has looked up via pstn and native lookup let pstnLookup: string; let nativeLookup: string; + const deviceId = 'my-device'; beforeEach(async () => { stubClient(); @@ -136,6 +147,7 @@ describe('LegacyCallHandler', () => { fakeCall = new FakeCall(roomId); return fakeCall; }; + MatrixClientPeg.get().deviceId = deviceId; MatrixClientPeg.get().getThirdpartyProtocols = () => { return Promise.resolve({ @@ -426,4 +438,116 @@ describe('LegacyCallHandler without third party protocols', () => { // but it should appear to the user to be in thw native room for Bob expect(callHandler.roomIdForCall(fakeCall)).toEqual(NATIVE_ROOM_ALICE); }); + + it('should force calls to silent when local notifications are silenced', async () => { + jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockImplementation((eventType) => { + if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { + return new MatrixEvent({ + type: eventType, + content: { + is_silenced: true, + }, + }); + } + }); + + expect(callHandler.isForcedSilent()).toEqual(true); + }); + + fdescribe('incoming calls', () => { + const roomId = 'test-room-id'; + + const mockAudioElement = { + play: jest.fn(), + pause: jest.fn(), + } as unknown as HTMLMediaElement; + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(SettingsStore, 'getValue').mockImplementation(setting => + setting === UIFeature.Voip); + + jest.spyOn(MatrixClientPeg.get(), 'supportsVoip').mockReturnValue(true); + + MatrixClientPeg.get().isFallbackICEServerAllowed = jest.fn(); + MatrixClientPeg.get().prepareToEncrypt = jest.fn(); + + MatrixClientPeg.get().pushRules = { + global: { + [PushRuleKind.Override]: [{ + rule_id: RuleId.IncomingCall, + default: false, + enabled: true, + actions: [ + { + set_tweak: TweakName.Sound, + value: 'ring', + }, + ] + , + }], + }, + }; + + jest.spyOn(document, 'getElementById').mockReturnValue(mockAudioElement); + + jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockReturnValue(undefined); + }); + + it('listens for incoming call events when voip is enabled', () => { + const call = new MatrixCall({ + client: MatrixClientPeg.get(), + roomId, + }); + const cli = MatrixClientPeg.get(); + + cli.emit(CallEventHandlerEvent.Incoming, call); + + // call added to call map + expect(callHandler.getCallForRoom(roomId)).toEqual(call); + }); + + it('rings when incoming call state is ringing and notifications set to ring', () => { + const call = new MatrixCall({ + client: MatrixClientPeg.get(), + roomId, + }); + const cli = MatrixClientPeg.get(); + + cli.emit(CallEventHandlerEvent.Incoming, call); + + // call added to call map + expect(callHandler.getCallForRoom(roomId)).toEqual(call); + call.emit(CallEvent.State, CallState.Ringing, CallState.Connected); + + // ringer audio element started + expect(mockAudioElement.play).toHaveBeenCalled(); + }); + + it('does not ring when incoming call state is ringing but local notifications are silenced', () => { + jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockImplementation((eventType) => { + if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { + return new MatrixEvent({ + type: eventType, + content: { + is_silenced: true, + }, + }); + } + }); + const call = new MatrixCall({ + client: MatrixClientPeg.get(), + roomId, + }); + const cli = MatrixClientPeg.get(); + + cli.emit(CallEventHandlerEvent.Incoming, call); + + // call added to call map + expect(callHandler.getCallForRoom(roomId)).toEqual(call); + call.emit(CallEvent.State, CallState.Ringing, CallState.Connected); + + // ringer audio element started + expect(mockAudioElement.play).not.toHaveBeenCalled(); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 154f58e620d..ab66a618da5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3198,6 +3198,11 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== +browser-request@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/browser-request/-/browser-request-0.3.3.tgz#9ece5b5aca89a29932242e18bf933def9876cc17" + integrity sha512-YyNI4qJJ+piQG6MMEuo7J3Bzaqssufx04zpEKYfSrl/1Op59HWali9zMtBpXnkmqMcOuWJPZvudrm9wISmnCbg== + browserslist@^4.20.2, browserslist@^4.21.3: version "4.21.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a" From fd6b0831fb1d7f5736fb1ee79b38754060f4a606 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 14 Oct 2022 13:35:44 +0200 Subject: [PATCH 2/6] more coverage for silencing --- test/LegacyCallHandler-test.ts | 75 ++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/test/LegacyCallHandler-test.ts b/test/LegacyCallHandler-test.ts index 34a2978fccd..2fd774ae509 100644 --- a/test/LegacyCallHandler-test.ts +++ b/test/LegacyCallHandler-test.ts @@ -439,22 +439,7 @@ describe('LegacyCallHandler without third party protocols', () => { expect(callHandler.roomIdForCall(fakeCall)).toEqual(NATIVE_ROOM_ALICE); }); - it('should force calls to silent when local notifications are silenced', async () => { - jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockImplementation((eventType) => { - if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { - return new MatrixEvent({ - type: eventType, - content: { - is_silenced: true, - }, - }); - } - }); - - expect(callHandler.isForcedSilent()).toEqual(true); - }); - - fdescribe('incoming calls', () => { + describe('incoming calls', () => { const roomId = 'test-room-id'; const mockAudioElement = { @@ -490,7 +475,17 @@ describe('LegacyCallHandler without third party protocols', () => { jest.spyOn(document, 'getElementById').mockReturnValue(mockAudioElement); - jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockReturnValue(undefined); + // silence local notifications by default + jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockImplementation((eventType) => { + if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { + return new MatrixEvent({ + type: eventType, + content: { + is_silenced: true, + }, + }); + } + }); }); it('listens for incoming call events when voip is enabled', () => { @@ -507,6 +502,8 @@ describe('LegacyCallHandler without third party protocols', () => { }); it('rings when incoming call state is ringing and notifications set to ring', () => { + // remove local notification silencing mock for this test + jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockReturnValue(undefined); const call = new MatrixCall({ client: MatrixClientPeg.get(), roomId, @@ -524,16 +521,6 @@ describe('LegacyCallHandler without third party protocols', () => { }); it('does not ring when incoming call state is ringing but local notifications are silenced', () => { - jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockImplementation((eventType) => { - if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { - return new MatrixEvent({ - type: eventType, - content: { - is_silenced: true, - }, - }); - } - }); const call = new MatrixCall({ client: MatrixClientPeg.get(), roomId, @@ -548,6 +535,40 @@ describe('LegacyCallHandler without third party protocols', () => { // ringer audio element started expect(mockAudioElement.play).not.toHaveBeenCalled(); + expect(callHandler.isCallSilenced(call.callId)).toEqual(true); + }); + + it('should force calls to silent when local notifications are silenced', async () => { + const call = new MatrixCall({ + client: MatrixClientPeg.get(), + roomId, + }); + const cli = MatrixClientPeg.get(); + + cli.emit(CallEventHandlerEvent.Incoming, call); + + expect(callHandler.isForcedSilent()).toEqual(true); + expect(callHandler.isCallSilenced(call.callId)).toEqual(true); + }); + + it('does not unsilence calls when local notifications are silenced', async () => { + const call = new MatrixCall({ + client: MatrixClientPeg.get(), + roomId, + }); + const cli = MatrixClientPeg.get(); + const callHandlerEmitSpy = jest.spyOn(callHandler, 'emit'); + + cli.emit(CallEventHandlerEvent.Incoming, call); + // reset emit call count + callHandlerEmitSpy.mockClear(); + + callHandler.unSilenceCall(call.callId); + expect(callHandlerEmitSpy).not.toHaveBeenCalled(); + // call still silenced + expect(callHandler.isCallSilenced(call.callId)).toEqual(true); + // ringer not played + expect(mockAudioElement.play).not.toHaveBeenCalled(); }); }); }); From 1095cec52546d8ed9e108b663a380303b95ba095 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 14 Oct 2022 17:01:22 +0200 Subject: [PATCH 3/6] explain disabled silence button --- src/LegacyCallHandler.tsx | 2 -- src/i18n/strings/en_EN.json | 5 +++-- src/toasts/IncomingLegacyCallToast.tsx | 7 ++++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index c49a25c7e3f..41098dcb4db 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -589,8 +589,6 @@ export default class LegacyCallHandler extends EventEmitter { action.value === "ring" )); - console.log('hhhh', { pushRuleEnabled, tweakSetToRing }); - if (pushRuleEnabled && tweakSetToRing && !this.isForcedSilent()) { this.play(AudioID.Ring); } else { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d4ba636e360..b0ea381c7e8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -805,13 +805,14 @@ "Video call started": "Video call started", "Video": "Video", "Close": "Close", + "Sound on": "Sound on", + "Silence call": "Silence call", + "Notifications silenced": "Notifications silenced", "Unknown caller": "Unknown caller", "Voice call": "Voice call", "Video call": "Video call", "Decline": "Decline", "Accept": "Accept", - "Sound on": "Sound on", - "Silence call": "Silence call", "Use app for a better experience": "Use app for a better experience", "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.": "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.", "Use app": "Use app", diff --git a/src/toasts/IncomingLegacyCallToast.tsx b/src/toasts/IncomingLegacyCallToast.tsx index 839d49f94ab..1a61166277e 100644 --- a/src/toasts/IncomingLegacyCallToast.tsx +++ b/src/toasts/IncomingLegacyCallToast.tsx @@ -87,6 +87,11 @@ export default class IncomingLegacyCallToast extends React.Component ; } From 513806946a66a8535291ba802971c44aac1ec742 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 14 Oct 2022 17:04:40 +0200 Subject: [PATCH 4/6] lint --- src/toasts/IncomingLegacyCallToast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/toasts/IncomingLegacyCallToast.tsx b/src/toasts/IncomingLegacyCallToast.tsx index 1a61166277e..fec3fae1e9a 100644 --- a/src/toasts/IncomingLegacyCallToast.tsx +++ b/src/toasts/IncomingLegacyCallToast.tsx @@ -90,7 +90,7 @@ export default class IncomingLegacyCallToast extends React.Component Date: Fri, 14 Oct 2022 17:20:03 +0200 Subject: [PATCH 5/6] increase wait for modal --- test/components/views/settings/DevicesPanel-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/settings/DevicesPanel-test.tsx b/test/components/views/settings/DevicesPanel-test.tsx index ef9801adacc..a7baf139af3 100644 --- a/test/components/views/settings/DevicesPanel-test.tsx +++ b/test/components/views/settings/DevicesPanel-test.tsx @@ -197,7 +197,7 @@ describe('', () => { await flushPromises(); // modal rendering has some weird sleeps - await sleep(10); + await sleep(20); // close the modal without submission act(() => { From 01fa7dad31a35545c24b9339ba5b63086227b7cf Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 17 Oct 2022 10:56:22 +0200 Subject: [PATCH 6/6] more tests --- test/test-utils/client.ts | 2 + test/toasts/IncomingLegacyCallToast-test.tsx | 80 +++++++++++++++++++ .../IncomingLegacyCallToast-test.tsx.snap | 30 +++++++ 3 files changed, 112 insertions(+) create mode 100644 test/toasts/IncomingLegacyCallToast-test.tsx create mode 100644 test/toasts/__snapshots__/IncomingLegacyCallToast-test.tsx.snap diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 6478743458c..d3274c589a8 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -78,6 +78,7 @@ export const mockClientMethodsUser = (userId = '@alice:domain') => ({ getThreePids: jest.fn().mockResolvedValue({ threepids: [] }), getAccessToken: jest.fn(), getDeviceId: jest.fn(), + getAccountData: jest.fn(), }); /** @@ -103,6 +104,7 @@ export const mockClientMethodsServer = (): Partial', () => { + const userId = '@alice:server.org'; + const deviceId = 'my-device'; + + jest.spyOn(DMRoomMap, 'shared').mockReturnValue({ + getUserIdForRoomId: jest.fn(), + } as unknown as DMRoomMap); + + const mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsServer(), + getRoom: jest.fn(), + }); + const mockRoom = new Room('!room:server.org', mockClient, userId); + mockClient.deviceId = deviceId; + + const call = new MatrixCall({ client: mockClient }); + const defaultProps = { + call, + }; + const getComponent = (props = {}) => ; + + beforeEach(() => { + jest.clearAllMocks(); + mockClient.getAccountData.mockReturnValue(undefined); + mockClient.getRoom.mockReturnValue(mockRoom); + }); + + it('renders when silence button when call is not silenced', () => { + const { getByLabelText } = render(getComponent()); + expect(getByLabelText('Silence call')).toMatchSnapshot(); + }); + + it('renders sound on button when call is silenced', () => { + LegacyCallHandler.instance.silenceCall(call.callId); + const { getByLabelText } = render(getComponent()); + expect(getByLabelText('Sound on')).toMatchSnapshot(); + }); + + it('renders disabled silenced button when call is forced to silent', () => { + // silence local notifications -> force call ringer to silent + mockClient.getAccountData.mockImplementation((eventType) => { + if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { + return new MatrixEvent({ + type: eventType, + content: { + is_silenced: true, + }, + }); + } + }); + const { getByLabelText } = render(getComponent()); + expect(getByLabelText('Notifications silenced')).toMatchSnapshot(); + }); +}); diff --git a/test/toasts/__snapshots__/IncomingLegacyCallToast-test.tsx.snap b/test/toasts/__snapshots__/IncomingLegacyCallToast-test.tsx.snap new file mode 100644 index 00000000000..55252462bdf --- /dev/null +++ b/test/toasts/__snapshots__/IncomingLegacyCallToast-test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders disabled silenced button when call is forced to silent 1`] = ` +
+`; + +exports[` renders sound on button when call is silenced 1`] = ` +
+`; + +exports[` renders when silence button when call is not silenced 1`] = ` +
+`;