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

Device manager - eagerly create m.local_notification_settings events #9353

Merged
merged 10 commits into from
Oct 10, 2022
16 changes: 13 additions & 3 deletions src/Notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
import {
PermissionChanged as PermissionChangedEvent,
} from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";

import { MatrixClientPeg } from './MatrixClientPeg';
import { PosthogAnalytics } from "./PosthogAnalytics";
Expand All @@ -50,6 +51,7 @@ import { localNotificationsAreSilenced } from "./utils/notifications";
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
import ToastStore from "./stores/ToastStore";
import { ElementCall } from "./models/Call";
import { createLocalNotificationSettingsIfNeeded } from './utils/notifications';

/*
* Dispatches:
Expand Down Expand Up @@ -351,12 +353,20 @@ export const Notifier = {
return this.toolbarHidden;
},

onSyncStateChange: function(state: string) {
if (state === "SYNCING") {
onSyncStateChange: function(state: SyncState, prevState?: SyncState, data?: ISyncStateData) {
if (state === SyncState.Syncing) {
this.isSyncing = true;
} else if (state === "STOPPED" || state === "ERROR") {
} else if (state === SyncState.Stopped || state === SyncState.Error) {
this.isSyncing = false;
}

// wait for first non-cached sync to complete
if (
![SyncState.Stopped, SyncState.Error].includes(state) &&
!data?.fromCache
) {
createLocalNotificationSettingsIfNeeded(MatrixClientPeg.get());
}
},

onEvent: function(ev: MatrixEvent) {
Expand Down
28 changes: 27 additions & 1 deletion src/utils/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { MatrixClient } from "matrix-js-sdk/src/client";
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX } from "matrix-js-sdk/src/@types/event";
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
import { MatrixClient } from "matrix-js-sdk/src/client";

import SettingsStore from "../settings/SettingsStore";

export const deviceNotificationSettingsKeys = [
"notificationsEnabled",
"notificationBodyEnabled",
"audioNotificationsEnabled",
];

export function getLocalNotificationAccountDataEventType(deviceId: string): string {
return `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
}

export async function createLocalNotificationSettingsIfNeeded(cli: MatrixClient): Promise<void> {
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId);
const event = cli.getAccountData(eventType);
// New sessions will create an account data event to signify they support
// remote toggling of push notifications on this device. Default `is_silenced=true`
// For backwards compat purposes, older sessions will need to check settings value
// to determine what the state of `is_silenced`
if (!event) {
// If any of the above is true, we fall in the "backwards compat" case,
// and `is_silenced` will be set to `false`
const isSilenced = !deviceNotificationSettingsKeys.some(key => SettingsStore.getValue(key));

await cli.setAccountData(eventType, {
is_silenced: isSilenced,
});
}
}

export function localNotificationsAreSilenced(cli: MatrixClient): boolean {
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId);
const event = cli.getAccountData(eventType);
Expand Down
60 changes: 56 additions & 4 deletions test/Notifier-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { mocked, MockedObject } from "jest-mock";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SyncState } from "matrix-js-sdk/src/sync";

import BasePlatform from "../src/BasePlatform";
import { ElementCall } from "../src/models/Call";
import Notifier from "../src/Notifier";
import SettingsStore from "../src/settings/SettingsStore";
import ToastStore from "../src/stores/ToastStore";
import { getLocalNotificationAccountDataEventType } from "../src/utils/notifications";
import {
createLocalNotificationSettingsIfNeeded,
getLocalNotificationAccountDataEventType,
} from "../src/utils/notifications";
import { getMockClientWithEventEmitter, mkEvent, mkRoom, mockPlatformPeg } from "./test-utils";
import { IncomingCallToast } from "../src/toasts/IncomingCallToast";

jest.mock("../src/utils/notifications", () => ({
// @ts-ignore
...jest.requireActual("../src/utils/notifications"),
createLocalNotificationSettingsIfNeeded: jest.fn(),
}));

describe("Notifier", () => {
const roomId = "!room1:server";
const testEvent = mkEvent({
Expand Down Expand Up @@ -111,7 +121,7 @@ describe("Notifier", () => {
tweaks: {},
});

Notifier.onSyncStateChange("SYNCING");
Notifier.onSyncStateChange(SyncState.Syncing);
});

afterEach(() => {
Expand Down Expand Up @@ -169,4 +179,46 @@ describe("Notifier", () => {
expect(ToastStore.sharedInstance().addOrReplaceToast).not.toHaveBeenCalled();
});
});

describe('local notification settings', () => {
const createLocalNotificationSettingsIfNeededMock = mocked(createLocalNotificationSettingsIfNeeded);
let hasStartedNotiferBefore = false;
beforeEach(() => {
// notifier defines some listener functions in start
// and references them in stop
// so blows up if stopped before it was started
if (hasStartedNotiferBefore) {
Notifier.stop();
}
Notifier.start();
hasStartedNotiferBefore = true;
createLocalNotificationSettingsIfNeededMock.mockClear();
});

afterAll(() => {
Notifier.stop();
});

it('does not create local notifications event after a sync error', () => {
mockClient.emit(ClientEvent.Sync, SyncState.Error, SyncState.Syncing);
expect(createLocalNotificationSettingsIfNeededMock).not.toHaveBeenCalled();
});

it('does not create local notifications event after sync stops', () => {
mockClient.emit(ClientEvent.Sync, SyncState.Stopped, SyncState.Syncing);
expect(createLocalNotificationSettingsIfNeededMock).not.toHaveBeenCalled();
});

it('does not create local notifications event after a cached sync', () => {
mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing, {
fromCache: true,
});
expect(createLocalNotificationSettingsIfNeededMock).not.toHaveBeenCalled();
});

it('creates local notifications event after a non-cached sync', () => {
mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing, {});
expect(createLocalNotificationSettingsIfNeededMock).toHaveBeenCalled();
});
});
});
34 changes: 34 additions & 0 deletions test/utils/notifications-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { mocked } from "jest-mock";
import {
localNotificationsAreSilenced,
getLocalNotificationAccountDataEventType,
createLocalNotificationSettingsIfNeeded,
deviceNotificationSettingsKeys,
} from "../../src/utils/notifications";
import SettingsStore from "../../src/settings/SettingsStore";
import { getMockClientWithEventEmitter } from "../test-utils/client";
Expand All @@ -46,6 +48,38 @@ describe('notifications', () => {
mocked(SettingsStore).getValue.mockReturnValue(false);
});

describe('createLocalNotification', () => {
it('creates account data event', async () => {
await createLocalNotificationSettingsIfNeeded(mockClient);
const event = mockClient.getAccountData(accountDataEventKey);
expect(event?.getContent().is_silenced).toBe(true);
});

it.each(deviceNotificationSettingsKeys)(
'unsilenced for existing sessions when %s setting is truthy',
async (settingKey) => {
mocked(SettingsStore)
.getValue
.mockImplementation((key) => {
return key === settingKey;
});

await createLocalNotificationSettingsIfNeeded(mockClient);
const event = mockClient.getAccountData(accountDataEventKey);
expect(event?.getContent().is_silenced).toBe(false);
});

it("does not override an existing account event data", async () => {
mockClient.setAccountData(accountDataEventKey, {
is_silenced: false,
});

await createLocalNotificationSettingsIfNeeded(mockClient);
const event = mockClient.getAccountData(accountDataEventKey);
expect(event?.getContent().is_silenced).toBe(false);
});
});

describe('localNotificationsAreSilenced', () => {
it('defaults to true when no setting exists', () => {
expect(localNotificationsAreSilenced(mockClient)).toBeTruthy();
Expand Down