Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 2d9f828

Browse files
Kerryt3chguy
andauthored
Device manager - silence call ringers when local notifications are silenced (#9420)
* silence call ringers when local notifications are silenced * more coverage for silencing * explain disabled silence button * lint * increase wait for modal Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
1 parent 1d18608 commit 2d9f828

File tree

8 files changed

+280
-8
lines changed

8 files changed

+280
-8
lines changed

src/LegacyCallHandler.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { KIND_CALL_TRANSFER } from "./components/views/dialogs/InviteDialogTypes
6262
import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload";
6363
import { findDMForUser } from './utils/dm/findDMForUser';
6464
import { getJoinedNonFunctionalMembers } from './utils/room/getJoinedNonFunctionalMembers';
65+
import { localNotificationsAreSilenced } from './utils/notifications';
6566

6667
export const PROTOCOL_PSTN = 'm.protocol.pstn';
6768
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
@@ -184,6 +185,11 @@ export default class LegacyCallHandler extends EventEmitter {
184185
}
185186
}
186187

188+
public isForcedSilent(): boolean {
189+
const cli = MatrixClientPeg.get();
190+
return localNotificationsAreSilenced(cli);
191+
}
192+
187193
public silenceCall(callId: string): void {
188194
this.silencedCalls.add(callId);
189195
this.emit(LegacyCallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
@@ -194,13 +200,14 @@ export default class LegacyCallHandler extends EventEmitter {
194200
}
195201

196202
public unSilenceCall(callId: string): void {
203+
if (this.isForcedSilent) return;
197204
this.silencedCalls.delete(callId);
198205
this.emit(LegacyCallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
199206
this.play(AudioID.Ring);
200207
}
201208

202209
public isCallSilenced(callId: string): boolean {
203-
return this.silencedCalls.has(callId);
210+
return this.isForcedSilent() || this.silencedCalls.has(callId);
204211
}
205212

206213
/**
@@ -582,7 +589,7 @@ export default class LegacyCallHandler extends EventEmitter {
582589
action.value === "ring"
583590
));
584591

585-
if (pushRuleEnabled && tweakSetToRing) {
592+
if (pushRuleEnabled && tweakSetToRing && !this.isForcedSilent()) {
586593
this.play(AudioID.Ring);
587594
} else {
588595
this.silenceCall(call.callId);

src/i18n/strings/en_EN.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -806,13 +806,14 @@
806806
"Video call started": "Video call started",
807807
"Video": "Video",
808808
"Close": "Close",
809+
"Sound on": "Sound on",
810+
"Silence call": "Silence call",
811+
"Notifications silenced": "Notifications silenced",
809812
"Unknown caller": "Unknown caller",
810813
"Voice call": "Voice call",
811814
"Video call": "Video call",
812815
"Decline": "Decline",
813816
"Accept": "Accept",
814-
"Sound on": "Sound on",
815-
"Silence call": "Silence call",
816817
"Use app for a better experience": "Use app for a better experience",
817818
"%(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.",
818819
"Use app": "Use app",

src/toasts/IncomingLegacyCallToast.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ export default class IncomingLegacyCallToast extends React.Component<IProps, ISt
8585
const call = this.props.call;
8686
const room = MatrixClientPeg.get().getRoom(LegacyCallHandler.instance.roomIdForCall(call));
8787
const isVoice = call.type === CallType.Voice;
88+
const callForcedSilent = LegacyCallHandler.instance.isForcedSilent();
89+
90+
let silenceButtonTooltip = this.state.silenced ? _t("Sound on") : _t("Silence call");
91+
if (callForcedSilent) {
92+
silenceButtonTooltip = _t("Notifications silenced");
93+
}
8894

8995
const contentClass = classNames("mx_IncomingLegacyCallToast_content", {
9096
"mx_IncomingLegacyCallToast_content_voice": isVoice,
@@ -128,8 +134,9 @@ export default class IncomingLegacyCallToast extends React.Component<IProps, ISt
128134
</div>
129135
<AccessibleTooltipButton
130136
className={silenceClass}
137+
disabled={callForcedSilent}
131138
onClick={this.onSilenceClick}
132-
title={this.state.silenced ? _t("Sound on") : _t("Silence call")}
139+
title={silenceButtonTooltip}
133140
/>
134141
</React.Fragment>;
135142
}

test/LegacyCallHandler-test.ts

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,18 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { IProtocol } from 'matrix-js-sdk/src/matrix';
18-
import { CallEvent, CallState, CallType } from 'matrix-js-sdk/src/webrtc/call';
17+
import {
18+
IProtocol,
19+
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
20+
MatrixEvent,
21+
PushRuleKind,
22+
RuleId,
23+
TweakName,
24+
} from 'matrix-js-sdk/src/matrix';
25+
import { CallEvent, CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
1926
import EventEmitter from 'events';
2027
import { mocked } from 'jest-mock';
28+
import { CallEventHandlerEvent } from 'matrix-js-sdk/src/webrtc/callEventHandler';
2129

2230
import LegacyCallHandler, {
2331
LegacyCallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL,
@@ -28,6 +36,8 @@ import DMRoomMap from '../src/utils/DMRoomMap';
2836
import SdkConfig from '../src/SdkConfig';
2937
import { Action } from "../src/dispatcher/actions";
3038
import { getFunctionalMembers } from "../src/utils/room/getFunctionalMembers";
39+
import SettingsStore from '../src/settings/SettingsStore';
40+
import { UIFeature } from '../src/settings/UIFeature';
3141

3242
jest.mock("../src/utils/room/getFunctionalMembers", () => ({
3343
getFunctionalMembers: jest.fn(),
@@ -126,6 +136,7 @@ describe('LegacyCallHandler', () => {
126136
// what addresses the app has looked up via pstn and native lookup
127137
let pstnLookup: string;
128138
let nativeLookup: string;
139+
const deviceId = 'my-device';
129140

130141
beforeEach(async () => {
131142
stubClient();
@@ -136,6 +147,7 @@ describe('LegacyCallHandler', () => {
136147
fakeCall = new FakeCall(roomId);
137148
return fakeCall;
138149
};
150+
MatrixClientPeg.get().deviceId = deviceId;
139151

140152
MatrixClientPeg.get().getThirdpartyProtocols = () => {
141153
return Promise.resolve({
@@ -426,4 +438,137 @@ describe('LegacyCallHandler without third party protocols', () => {
426438
// but it should appear to the user to be in thw native room for Bob
427439
expect(callHandler.roomIdForCall(fakeCall)).toEqual(NATIVE_ROOM_ALICE);
428440
});
441+
442+
describe('incoming calls', () => {
443+
const roomId = 'test-room-id';
444+
445+
const mockAudioElement = {
446+
play: jest.fn(),
447+
pause: jest.fn(),
448+
} as unknown as HTMLMediaElement;
449+
beforeEach(() => {
450+
jest.clearAllMocks();
451+
jest.spyOn(SettingsStore, 'getValue').mockImplementation(setting =>
452+
setting === UIFeature.Voip);
453+
454+
jest.spyOn(MatrixClientPeg.get(), 'supportsVoip').mockReturnValue(true);
455+
456+
MatrixClientPeg.get().isFallbackICEServerAllowed = jest.fn();
457+
MatrixClientPeg.get().prepareToEncrypt = jest.fn();
458+
459+
MatrixClientPeg.get().pushRules = {
460+
global: {
461+
[PushRuleKind.Override]: [{
462+
rule_id: RuleId.IncomingCall,
463+
default: false,
464+
enabled: true,
465+
actions: [
466+
{
467+
set_tweak: TweakName.Sound,
468+
value: 'ring',
469+
},
470+
]
471+
,
472+
}],
473+
},
474+
};
475+
476+
jest.spyOn(document, 'getElementById').mockReturnValue(mockAudioElement);
477+
478+
// silence local notifications by default
479+
jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockImplementation((eventType) => {
480+
if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
481+
return new MatrixEvent({
482+
type: eventType,
483+
content: {
484+
is_silenced: true,
485+
},
486+
});
487+
}
488+
});
489+
});
490+
491+
it('listens for incoming call events when voip is enabled', () => {
492+
const call = new MatrixCall({
493+
client: MatrixClientPeg.get(),
494+
roomId,
495+
});
496+
const cli = MatrixClientPeg.get();
497+
498+
cli.emit(CallEventHandlerEvent.Incoming, call);
499+
500+
// call added to call map
501+
expect(callHandler.getCallForRoom(roomId)).toEqual(call);
502+
});
503+
504+
it('rings when incoming call state is ringing and notifications set to ring', () => {
505+
// remove local notification silencing mock for this test
506+
jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockReturnValue(undefined);
507+
const call = new MatrixCall({
508+
client: MatrixClientPeg.get(),
509+
roomId,
510+
});
511+
const cli = MatrixClientPeg.get();
512+
513+
cli.emit(CallEventHandlerEvent.Incoming, call);
514+
515+
// call added to call map
516+
expect(callHandler.getCallForRoom(roomId)).toEqual(call);
517+
call.emit(CallEvent.State, CallState.Ringing, CallState.Connected);
518+
519+
// ringer audio element started
520+
expect(mockAudioElement.play).toHaveBeenCalled();
521+
});
522+
523+
it('does not ring when incoming call state is ringing but local notifications are silenced', () => {
524+
const call = new MatrixCall({
525+
client: MatrixClientPeg.get(),
526+
roomId,
527+
});
528+
const cli = MatrixClientPeg.get();
529+
530+
cli.emit(CallEventHandlerEvent.Incoming, call);
531+
532+
// call added to call map
533+
expect(callHandler.getCallForRoom(roomId)).toEqual(call);
534+
call.emit(CallEvent.State, CallState.Ringing, CallState.Connected);
535+
536+
// ringer audio element started
537+
expect(mockAudioElement.play).not.toHaveBeenCalled();
538+
expect(callHandler.isCallSilenced(call.callId)).toEqual(true);
539+
});
540+
541+
it('should force calls to silent when local notifications are silenced', async () => {
542+
const call = new MatrixCall({
543+
client: MatrixClientPeg.get(),
544+
roomId,
545+
});
546+
const cli = MatrixClientPeg.get();
547+
548+
cli.emit(CallEventHandlerEvent.Incoming, call);
549+
550+
expect(callHandler.isForcedSilent()).toEqual(true);
551+
expect(callHandler.isCallSilenced(call.callId)).toEqual(true);
552+
});
553+
554+
it('does not unsilence calls when local notifications are silenced', async () => {
555+
const call = new MatrixCall({
556+
client: MatrixClientPeg.get(),
557+
roomId,
558+
});
559+
const cli = MatrixClientPeg.get();
560+
const callHandlerEmitSpy = jest.spyOn(callHandler, 'emit');
561+
562+
cli.emit(CallEventHandlerEvent.Incoming, call);
563+
// reset emit call count
564+
callHandlerEmitSpy.mockClear();
565+
566+
callHandler.unSilenceCall(call.callId);
567+
expect(callHandlerEmitSpy).not.toHaveBeenCalled();
568+
// call still silenced
569+
expect(callHandler.isCallSilenced(call.callId)).toEqual(true);
570+
// ringer not played
571+
expect(mockAudioElement.play).not.toHaveBeenCalled();
572+
});
573+
});
429574
});

test/components/views/settings/DevicesPanel-test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ describe('<DevicesPanel />', () => {
197197

198198
await flushPromises();
199199
// modal rendering has some weird sleeps
200-
await sleep(10);
200+
await sleep(20);
201201

202202
// close the modal without submission
203203
act(() => {

test/test-utils/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export const mockClientMethodsUser = (userId = '@alice:domain') => ({
7878
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
7979
getAccessToken: jest.fn(),
8080
getDeviceId: jest.fn(),
81+
getAccountData: jest.fn(),
8182
});
8283

8384
/**
@@ -103,6 +104,7 @@ export const mockClientMethodsServer = (): Partial<Record<MethodKeysOf<MatrixCli
103104
getCapabilities: jest.fn().mockReturnValue({}),
104105
getClientWellKnown: jest.fn().mockReturnValue({}),
105106
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
107+
isFallbackICEServerAllowed: jest.fn(),
106108
});
107109

108110
export const mockClientMethodsDevice = (
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
import { render } from '@testing-library/react';
17+
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixEvent, Room } from 'matrix-js-sdk/src/matrix';
18+
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
19+
import React from 'react';
20+
21+
import LegacyCallHandler from '../../src/LegacyCallHandler';
22+
import IncomingLegacyCallToast from "../../src/toasts/IncomingLegacyCallToast";
23+
import DMRoomMap from '../../src/utils/DMRoomMap';
24+
import { getMockClientWithEventEmitter, mockClientMethodsServer, mockClientMethodsUser } from '../test-utils';
25+
26+
describe('<IncomingLegacyCallToast />', () => {
27+
const userId = '@alice:server.org';
28+
const deviceId = 'my-device';
29+
30+
jest.spyOn(DMRoomMap, 'shared').mockReturnValue({
31+
getUserIdForRoomId: jest.fn(),
32+
} as unknown as DMRoomMap);
33+
34+
const mockClient = getMockClientWithEventEmitter({
35+
...mockClientMethodsUser(userId),
36+
...mockClientMethodsServer(),
37+
getRoom: jest.fn(),
38+
});
39+
const mockRoom = new Room('!room:server.org', mockClient, userId);
40+
mockClient.deviceId = deviceId;
41+
42+
const call = new MatrixCall({ client: mockClient });
43+
const defaultProps = {
44+
call,
45+
};
46+
const getComponent = (props = {}) => <IncomingLegacyCallToast {...defaultProps} {...props} />;
47+
48+
beforeEach(() => {
49+
jest.clearAllMocks();
50+
mockClient.getAccountData.mockReturnValue(undefined);
51+
mockClient.getRoom.mockReturnValue(mockRoom);
52+
});
53+
54+
it('renders when silence button when call is not silenced', () => {
55+
const { getByLabelText } = render(getComponent());
56+
expect(getByLabelText('Silence call')).toMatchSnapshot();
57+
});
58+
59+
it('renders sound on button when call is silenced', () => {
60+
LegacyCallHandler.instance.silenceCall(call.callId);
61+
const { getByLabelText } = render(getComponent());
62+
expect(getByLabelText('Sound on')).toMatchSnapshot();
63+
});
64+
65+
it('renders disabled silenced button when call is forced to silent', () => {
66+
// silence local notifications -> force call ringer to silent
67+
mockClient.getAccountData.mockImplementation((eventType) => {
68+
if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
69+
return new MatrixEvent({
70+
type: eventType,
71+
content: {
72+
is_silenced: true,
73+
},
74+
});
75+
}
76+
});
77+
const { getByLabelText } = render(getComponent());
78+
expect(getByLabelText('Notifications silenced')).toMatchSnapshot();
79+
});
80+
});

0 commit comments

Comments
 (0)