Device manager - eagerly create m.local_notification_settings events (#9353)

* eagerly save m.local_notification_settings events

* unskip test

* create local notification settings after first non-cached sync
This commit is contained in:
Kerry 2022-10-10 16:18:38 +02:00 committed by GitHub
parent cf029c51dc
commit c795ada78c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 130 additions and 8 deletions

View file

@ -26,6 +26,7 @@ import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
import { import {
PermissionChanged as PermissionChangedEvent, PermissionChanged as PermissionChangedEvent,
} from "@matrix-org/analytics-events/types/typescript/PermissionChanged"; } from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
import { PosthogAnalytics } from "./PosthogAnalytics"; import { PosthogAnalytics } from "./PosthogAnalytics";
@ -50,6 +51,7 @@ import { localNotificationsAreSilenced } from "./utils/notifications";
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
import ToastStore from "./stores/ToastStore"; import ToastStore from "./stores/ToastStore";
import { ElementCall } from "./models/Call"; import { ElementCall } from "./models/Call";
import { createLocalNotificationSettingsIfNeeded } from './utils/notifications';
/* /*
* Dispatches: * Dispatches:
@ -351,12 +353,20 @@ export const Notifier = {
return this.toolbarHidden; return this.toolbarHidden;
}, },
onSyncStateChange: function(state: string) { onSyncStateChange: function(state: SyncState, prevState?: SyncState, data?: ISyncStateData) {
if (state === "SYNCING") { if (state === SyncState.Syncing) {
this.isSyncing = true; this.isSyncing = true;
} else if (state === "STOPPED" || state === "ERROR") { } else if (state === SyncState.Stopped || state === SyncState.Error) {
this.isSyncing = false; 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) { onEvent: function(ev: MatrixEvent) {

View file

@ -14,14 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { 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 { MatrixClient } from "matrix-js-sdk/src/client";
import SettingsStore from "../settings/SettingsStore";
export const deviceNotificationSettingsKeys = [
"notificationsEnabled",
"notificationBodyEnabled",
"audioNotificationsEnabled",
];
export function getLocalNotificationAccountDataEventType(deviceId: string): string { export function getLocalNotificationAccountDataEventType(deviceId: string): string {
return `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`; 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 { export function localNotificationsAreSilenced(cli: MatrixClient): boolean {
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId); const eventType = getLocalNotificationAccountDataEventType(cli.deviceId);
const event = cli.getAccountData(eventType); const event = cli.getAccountData(eventType);

View file

@ -14,20 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MockedObject } from "jest-mock"; import { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SyncState } from "matrix-js-sdk/src/sync";
import BasePlatform from "../src/BasePlatform"; import BasePlatform from "../src/BasePlatform";
import { ElementCall } from "../src/models/Call"; import { ElementCall } from "../src/models/Call";
import Notifier from "../src/Notifier"; import Notifier from "../src/Notifier";
import SettingsStore from "../src/settings/SettingsStore"; import SettingsStore from "../src/settings/SettingsStore";
import ToastStore from "../src/stores/ToastStore"; 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 { getMockClientWithEventEmitter, mkEvent, mkRoom, mockPlatformPeg } from "./test-utils";
import { IncomingCallToast } from "../src/toasts/IncomingCallToast"; import { IncomingCallToast } from "../src/toasts/IncomingCallToast";
jest.mock("../src/utils/notifications", () => ({
// @ts-ignore
...jest.requireActual("../src/utils/notifications"),
createLocalNotificationSettingsIfNeeded: jest.fn(),
}));
describe("Notifier", () => { describe("Notifier", () => {
const roomId = "!room1:server"; const roomId = "!room1:server";
const testEvent = mkEvent({ const testEvent = mkEvent({
@ -111,7 +121,7 @@ describe("Notifier", () => {
tweaks: {}, tweaks: {},
}); });
Notifier.onSyncStateChange("SYNCING"); Notifier.onSyncStateChange(SyncState.Syncing);
}); });
afterEach(() => { afterEach(() => {
@ -169,4 +179,46 @@ describe("Notifier", () => {
expect(ToastStore.sharedInstance().addOrReplaceToast).not.toHaveBeenCalled(); 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();
});
});
}); });

View file

@ -20,6 +20,8 @@ import { mocked } from "jest-mock";
import { import {
localNotificationsAreSilenced, localNotificationsAreSilenced,
getLocalNotificationAccountDataEventType, getLocalNotificationAccountDataEventType,
createLocalNotificationSettingsIfNeeded,
deviceNotificationSettingsKeys,
} 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";
@ -46,6 +48,38 @@ describe('notifications', () => {
mocked(SettingsStore).getValue.mockReturnValue(false); 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', () => { describe('localNotificationsAreSilenced', () => {
it('defaults to true when no setting exists', () => { it('defaults to true when no setting exists', () => {
expect(localNotificationsAreSilenced(mockClient)).toBeTruthy(); expect(localNotificationsAreSilenced(mockClient)).toBeTruthy();