mirror of https://github.com/vector-im/riot-web
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
parent
1d1860842e
commit
2d9f828810
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
/>
|
||||
`;
|
Loading…
Reference in New Issue