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>
pull/28217/head
Kerry 2022-10-17 11:16:04 +02:00 committed by GitHub
parent 1d1860842e
commit 2d9f828810
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 280 additions and 8 deletions

View File

@ -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,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);

View File

@ -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",

View File

@ -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,
@ -128,8 +134,9 @@ export default class IncomingLegacyCallToast extends React.Component<IProps, ISt
</div>
<AccessibleTooltipButton
className={silenceClass}
disabled={callForcedSilent}
onClick={this.onSilenceClick}
title={this.state.silenced ? _t("Sound on") : _t("Silence call")}
title={silenceButtonTooltip}
/>
</React.Fragment>;
}

View File

@ -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,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();
});
});
});

View File

@ -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(() => {

View File

@ -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<Record<MethodKeysOf<MatrixCli
getCapabilities: jest.fn().mockReturnValue({}),
getClientWellKnown: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
isFallbackICEServerAllowed: jest.fn(),
});
export const mockClientMethodsDevice = (

View File

@ -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();
});
});

View File

@ -0,0 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<IncomingLegacyCallToast /> renders disabled silenced button when call is forced to silent 1`] = `
<div
aria-disabled="true"
aria-label="Notifications silenced"
class="mx_AccessibleButton mx_IncomingLegacyCallToast_iconButton mx_IncomingLegacyCallToast_unSilence mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
/>
`;
exports[`<IncomingLegacyCallToast /> renders sound on button when call is silenced 1`] = `
<div
aria-label="Sound on"
class="mx_AccessibleButton mx_IncomingLegacyCallToast_iconButton mx_IncomingLegacyCallToast_unSilence"
role="button"
tabindex="0"
/>
`;
exports[`<IncomingLegacyCallToast /> renders when silence button when call is not silenced 1`] = `
<div
aria-label="Silence call"
class="mx_AccessibleButton mx_IncomingLegacyCallToast_iconButton mx_IncomingLegacyCallToast_silence"
role="button"
tabindex="0"
/>
`;