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

Device manager - silence call ringers when local notifications are silenced #9420

Merged
merged 11 commits into from
Oct 17, 2022
11 changes: 9 additions & 2 deletions src/LegacyCallHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -194,13 +200,14 @@ export default class LegacyCallHandler extends EventEmitter {
}

public unSilenceCall(callId: string): void {
if (this.isForcedSilent) return;
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
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);
}

/**
Expand Down Expand Up @@ -582,7 +589,7 @@ export default class LegacyCallHandler extends EventEmitter {
action.value === "ring"
));

if (pushRuleEnabled && tweakSetToRing) {
if (pushRuleEnabled && tweakSetToRing && !this.isForcedSilent()) {
this.play(AudioID.Ring);
} else {
this.silenceCall(call.callId);
Expand Down
5 changes: 3 additions & 2 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 8 additions & 1 deletion src/toasts/IncomingLegacyCallToast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ export default class IncomingLegacyCallToast extends React.Component<IProps, ISt
const call = this.props.call;
const room = MatrixClientPeg.get().getRoom(LegacyCallHandler.instance.roomIdForCall(call));
const isVoice = call.type === CallType.Voice;
const callForcedSilent = LegacyCallHandler.instance.isForcedSilent();

let silenceButtonTooltip = this.state.silenced ? _t("Sound on") : _t("Silence call");
if (callForcedSilent) {
silenceButtonTooltip = _t("Notifications silenced");
}

const contentClass = classNames("mx_IncomingLegacyCallToast_content", {
"mx_IncomingLegacyCallToast_content_voice": isVoice,
Expand Down Expand Up @@ -128,8 +134,9 @@ export default class IncomingLegacyCallToast extends React.Component<IProps, ISt
</div>
<AccessibleTooltipButton
className={silenceClass}
disabled={callForcedSilent}
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
onClick={this.onSilenceClick}
title={this.state.silenced ? _t("Sound on") : _t("Silence call")}
title={silenceButtonTooltip}
/>
</React.Fragment>;
}
Expand Down
149 changes: 147 additions & 2 deletions test/LegacyCallHandler-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -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();
Expand All @@ -136,6 +147,7 @@ describe('LegacyCallHandler', () => {
fakeCall = new FakeCall(roomId);
return fakeCall;
};
MatrixClientPeg.get().deviceId = deviceId;

MatrixClientPeg.get().getThirdpartyProtocols = () => {
return Promise.resolve({
Expand Down Expand Up @@ -426,4 +438,137 @@ 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);
});

describe('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);

// 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', () => {
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', () => {
// remove local notification silencing mock for this test
jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockReturnValue(undefined);
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', () => {
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();
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();
});
});
});
2 changes: 1 addition & 1 deletion test/components/views/settings/DevicesPanel-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe('<DevicesPanel />', () => {

await flushPromises();
// modal rendering has some weird sleeps
await sleep(10);
await sleep(20);

// close the modal without submission
act(() => {
Expand Down