Make clear notifications work with threads (#9575)

This commit is contained in:
Germain 2022-11-15 10:27:13 +00:00 committed by GitHub
parent e66027cd0c
commit c10339ad68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 241 additions and 237 deletions

View file

@ -42,7 +42,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import TagComposer from "../elements/TagComposer"; import TagComposer from "../elements/TagComposer";
import { objectClone } from "../../../utils/objects"; import { objectClone } from "../../../utils/objects";
import { arrayDiff } from "../../../utils/arrays"; import { arrayDiff } from "../../../utils/arrays";
import { getLocalNotificationAccountDataEventType } from "../../../utils/notifications"; import { clearAllNotifications, getLocalNotificationAccountDataEventType } from "../../../utils/notifications";
// TODO: this "view" component still has far too much application logic in it, // TODO: this "view" component still has far too much application logic in it,
// which should be factored out to other files. // which should be factored out to other files.
@ -112,6 +112,8 @@ interface IState {
desktopNotifications: boolean; desktopNotifications: boolean;
desktopShowBody: boolean; desktopShowBody: boolean;
audioNotifications: boolean; audioNotifications: boolean;
clearingNotifications: boolean;
} }
export default class Notifications extends React.PureComponent<IProps, IState> { export default class Notifications extends React.PureComponent<IProps, IState> {
@ -126,6 +128,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
desktopNotifications: SettingsStore.getValue("notificationsEnabled"), desktopNotifications: SettingsStore.getValue("notificationsEnabled"),
desktopShowBody: SettingsStore.getValue("notificationBodyEnabled"), desktopShowBody: SettingsStore.getValue("notificationBodyEnabled"),
audioNotifications: SettingsStore.getValue("audioNotificationsEnabled"), audioNotifications: SettingsStore.getValue("audioNotificationsEnabled"),
clearingNotifications: false,
}; };
this.settingWatchers = [ this.settingWatchers = [
@ -177,8 +180,12 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
])).reduce((p, c) => Object.assign(c, p), {}); ])).reduce((p, c) => Object.assign(c, p), {});
this.setState<keyof Omit<IState, this.setState<keyof Omit<IState,
"deviceNotificationsEnabled" | "desktopNotifications" | "desktopShowBody" | "audioNotifications"> "deviceNotificationsEnabled" |
>({ "desktopNotifications" |
"desktopShowBody" |
"audioNotifications" |
"clearingNotifications"
>>({
...newState, ...newState,
phase: Phase.Ready, phase: Phase.Ready,
}); });
@ -433,17 +440,14 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
} }
}; };
private onClearNotificationsClicked = () => { private onClearNotificationsClicked = async (): Promise<void> => {
const client = MatrixClientPeg.get(); try {
client.getRooms().forEach(r => { this.setState({ clearingNotifications: true });
if (r.getUnreadNotificationCount() > 0) { const client = MatrixClientPeg.get();
const events = r.getLiveTimeline().getEvents(); await clearAllNotifications(client);
if (events.length) { } finally {
// noinspection JSIgnoredPromiseFromCall this.setState({ clearingNotifications: false });
client.sendReadReceipt(events[events.length - 1]); }
}
}
});
}; };
private async setKeywords(keywords: string[], originalRules: IAnnotatedPushRule[]) { private async setKeywords(keywords: string[], originalRules: IAnnotatedPushRule[]) {
@ -531,7 +535,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
private renderTopSection() { private renderTopSection() {
const masterSwitch = <LabelledToggleSwitch const masterSwitch = <LabelledToggleSwitch
data-test-id='notif-master-switch' data-testid='notif-master-switch'
value={!this.isInhibited} value={!this.isInhibited}
label={_t("Enable notifications for this account")} label={_t("Enable notifications for this account")}
caption={_t("Turn off to disable notifications on all your devices and sessions")} caption={_t("Turn off to disable notifications on all your devices and sessions")}
@ -546,7 +550,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
const emailSwitches = (this.state.threepids || []).filter(t => t.medium === ThreepidMedium.Email) const emailSwitches = (this.state.threepids || []).filter(t => t.medium === ThreepidMedium.Email)
.map(e => <LabelledToggleSwitch .map(e => <LabelledToggleSwitch
data-test-id='notif-email-switch' data-testid='notif-email-switch'
key={e.address} key={e.address}
value={this.state.pushers.some(p => p.kind === "email" && p.pushkey === e.address)} value={this.state.pushers.some(p => p.kind === "email" && p.pushkey === e.address)}
label={_t("Enable email notifications for %(email)s", { email: e.address })} label={_t("Enable email notifications for %(email)s", { email: e.address })}
@ -558,7 +562,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
{ masterSwitch } { masterSwitch }
<LabelledToggleSwitch <LabelledToggleSwitch
data-test-id='notif-device-switch' data-testid='notif-device-switch'
value={this.state.deviceNotificationsEnabled} value={this.state.deviceNotificationsEnabled}
label={_t("Enable notifications for this device")} label={_t("Enable notifications for this device")}
onChange={checked => this.updateDeviceNotifications(checked)} onChange={checked => this.updateDeviceNotifications(checked)}
@ -567,21 +571,21 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
{ this.state.deviceNotificationsEnabled && (<> { this.state.deviceNotificationsEnabled && (<>
<LabelledToggleSwitch <LabelledToggleSwitch
data-test-id='notif-setting-notificationsEnabled' data-testid='notif-setting-notificationsEnabled'
value={this.state.desktopNotifications} value={this.state.desktopNotifications}
onChange={this.onDesktopNotificationsChanged} onChange={this.onDesktopNotificationsChanged}
label={_t('Enable desktop notifications for this session')} label={_t('Enable desktop notifications for this session')}
disabled={this.state.phase === Phase.Persisting} disabled={this.state.phase === Phase.Persisting}
/> />
<LabelledToggleSwitch <LabelledToggleSwitch
data-test-id='notif-setting-notificationBodyEnabled' data-testid='notif-setting-notificationBodyEnabled'
value={this.state.desktopShowBody} value={this.state.desktopShowBody}
onChange={this.onDesktopShowBodyChanged} onChange={this.onDesktopShowBodyChanged}
label={_t('Show message in desktop notification')} label={_t('Show message in desktop notification')}
disabled={this.state.phase === Phase.Persisting} disabled={this.state.phase === Phase.Persisting}
/> />
<LabelledToggleSwitch <LabelledToggleSwitch
data-test-id='notif-setting-audioNotificationsEnabled' data-testid='notif-setting-audioNotificationsEnabled'
value={this.state.audioNotifications} value={this.state.audioNotifications}
onChange={this.onAudioNotificationsChanged} onChange={this.onAudioNotificationsChanged}
label={_t('Enable audible notifications for this session')} label={_t('Enable audible notifications for this session')}
@ -605,8 +609,10 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
) { ) {
clearNotifsButton = <AccessibleButton clearNotifsButton = <AccessibleButton
onClick={this.onClearNotificationsClicked} onClick={this.onClearNotificationsClicked}
disabled={this.state.clearingNotifications}
kind='danger' kind='danger'
className='mx_UserNotifSettings_clearNotifsButton' className='mx_UserNotifSettings_clearNotifsButton'
data-testid="clear-notifications"
>{ _t("Clear notifications") }</AccessibleButton>; >{ _t("Clear notifications") }</AccessibleButton>;
} }
@ -653,7 +659,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
const fieldsetRows = this.state.vectorPushRules[category].map(r => const fieldsetRows = this.state.vectorPushRules[category].map(r =>
<fieldset <fieldset
key={category + r.ruleId} key={category + r.ruleId}
data-test-id={category + r.ruleId} data-testid={category + r.ruleId}
className='mx_UserNotifSettings_gridRowContainer' className='mx_UserNotifSettings_gridRowContainer'
> >
<legend className='mx_UserNotifSettings_gridRowLabel'>{ r.description }</legend> <legend className='mx_UserNotifSettings_gridRowLabel'>{ r.description }</legend>
@ -678,7 +684,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
} }
return <> return <>
<div data-test-id={`notif-section-${category}`} className='mx_UserNotifSettings_grid'> <div data-testid={`notif-section-${category}`} className='mx_UserNotifSettings_grid'>
<span className='mx_UserNotifSettings_gridRowLabel mx_UserNotifSettings_gridRowHeading'>{ sectionName }</span> <span className='mx_UserNotifSettings_gridRowLabel mx_UserNotifSettings_gridRowHeading'>{ sectionName }</span>
<span className='mx_UserNotifSettings_gridColumnLabel'>{ VectorStateToLabel[VectorState.Off] }</span> <span className='mx_UserNotifSettings_gridColumnLabel'>{ VectorStateToLabel[VectorState.Off] }</span>
<span className='mx_UserNotifSettings_gridColumnLabel'>{ VectorStateToLabel[VectorState.On] }</span> <span className='mx_UserNotifSettings_gridColumnLabel'>{ VectorStateToLabel[VectorState.On] }</span>
@ -715,7 +721,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
// Ends up default centered // Ends up default centered
return <Spinner />; return <Spinner />;
} else if (this.state.phase === Phase.Error) { } else if (this.state.phase === Phase.Error) {
return <p data-test-id='error-message'>{ _t("There was an error loading your notification settings.") }</p>; return <p data-testid='error-message'>{ _t("There was an error loading your notification settings.") }</p>;
} }
return <div className='mx_UserNotifSettings'> return <div className='mx_UserNotifSettings'>

View file

@ -17,6 +17,8 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX } from "matrix-js-sdk/src/@types/event"; import { LOCAL_NOTIFICATION_SETTINGS_PREFIX } from "matrix-js-sdk/src/@types/event";
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications"; import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
import { Room } from "matrix-js-sdk/src/models/room";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
@ -56,3 +58,31 @@ export function localNotificationsAreSilenced(cli: MatrixClient): boolean {
const event = cli.getAccountData(eventType); const event = cli.getAccountData(eventType);
return event?.getContent<LocalNotificationSettings>()?.is_silenced ?? false; return event?.getContent<LocalNotificationSettings>()?.is_silenced ?? false;
} }
export function clearAllNotifications(client: MatrixClient): Promise<Array<{}>> {
const receiptPromises = client.getRooms().reduce((promises, room: Room) => {
if (room.getUnreadNotificationCount() > 0) {
const roomEvents = room.getLiveTimeline().getEvents();
const lastThreadEvents = room.lastThread?.events;
const lastRoomEvent = roomEvents?.[roomEvents?.length - 1];
const lastThreadLastEvent = lastThreadEvents?.[lastThreadEvents?.length - 1];
const lastEvent = (lastRoomEvent?.getTs() ?? 0) > (lastThreadLastEvent?.getTs() ?? 0)
? lastRoomEvent
: lastThreadLastEvent;
if (lastEvent) {
const receiptType = SettingsStore.getValue("sendReadReceipts", room.roomId)
? ReceiptType.Read
: ReceiptType.ReadPrivate;
const promise = client.sendReadReceipt(lastEvent, receiptType, true);
promises.push(promise);
}
}
return promises;
}, []);
return Promise.all(receiptPromises);
}

View file

@ -13,8 +13,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from 'enzyme';
import { import {
IPushRule, IPushRule,
IPushRules, IPushRules,
@ -22,14 +20,17 @@ import {
IPusher, IPusher,
LOCAL_NOTIFICATION_SETTINGS_PREFIX, LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MatrixEvent, MatrixEvent,
Room,
NotificationCountType,
} from 'matrix-js-sdk/src/matrix'; } from 'matrix-js-sdk/src/matrix';
import { IThreepid, ThreepidMedium } from 'matrix-js-sdk/src/@types/threepids'; import { IThreepid, ThreepidMedium } from 'matrix-js-sdk/src/@types/threepids';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { fireEvent, getByTestId, render, screen, waitFor } from '@testing-library/react';
import Notifications from '../../../../src/components/views/settings/Notifications'; import Notifications from '../../../../src/components/views/settings/Notifications';
import SettingsStore from "../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../src/settings/SettingsStore";
import { StandardActions } from '../../../../src/notifications/StandardActions'; import { StandardActions } from '../../../../src/notifications/StandardActions';
import { getMockClientWithEventEmitter } from '../../../test-utils'; import { getMockClientWithEventEmitter, mkMessage } from '../../../test-utils';
// don't pollute test output with error logs from mock rejections // don't pollute test output with error logs from mock rejections
jest.mock("matrix-js-sdk/src/logger"); jest.mock("matrix-js-sdk/src/logger");
@ -56,13 +57,12 @@ const pushRules: IPushRules = { "global": { "underride": [{ "conditions": [{ "ki
const flushPromises = async () => await new Promise(resolve => setTimeout(resolve)); const flushPromises = async () => await new Promise(resolve => setTimeout(resolve));
describe('<Notifications />', () => { describe('<Notifications />', () => {
const getComponent = () => mount(<Notifications />); const getComponent = () => render(<Notifications />);
// get component, wait for async data and force a render // get component, wait for async data and force a render
const getComponentAndWait = async () => { const getComponentAndWait = async () => {
const component = getComponent(); const component = getComponent();
await flushPromises(); await flushPromises();
component.setProps({});
return component; return component;
}; };
@ -85,11 +85,11 @@ describe('<Notifications />', () => {
} }
}), }),
setAccountData: jest.fn(), setAccountData: jest.fn(),
sendReadReceipt: jest.fn(),
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
}); });
mockClient.getPushRules.mockResolvedValue(pushRules); mockClient.getPushRules.mockResolvedValue(pushRules);
const findByTestId = (component, id) => component.find(`[data-test-id="${id}"]`);
beforeEach(() => { beforeEach(() => {
mockClient.getPushRules.mockClear().mockResolvedValue(pushRules); mockClient.getPushRules.mockClear().mockResolvedValue(pushRules);
mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] }); mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] });
@ -97,25 +97,25 @@ describe('<Notifications />', () => {
mockClient.setPusher.mockClear().mockResolvedValue({}); mockClient.setPusher.mockClear().mockResolvedValue({});
}); });
it('renders spinner while loading', () => { it('renders spinner while loading', async () => {
const component = getComponent(); getComponent();
expect(component.find('.mx_Spinner').length).toBeTruthy(); expect(screen.getByTestId('spinner')).toBeInTheDocument();
}); });
it('renders error message when fetching push rules fails', async () => { it('renders error message when fetching push rules fails', async () => {
mockClient.getPushRules.mockRejectedValue({}); mockClient.getPushRules.mockRejectedValue({});
const component = await getComponentAndWait(); await getComponentAndWait();
expect(findByTestId(component, 'error-message').length).toBeTruthy(); expect(screen.getByTestId('error-message')).toBeInTheDocument();
}); });
it('renders error message when fetching pushers fails', async () => { it('renders error message when fetching pushers fails', async () => {
mockClient.getPushers.mockRejectedValue({}); mockClient.getPushers.mockRejectedValue({});
const component = await getComponentAndWait(); await getComponentAndWait();
expect(findByTestId(component, 'error-message').length).toBeTruthy(); expect(screen.getByTestId('error-message')).toBeInTheDocument();
}); });
it('renders error message when fetching threepids fails', async () => { it('renders error message when fetching threepids fails', async () => {
mockClient.getThreePids.mockRejectedValue({}); mockClient.getThreePids.mockRejectedValue({});
const component = await getComponentAndWait(); await getComponentAndWait();
expect(findByTestId(component, 'error-message').length).toBeTruthy(); expect(screen.getByTestId('error-message')).toBeInTheDocument();
}); });
describe('main notification switches', () => { describe('main notification switches', () => {
@ -127,18 +127,18 @@ describe('<Notifications />', () => {
}, },
} as unknown as IPushRules; } as unknown as IPushRules;
mockClient.getPushRules.mockClear().mockResolvedValue(disableNotificationsPushRules); mockClient.getPushRules.mockClear().mockResolvedValue(disableNotificationsPushRules);
const component = await getComponentAndWait(); const { container } = await getComponentAndWait();
expect(component).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it('renders switches correctly', async () => { it('renders switches correctly', async () => {
const component = await getComponentAndWait(); await getComponentAndWait();
expect(findByTestId(component, 'notif-master-switch').length).toBeTruthy(); expect(screen.getByTestId('notif-master-switch')).toBeInTheDocument();
expect(findByTestId(component, 'notif-device-switch').length).toBeTruthy(); expect(screen.getByTestId('notif-device-switch')).toBeInTheDocument();
expect(findByTestId(component, 'notif-setting-notificationsEnabled').length).toBeTruthy(); expect(screen.getByTestId('notif-setting-notificationsEnabled')).toBeInTheDocument();
expect(findByTestId(component, 'notif-setting-notificationBodyEnabled').length).toBeTruthy(); expect(screen.getByTestId('notif-setting-notificationBodyEnabled')).toBeInTheDocument();
expect(findByTestId(component, 'notif-setting-audioNotificationsEnabled').length).toBeTruthy(); expect(screen.getByTestId('notif-setting-audioNotificationsEnabled')).toBeInTheDocument();
}); });
describe('email switches', () => { describe('email switches', () => {
@ -156,9 +156,8 @@ describe('<Notifications />', () => {
}); });
it('renders email switches correctly when email 3pids exist', async () => { it('renders email switches correctly when email 3pids exist', async () => {
const component = await getComponentAndWait(); await getComponentAndWait();
expect(screen.getByTestId('notif-email-switch')).toBeInTheDocument();
expect(findByTestId(component, 'notif-email-switch')).toMatchSnapshot();
}); });
it('renders email switches correctly when notifications are on for email', async () => { it('renders email switches correctly when notifications are on for email', async () => {
@ -167,19 +166,20 @@ describe('<Notifications />', () => {
{ kind: 'email', pushkey: testEmail } as unknown as IPusher, { kind: 'email', pushkey: testEmail } as unknown as IPusher,
], ],
}); });
const component = await getComponentAndWait(); await getComponentAndWait();
expect(findByTestId(component, 'notif-email-switch').props().value).toEqual(true); const emailSwitch = screen.getByTestId('notif-email-switch');
expect(emailSwitch.querySelector('[aria-checked="true"]')).toBeInTheDocument();
}); });
it('enables email notification when toggling on', async () => { it('enables email notification when toggling on', async () => {
const component = await getComponentAndWait(); await getComponentAndWait();
const emailToggle = findByTestId(component, 'notif-email-switch') const emailToggle = screen.getByTestId('notif-email-switch')
.find('div[role="switch"]'); .querySelector('div[role="switch"]');
await act(async () => { await act(async () => {
emailToggle.simulate('click'); fireEvent.click(emailToggle);
}); });
expect(mockClient.setPusher).toHaveBeenCalledWith(expect.objectContaining({ expect(mockClient.setPusher).toHaveBeenCalledWith(expect.objectContaining({
@ -194,32 +194,31 @@ describe('<Notifications />', () => {
it('displays error when pusher update fails', async () => { it('displays error when pusher update fails', async () => {
mockClient.setPusher.mockRejectedValue({}); mockClient.setPusher.mockRejectedValue({});
const component = await getComponentAndWait(); await getComponentAndWait();
const emailToggle = findByTestId(component, 'notif-email-switch') const emailToggle = screen.getByTestId('notif-email-switch')
.find('div[role="switch"]'); .querySelector('div[role="switch"]');
await act(async () => { await act(async () => {
emailToggle.simulate('click'); fireEvent.click(emailToggle);
}); });
// force render // force render
await flushPromises(); await flushPromises();
await component.setProps({});
expect(findByTestId(component, 'error-message').length).toBeTruthy(); expect(screen.getByTestId('error-message')).toBeInTheDocument();
}); });
it('enables email notification when toggling off', async () => { it('enables email notification when toggling off', async () => {
const testPusher = { kind: 'email', pushkey: 'tester@test.com' } as unknown as IPusher; const testPusher = { kind: 'email', pushkey: 'tester@test.com' } as unknown as IPusher;
mockClient.getPushers.mockResolvedValue({ pushers: [testPusher] }); mockClient.getPushers.mockResolvedValue({ pushers: [testPusher] });
const component = await getComponentAndWait(); await getComponentAndWait();
const emailToggle = findByTestId(component, 'notif-email-switch') const emailToggle = screen.getByTestId('notif-email-switch')
.find('div[role="switch"]'); .querySelector('div[role="switch"]');
await act(async () => { await act(async () => {
emailToggle.simulate('click'); fireEvent.click(emailToggle);
}); });
expect(mockClient.setPusher).toHaveBeenCalledWith({ expect(mockClient.setPusher).toHaveBeenCalledWith({
@ -229,67 +228,64 @@ describe('<Notifications />', () => {
}); });
it('toggles and sets settings correctly', async () => { it('toggles and sets settings correctly', async () => {
const component = await getComponentAndWait(); await getComponentAndWait();
let audioNotifsToggle: ReactWrapper; let audioNotifsToggle;
const update = () => { const update = () => {
audioNotifsToggle = findByTestId(component, 'notif-setting-audioNotificationsEnabled') audioNotifsToggle = screen.getByTestId('notif-setting-audioNotificationsEnabled')
.find('div[role="switch"]'); .querySelector('div[role="switch"]');
}; };
update(); update();
expect(audioNotifsToggle.getDOMNode<HTMLElement>().getAttribute("aria-checked")).toEqual("true"); expect(audioNotifsToggle.getAttribute("aria-checked")).toEqual("true");
expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(true); expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(true);
act(() => { audioNotifsToggle.simulate('click'); }); act(() => { fireEvent.click(audioNotifsToggle); });
update(); update();
expect(audioNotifsToggle.getDOMNode<HTMLElement>().getAttribute("aria-checked")).toEqual("false"); expect(audioNotifsToggle.getAttribute("aria-checked")).toEqual("false");
expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(false); expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(false);
}); });
}); });
describe('individual notification level settings', () => { describe('individual notification level settings', () => {
const getCheckedRadioForRule = (ruleEl) =>
ruleEl.find('input[type="radio"][checked=true]').props()['aria-label'];
it('renders categories correctly', async () => { it('renders categories correctly', async () => {
const component = await getComponentAndWait(); await getComponentAndWait();
expect(findByTestId(component, 'notif-section-vector_global').length).toBeTruthy(); expect(screen.getByTestId('notif-section-vector_global')).toBeInTheDocument();
expect(findByTestId(component, 'notif-section-vector_mentions').length).toBeTruthy(); expect(screen.getByTestId('notif-section-vector_mentions')).toBeInTheDocument();
expect(findByTestId(component, 'notif-section-vector_other').length).toBeTruthy(); expect(screen.getByTestId('notif-section-vector_other')).toBeInTheDocument();
}); });
it('renders radios correctly', async () => { it('renders radios correctly', async () => {
const component = await getComponentAndWait(); await getComponentAndWait();
const section = 'vector_global'; const section = 'vector_global';
const globalSection = findByTestId(component, `notif-section-${section}`); const globalSection = screen.getByTestId(`notif-section-${section}`);
// 4 notification rules with class 'global' // 4 notification rules with class 'global'
expect(globalSection.find('fieldset').length).toEqual(4); expect(globalSection.querySelectorAll('fieldset').length).toEqual(4);
// oneToOneRule is set to 'on' // oneToOneRule is set to 'on'
const oneToOneRuleElement = findByTestId(component, section + oneToOneRule.rule_id); const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
expect(getCheckedRadioForRule(oneToOneRuleElement)).toEqual('On'); expect(oneToOneRuleElement.querySelector("[aria-label='On']")).toBeInTheDocument();
// encryptedOneToOneRule is set to 'loud' // encryptedOneToOneRule is set to 'loud'
const encryptedOneToOneElement = findByTestId(component, section + encryptedOneToOneRule.rule_id); const encryptedOneToOneElement = screen.getByTestId(section + encryptedOneToOneRule.rule_id);
expect(getCheckedRadioForRule(encryptedOneToOneElement)).toEqual('Noisy'); expect(encryptedOneToOneElement.querySelector("[aria-label='Noisy']")).toBeInTheDocument();
// encryptedGroupRule is set to 'off' // encryptedGroupRule is set to 'off'
const encryptedGroupElement = findByTestId(component, section + encryptedGroupRule.rule_id); const encryptedGroupElement = screen.getByTestId(section + encryptedGroupRule.rule_id);
expect(getCheckedRadioForRule(encryptedGroupElement)).toEqual('Off'); expect(encryptedGroupElement.querySelector("[aria-label='Off']")).toBeInTheDocument();
}); });
it('updates notification level when changed', async () => { it('updates notification level when changed', async () => {
const component = await getComponentAndWait(); await getComponentAndWait();
const section = 'vector_global'; const section = 'vector_global';
// oneToOneRule is set to 'on' // oneToOneRule is set to 'on'
// and is kind: 'underride' // and is kind: 'underride'
const oneToOneRuleElement = findByTestId(component, section + oneToOneRule.rule_id); const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
await act(async () => { await act(async () => {
// toggle at 0 is 'off' const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]');
const offToggle = oneToOneRuleElement.find('input[type="radio"]').at(0); fireEvent.click(offToggle);
offToggle.simulate('change');
}); });
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith( expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith(
@ -300,4 +296,32 @@ describe('<Notifications />', () => {
'global', 'underride', oneToOneRule.rule_id, StandardActions.ACTION_DONT_NOTIFY); 'global', 'underride', oneToOneRule.rule_id, StandardActions.ACTION_DONT_NOTIFY);
}); });
}); });
describe("clear all notifications", () => {
it("clears all notifications", async () => {
const room = new Room("room123", mockClient, "@alice:example.org");
mockClient.getRooms.mockReset().mockReturnValue([room]);
const message = mkMessage({
event: true,
room: "room123",
user: "@alice:example.org",
ts: 1,
});
room.addLiveEvents([message]);
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
const { container } = await getComponentAndWait();
const clearNotificationEl = getByTestId(container, "clear-notifications");
fireEvent.click(clearNotificationEl);
expect(clearNotificationEl.className).toContain("mx_AccessibleButton_disabled");
expect(mockClient.sendReadReceipt).toHaveBeenCalled();
await waitFor(() => {
expect(clearNotificationEl.className).not.toContain("mx_AccessibleButton_disabled");
});
});
});
}); });

View file

@ -1,157 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Notifications /> main notification switches email switches renders email switches correctly when email 3pids exist 1`] = `
<LabelledToggleSwitch
data-test-id="notif-email-switch"
disabled={false}
key="tester@test.com"
label="Enable email notifications for tester@test.com"
onChange={[Function]}
value={false}
>
<div
className="mx_SettingsFlag"
>
<span
className="mx_SettingsFlag_label"
>
Enable email notifications for tester@test.com
</span>
<_default
checked={false}
disabled={false}
onChange={[Function]}
title="Enable email notifications for tester@test.com"
>
<AccessibleTooltipButton
aria-checked={false}
aria-disabled={false}
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
onClick={[Function]}
role="switch"
title="Enable email notifications for tester@test.com"
>
<AccessibleButton
aria-checked={false}
aria-disabled={false}
aria-label="Enable email notifications for tester@test.com"
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
element="div"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
role="switch"
tabIndex={0}
>
<div
aria-checked={false}
aria-disabled={false}
aria-label="Enable email notifications for tester@test.com"
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
role="switch"
tabIndex={0}
>
<div
className="mx_ToggleSwitch_ball"
/>
</div>
</AccessibleButton>
</AccessibleTooltipButton>
</_default>
</div>
</LabelledToggleSwitch>
`;
exports[`<Notifications /> main notification switches renders only enable notifications switch when notifications are disabled 1`] = ` exports[`<Notifications /> main notification switches renders only enable notifications switch when notifications are disabled 1`] = `
<Notifications> <div>
<div <div
className="mx_UserNotifSettings" class="mx_UserNotifSettings"
> >
<LabelledToggleSwitch <div
caption="Turn off to disable notifications on all your devices and sessions" class="mx_SettingsFlag"
data-test-id="notif-master-switch" data-testid="notif-master-switch"
disabled={false}
label="Enable notifications for this account"
onChange={[Function]}
value={false}
> >
<div <span
className="mx_SettingsFlag" class="mx_SettingsFlag_label"
> >
Enable notifications for this account
<br />
<span <span
className="mx_SettingsFlag_label" class="mx_Caption"
> >
Enable notifications for this account Turn off to disable notifications on all your devices and sessions
<br />
<Caption>
<span
className="mx_Caption"
>
Turn off to disable notifications on all your devices and sessions
</span>
</Caption>
</span> </span>
<_default </span>
checked={false} <div
disabled={false} aria-checked="false"
onChange={[Function]} aria-disabled="false"
title="Enable notifications for this account" aria-label="Enable notifications for this account"
> class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
<AccessibleTooltipButton role="switch"
aria-checked={false} tabindex="0"
aria-disabled={false} >
className="mx_ToggleSwitch mx_ToggleSwitch_enabled" <div
onClick={[Function]} class="mx_ToggleSwitch_ball"
role="switch" />
title="Enable notifications for this account"
>
<AccessibleButton
aria-checked={false}
aria-disabled={false}
aria-label="Enable notifications for this account"
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
element="div"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
role="switch"
tabIndex={0}
>
<div
aria-checked={false}
aria-disabled={false}
aria-label="Enable notifications for this account"
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
role="switch"
tabIndex={0}
>
<div
className="mx_ToggleSwitch_ball"
/>
</div>
</AccessibleButton>
</AccessibleTooltipButton>
</_default>
</div> </div>
</LabelledToggleSwitch> </div>
</div> </div>
</Notifications> </div>
`; `;

View file

@ -16,15 +16,21 @@ limitations under the License.
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
import { import {
localNotificationsAreSilenced, localNotificationsAreSilenced,
getLocalNotificationAccountDataEventType, getLocalNotificationAccountDataEventType,
createLocalNotificationSettingsIfNeeded, createLocalNotificationSettingsIfNeeded,
deviceNotificationSettingsKeys, deviceNotificationSettingsKeys,
clearAllNotifications,
} from "../../src/utils/notifications"; } from "../../src/utils/notifications";
import SettingsStore from "../../src/settings/SettingsStore"; import SettingsStore from "../../src/settings/SettingsStore";
import { getMockClientWithEventEmitter } from "../test-utils/client"; import { getMockClientWithEventEmitter } from "../test-utils/client";
import { mkMessage, stubClient } from "../test-utils/test-utils";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
jest.mock("../../src/settings/SettingsStore"); jest.mock("../../src/settings/SettingsStore");
@ -99,4 +105,61 @@ describe('notifications', () => {
expect(localNotificationsAreSilenced(mockClient)).toBeFalsy(); expect(localNotificationsAreSilenced(mockClient)).toBeFalsy();
}); });
}); });
describe("clearAllNotifications", () => {
let client: MatrixClient;
let room: Room;
let sendReadReceiptSpy;
const ROOM_ID = "123";
const USER_ID = "@bob:example.org";
beforeEach(() => {
stubClient();
client = mocked(MatrixClientPeg.get());
room = new Room(ROOM_ID, client, USER_ID);
sendReadReceiptSpy = jest.spyOn(client, "sendReadReceipt").mockResolvedValue({});
jest.spyOn(client, "getRooms").mockReturnValue([room]);
jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => {
return name === "sendReadReceipts";
});
});
it("does not send any requests if everything has been read", () => {
clearAllNotifications(client);
expect(sendReadReceiptSpy).not.toBeCalled();
});
it("sends unthreaded receipt requests", () => {
const message = mkMessage({
event: true,
room: ROOM_ID,
user: USER_ID,
ts: 1,
});
room.addLiveEvents([message]);
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
clearAllNotifications(client);
expect(sendReadReceiptSpy).toBeCalledWith(message, ReceiptType.Read, true);
});
it("sends private read receipts", () => {
const message = mkMessage({
event: true,
room: ROOM_ID,
user: USER_ID,
ts: 1,
});
room.addLiveEvents([message]);
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false);
clearAllNotifications(client);
expect(sendReadReceiptSpy).toBeCalledWith(message, ReceiptType.ReadPrivate, true);
});
});
}); });