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 @@ -806,13 +806,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
2 changes: 2 additions & 0 deletions test/test-utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const mockClientMethodsUser = (userId = '@alice:domain') => ({
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(),
getDeviceId: jest.fn(),
getAccountData: jest.fn(),
});

/**
Expand All @@ -103,6 +104,7 @@ export const mockClientMethodsServer = (): Partial<Record<MethodKeysOf<MatrixCli
getCapabilities: jest.fn().mockReturnValue({}),
getClientWellKnown: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
isFallbackICEServerAllowed: jest.fn(),
});

export const mockClientMethodsDevice = (
Expand Down
80 changes: 80 additions & 0 deletions test/toasts/IncomingLegacyCallToast-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { render } from '@testing-library/react';
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixEvent, Room } from 'matrix-js-sdk/src/matrix';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import React from 'react';

import LegacyCallHandler from '../../src/LegacyCallHandler';
import IncomingLegacyCallToast from "../../src/toasts/IncomingLegacyCallToast";
import DMRoomMap from '../../src/utils/DMRoomMap';
import { getMockClientWithEventEmitter, mockClientMethodsServer, mockClientMethodsUser } from '../test-utils';

describe('<IncomingLegacyCallToast />', () => {
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 = {}) => <IncomingLegacyCallToast {...defaultProps} {...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();
});
});
Loading