diff --git a/src/Notifier.ts b/src/Notifier.ts index 875402d982..64f4a6547f 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -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"; @@ -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: @@ -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) { diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index f41edd24bb..7e8aff6d0b 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -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 { + 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); diff --git a/test/Notifier-test.ts b/test/Notifier-test.ts index 4bac0a5423..eb5d5c7fce 100644 --- a/test/Notifier-test.ts +++ b/test/Notifier-test.ts @@ -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({ @@ -111,7 +121,7 @@ describe("Notifier", () => { tweaks: {}, }); - Notifier.onSyncStateChange("SYNCING"); + Notifier.onSyncStateChange(SyncState.Syncing); }); afterEach(() => { @@ -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(); + }); + }); }); diff --git a/test/utils/notifications-test.ts b/test/utils/notifications-test.ts index b27a660ebf..ba134c1480 100644 --- a/test/utils/notifications-test.ts +++ b/test/utils/notifications-test.ts @@ -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"; @@ -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();