From 97765613bc7bf29608d6f4c4c713d90aef73eadd Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Sat, 17 Jun 2023 02:17:51 +0200 Subject: [PATCH] Implement new model, hooks and reconcilation code for new GYU notification settings (#11089) * Define new notification settings model * Add new hooks * make ts-strict happy * add unit tests * chore: make eslint/prettier happier :) * make ts-strict happier * Update src/notifications/NotificationUtils.ts Co-authored-by: Robin * Add tests for hooks * chore: fixed lint issues * Add comments --------- Co-authored-by: Robin --- src/hooks/useAsyncRefreshMemo.ts | 48 ++ src/hooks/useNotificationSettings.tsx | 81 +++ src/hooks/usePushers.ts | 23 + src/hooks/useThreepids.ts | 24 + .../NotificationSettings.ts | 67 +++ .../notificationsettings/PushRuleDiff.ts | 35 ++ .../notificationsettings/PushRuleMap.ts | 33 + .../reconcileNotificationSettings.ts | 228 +++++++ .../toNotificationSettings.ts | 95 +++ src/notifications/NotificationUtils.ts | 11 +- test/hooks/useNotificationSettings-test.tsx | 166 +++++ .../NotificationSettings-test.ts | 160 +++++ .../pushrules_default.json | 423 +++++++++++++ .../pushrules_default_new.json | 429 +++++++++++++ .../pushrules_sample.json | 566 ++++++++++++++++++ 15 files changed, 2383 insertions(+), 6 deletions(-) create mode 100644 src/hooks/useAsyncRefreshMemo.ts create mode 100644 src/hooks/useNotificationSettings.tsx create mode 100644 src/hooks/usePushers.ts create mode 100644 src/hooks/useThreepids.ts create mode 100644 src/models/notificationsettings/NotificationSettings.ts create mode 100644 src/models/notificationsettings/PushRuleDiff.ts create mode 100644 src/models/notificationsettings/PushRuleMap.ts create mode 100644 src/models/notificationsettings/reconcileNotificationSettings.ts create mode 100644 src/models/notificationsettings/toNotificationSettings.ts create mode 100644 test/hooks/useNotificationSettings-test.tsx create mode 100644 test/models/notificationsettings/NotificationSettings-test.ts create mode 100644 test/models/notificationsettings/pushrules_default.json create mode 100644 test/models/notificationsettings/pushrules_default_new.json create mode 100644 test/models/notificationsettings/pushrules_sample.json diff --git a/src/hooks/useAsyncRefreshMemo.ts b/src/hooks/useAsyncRefreshMemo.ts new file mode 100644 index 0000000000..31e4e21e45 --- /dev/null +++ b/src/hooks/useAsyncRefreshMemo.ts @@ -0,0 +1,48 @@ +/* +Copyright 2023 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 { DependencyList, useCallback, useEffect, useState } from "react"; + +type Fn = () => Promise; + +/** + * Works just like useMemo or our own useAsyncMemo, but additionally exposes a method to refresh the cached value + * as if the dependency had changed + * @param fn function to memoize + * @param deps React hooks dependencies for the function + * @param initialValue initial value + * @return tuple of cached value and refresh callback + */ +export function useAsyncRefreshMemo(fn: Fn, deps: DependencyList, initialValue: T): [T, () => void]; +export function useAsyncRefreshMemo(fn: Fn, deps: DependencyList, initialValue?: T): [T | undefined, () => void]; +export function useAsyncRefreshMemo(fn: Fn, deps: DependencyList, initialValue?: T): [T | undefined, () => void] { + const [value, setValue] = useState(initialValue); + const refresh = useCallback(() => { + let discard = false; + fn() + .then((v) => { + if (!discard) { + setValue(v); + } + }) + .catch((err) => console.error(err)); + return () => { + discard = true; + }; + }, deps); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(refresh, [refresh]); + return [value, refresh]; +} diff --git a/src/hooks/useNotificationSettings.tsx b/src/hooks/useNotificationSettings.tsx new file mode 100644 index 0000000000..b4174b4924 --- /dev/null +++ b/src/hooks/useNotificationSettings.tsx @@ -0,0 +1,81 @@ +/* +Copyright 2023 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 { IPushRules, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { NotificationSettings } from "../models/notificationsettings/NotificationSettings"; +import { PushRuleDiff } from "../models/notificationsettings/PushRuleDiff"; +import { reconcileNotificationSettings } from "../models/notificationsettings/reconcileNotificationSettings"; +import { toNotificationSettings } from "../models/notificationsettings/toNotificationSettings"; + +async function applyChanges(cli: MatrixClient, changes: PushRuleDiff): Promise { + await Promise.all(changes.deleted.map((change) => cli.deletePushRule("global", change.kind, change.rule_id))); + await Promise.all(changes.added.map((change) => cli.addPushRule("global", change.kind, change.rule_id, change))); + await Promise.all( + changes.updated.map(async (change) => { + if (change.enabled !== undefined) { + await cli.setPushRuleEnabled("global", change.kind, change.rule_id, change.enabled); + } + if (change.actions !== undefined) { + await cli.setPushRuleActions("global", change.kind, change.rule_id, change.actions); + } + }), + ); +} + +type UseNotificationSettings = { + model: NotificationSettings | null; + hasPendingChanges: boolean; + reconcile: (model: NotificationSettings) => void; +}; + +export function useNotificationSettings(cli: MatrixClient): UseNotificationSettings { + const supportsIntentionalMentions = useMemo(() => cli.supportsIntentionalMentions(), [cli]); + + const pushRules = useRef(null); + const [model, setModel] = useState(null); + const [hasPendingChanges, setPendingChanges] = useState(false); + const updatePushRules = useCallback(async () => { + const rules = await cli.getPushRules(); + const model = toNotificationSettings(rules, supportsIntentionalMentions); + const pendingChanges = reconcileNotificationSettings(rules, model, supportsIntentionalMentions); + pushRules.current = rules; + setPendingChanges( + pendingChanges.updated.length > 0 || pendingChanges.added.length > 0 || pendingChanges.deleted.length > 0, + ); + setModel(model); + }, [cli, supportsIntentionalMentions]); + + useEffect(() => { + updatePushRules().catch((err) => console.error(err)); + }, [cli, updatePushRules]); + + const reconcile = useCallback( + (model: NotificationSettings) => { + if (pushRules.current !== null) { + setModel(model); + const changes = reconcileNotificationSettings(pushRules.current, model, supportsIntentionalMentions); + applyChanges(cli, changes) + .then(updatePushRules) + .catch((err) => console.error(err)); + } + }, + [cli, updatePushRules, supportsIntentionalMentions], + ); + + return { model, hasPendingChanges, reconcile }; +} diff --git a/src/hooks/usePushers.ts b/src/hooks/usePushers.ts new file mode 100644 index 0000000000..2c6439f873 --- /dev/null +++ b/src/hooks/usePushers.ts @@ -0,0 +1,23 @@ +/* +Copyright 2023 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 { IPusher, MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { useAsyncRefreshMemo } from "./useAsyncRefreshMemo"; + +export function usePushers(client: MatrixClient): [IPusher[], () => void] { + return useAsyncRefreshMemo(() => client.getPushers().then((it) => it.pushers), [client], []); +} diff --git a/src/hooks/useThreepids.ts b/src/hooks/useThreepids.ts new file mode 100644 index 0000000000..b2762c0817 --- /dev/null +++ b/src/hooks/useThreepids.ts @@ -0,0 +1,24 @@ +/* +Copyright 2023 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 { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { IThreepid } from "matrix-js-sdk/src/@types/threepids"; + +import { useAsyncRefreshMemo } from "./useAsyncRefreshMemo"; + +export function useThreepids(client: MatrixClient): [IThreepid[], () => void] { + return useAsyncRefreshMemo(() => client.getThreePids().then((it) => it.threepids), [client], []); +} diff --git a/src/models/notificationsettings/NotificationSettings.ts b/src/models/notificationsettings/NotificationSettings.ts new file mode 100644 index 0000000000..4d396e6e5d --- /dev/null +++ b/src/models/notificationsettings/NotificationSettings.ts @@ -0,0 +1,67 @@ +/* +Copyright 2023 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 { RoomNotifState } from "../../RoomNotifs"; + +export type RoomDefaultNotificationLevel = RoomNotifState.AllMessages | RoomNotifState.MentionsOnly; + +export type NotificationSettings = { + globalMute: boolean; + defaultLevels: { + room: RoomDefaultNotificationLevel; + dm: RoomDefaultNotificationLevel; + }; + sound: { + people: string | undefined; + mentions: string | undefined; + calls: string | undefined; + }; + activity: { + invite: boolean; + status_event: boolean; + bot_notices: boolean; + }; + mentions: { + user: boolean; + keywords: boolean; + room: boolean; + }; + keywords: string[]; +}; + +export const DefaultNotificationSettings: NotificationSettings = { + globalMute: false, + defaultLevels: { + room: RoomNotifState.AllMessages, + dm: RoomNotifState.AllMessages, + }, + sound: { + people: "default", + mentions: "default", + calls: "ring", + }, + activity: { + invite: true, + status_event: false, + bot_notices: true, + }, + mentions: { + user: true, + room: true, + keywords: true, + }, + keywords: [], +}; diff --git a/src/models/notificationsettings/PushRuleDiff.ts b/src/models/notificationsettings/PushRuleDiff.ts new file mode 100644 index 0000000000..24917fd71e --- /dev/null +++ b/src/models/notificationsettings/PushRuleDiff.ts @@ -0,0 +1,35 @@ +/* +Copyright 2023 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 { IAnnotatedPushRule, PushRuleAction, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix"; + +export type PushRuleDiff = { + updated: PushRuleUpdate[]; + added: IAnnotatedPushRule[]; + deleted: PushRuleDeletion[]; +}; + +export type PushRuleDeletion = { + rule_id: RuleId | string; + kind: PushRuleKind; +}; + +export type PushRuleUpdate = { + rule_id: RuleId | string; + kind: PushRuleKind; + enabled?: boolean; + actions?: PushRuleAction[]; +}; diff --git a/src/models/notificationsettings/PushRuleMap.ts b/src/models/notificationsettings/PushRuleMap.ts new file mode 100644 index 0000000000..1e73a5fe4b --- /dev/null +++ b/src/models/notificationsettings/PushRuleMap.ts @@ -0,0 +1,33 @@ +/* +Copyright 2023 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 { IAnnotatedPushRule, IPushRules, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix"; + +export type PushRuleMap = Map; + +export function buildPushRuleMap(rulesets: IPushRules): PushRuleMap { + const rules = new Map(); + + for (const kind of Object.values(PushRuleKind)) { + for (const rule of rulesets.global[kind] ?? []) { + if (rule.rule_id.startsWith(".")) { + rules.set(rule.rule_id, { ...rule, kind }); + } + } + } + + return rules; +} diff --git a/src/models/notificationsettings/reconcileNotificationSettings.ts b/src/models/notificationsettings/reconcileNotificationSettings.ts new file mode 100644 index 0000000000..7fdd366e63 --- /dev/null +++ b/src/models/notificationsettings/reconcileNotificationSettings.ts @@ -0,0 +1,228 @@ +/* +Copyright 2023 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 { IPushRules, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix"; +import { deepCompare } from "matrix-js-sdk/src/utils"; + +import { NotificationUtils } from "../../notifications"; +import { StandardActions } from "../../notifications/StandardActions"; +import { RoomNotifState } from "../../RoomNotifs"; +import { NotificationSettings } from "./NotificationSettings"; +import { PushRuleDiff, PushRuleUpdate } from "./PushRuleDiff"; +import { buildPushRuleMap } from "./PushRuleMap"; + +function toStandardRules( + model: NotificationSettings, + supportsIntentionalMentions: boolean, +): Map { + const standardRules = new Map(); + + standardRules.set(RuleId.Master, { + rule_id: RuleId.Master, + kind: PushRuleKind.Override, + enabled: model.globalMute, + }); + + standardRules.set(RuleId.EncryptedMessage, { + rule_id: RuleId.EncryptedMessage, + kind: PushRuleKind.Underride, + enabled: true, + actions: NotificationUtils.encodeActions({ + notify: model.defaultLevels.room === RoomNotifState.AllMessages, + highlight: false, + }), + }); + standardRules.set(RuleId.Message, { + rule_id: RuleId.Message, + kind: PushRuleKind.Underride, + enabled: true, + actions: NotificationUtils.encodeActions({ + notify: model.defaultLevels.room === RoomNotifState.AllMessages, + highlight: false, + }), + }); + standardRules.set(RuleId.EncryptedDM, { + rule_id: RuleId.EncryptedDM, + kind: PushRuleKind.Underride, + enabled: true, + actions: NotificationUtils.encodeActions({ + notify: model.defaultLevels.dm === RoomNotifState.AllMessages, + highlight: false, + sound: model.sound.people, + }), + }); + standardRules.set(RuleId.DM, { + rule_id: RuleId.DM, + kind: PushRuleKind.Underride, + enabled: true, + actions: NotificationUtils.encodeActions({ + notify: model.defaultLevels.dm === RoomNotifState.AllMessages, + highlight: false, + sound: model.sound.people, + }), + }); + + standardRules.set(RuleId.SuppressNotices, { + rule_id: RuleId.SuppressNotices, + kind: PushRuleKind.Override, + enabled: !model.activity.bot_notices, + actions: StandardActions.ACTION_DONT_NOTIFY, + }); + standardRules.set(RuleId.InviteToSelf, { + rule_id: RuleId.InviteToSelf, + kind: PushRuleKind.Override, + enabled: model.activity.invite, + actions: NotificationUtils.encodeActions({ + notify: true, + highlight: false, + sound: model.sound.people, + }), + }); + standardRules.set(RuleId.MemberEvent, { + rule_id: RuleId.MemberEvent, + kind: PushRuleKind.Override, + enabled: true, + actions: model.activity.status_event ? StandardActions.ACTION_NOTIFY : StandardActions.ACTION_DONT_NOTIFY, + }); + + const mentionActions = NotificationUtils.encodeActions({ + notify: true, + sound: model.sound.mentions, + highlight: true, + }); + const userMentionActions = model.mentions.user ? mentionActions : StandardActions.ACTION_DONT_NOTIFY; + if (supportsIntentionalMentions) { + standardRules.set(RuleId.IsUserMention, { + rule_id: RuleId.IsUserMention, + kind: PushRuleKind.ContentSpecific, + enabled: true, + actions: userMentionActions, + }); + } + standardRules.set(RuleId.ContainsDisplayName, { + rule_id: RuleId.ContainsDisplayName, + kind: PushRuleKind.Override, + enabled: true, + actions: userMentionActions, + }); + standardRules.set(RuleId.ContainsUserName, { + rule_id: RuleId.ContainsUserName, + kind: PushRuleKind.ContentSpecific, + enabled: true, + actions: userMentionActions, + }); + + const roomMentionActions = model.mentions.room ? StandardActions.ACTION_NOTIFY : StandardActions.ACTION_DONT_NOTIFY; + if (supportsIntentionalMentions) { + standardRules.set(RuleId.IsRoomMention, { + rule_id: RuleId.IsRoomMention, + kind: PushRuleKind.ContentSpecific, + enabled: true, + actions: roomMentionActions, + }); + } + standardRules.set(RuleId.AtRoomNotification, { + rule_id: RuleId.AtRoomNotification, + kind: PushRuleKind.Override, + enabled: true, + actions: roomMentionActions, + }); + + standardRules.set(RuleId.Tombstone, { + rule_id: RuleId.Tombstone, + kind: PushRuleKind.Override, + enabled: model.activity.status_event, + actions: StandardActions.ACTION_HIGHLIGHT, + }); + + standardRules.set(RuleId.IncomingCall, { + rule_id: RuleId.IncomingCall, + kind: PushRuleKind.Underride, + enabled: true, + actions: NotificationUtils.encodeActions({ + notify: true, + sound: model.sound.calls, + }), + }); + + return standardRules; +} + +export function reconcileNotificationSettings( + pushRules: IPushRules, + model: NotificationSettings, + supportsIntentionalMentions: boolean, +): PushRuleDiff { + const changes: PushRuleDiff = { + updated: [], + added: [], + deleted: [], + }; + + const oldRules = buildPushRuleMap(pushRules); + const newRules = toStandardRules(model, supportsIntentionalMentions); + + for (const rule of newRules.values()) { + const original = oldRules.get(rule.rule_id); + let changed = false; + if (original === undefined) { + changed = true; + } else if (rule.enabled !== undefined && rule.enabled !== original.enabled) { + changed = true; + } else if (rule.actions !== undefined) { + const originalActions = NotificationUtils.decodeActions(original.actions); + const actions = NotificationUtils.decodeActions(rule.actions); + if (originalActions === null || actions === null) { + changed = true; + } else if (!deepCompare(actions, originalActions)) { + changed = true; + } + } + if (changed) { + changes.updated.push(rule); + } + } + + const contentRules = pushRules.global.content?.filter((rule) => !rule.rule_id.startsWith(".")) ?? []; + const newKeywords = new Set(model.keywords); + for (const rule of contentRules) { + if (!newKeywords.has(rule.pattern!)) { + changes.deleted.push({ + rule_id: rule.rule_id, + kind: PushRuleKind.ContentSpecific, + }); + } else if (rule.enabled !== model.mentions.keywords) { + changes.updated.push({ + rule_id: rule.rule_id, + kind: PushRuleKind.ContentSpecific, + enabled: model.mentions.keywords, + }); + } + newKeywords.delete(rule.pattern!); + } + for (const keyword of newKeywords) { + changes.added.push({ + rule_id: keyword, + kind: PushRuleKind.ContentSpecific, + default: false, + enabled: model.mentions.keywords, + pattern: keyword, + actions: StandardActions.ACTION_NOTIFY, + }); + } + + return changes; +} diff --git a/src/models/notificationsettings/toNotificationSettings.ts b/src/models/notificationsettings/toNotificationSettings.ts new file mode 100644 index 0000000000..cfb28718c4 --- /dev/null +++ b/src/models/notificationsettings/toNotificationSettings.ts @@ -0,0 +1,95 @@ +/* +Copyright 2023 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 { IPushRule, IPushRules, RuleId } from "matrix-js-sdk/src/matrix"; + +import { NotificationUtils } from "../../notifications"; +import { RoomNotifState } from "../../RoomNotifs"; +import { NotificationSettings } from "./NotificationSettings"; +import { buildPushRuleMap } from "./PushRuleMap"; + +function shouldNotify(rules: (IPushRule | null | undefined | false)[]): boolean { + if (rules.length === 0) { + return true; + } + for (const rule of rules) { + if (rule === null || rule === undefined || rule === false || !rule.enabled) { + continue; + } + const actions = NotificationUtils.decodeActions(rule.actions); + if (actions !== null && actions.notify) { + return true; + } + } + return false; +} + +function determineSound(rules: (IPushRule | null | undefined | false)[]): string | undefined { + for (const rule of rules) { + if (rule === null || rule === undefined || rule === false || !rule.enabled) { + continue; + } + const actions = NotificationUtils.decodeActions(rule.actions); + if (actions !== null && actions.notify && actions.sound !== undefined) { + return actions.sound; + } + } + return undefined; +} + +export function toNotificationSettings( + pushRules: IPushRules, + supportsIntentionalMentions: boolean, +): NotificationSettings { + const standardRules = buildPushRuleMap(pushRules); + const contentRules = pushRules.global.content?.filter((rule) => !rule.rule_id.startsWith(".")) ?? []; + const dmRules = [standardRules.get(RuleId.DM), standardRules.get(RuleId.EncryptedDM)]; + const roomRules = [standardRules.get(RuleId.Message), standardRules.get(RuleId.EncryptedMessage)]; + return { + globalMute: standardRules.get(RuleId.Master)?.enabled ?? false, + defaultLevels: { + room: shouldNotify(roomRules) ? RoomNotifState.AllMessages : RoomNotifState.MentionsOnly, + dm: shouldNotify(dmRules) ? RoomNotifState.AllMessages : RoomNotifState.MentionsOnly, + }, + sound: { + calls: determineSound([standardRules.get(RuleId.IncomingCall)]), + mentions: determineSound([ + supportsIntentionalMentions && standardRules.get(RuleId.IsUserMention), + standardRules.get(RuleId.ContainsUserName), + standardRules.get(RuleId.ContainsDisplayName), + ]), + people: determineSound(dmRules), + }, + activity: { + bot_notices: shouldNotify([standardRules.get(RuleId.SuppressNotices)]), + invite: shouldNotify([standardRules.get(RuleId.InviteToSelf)]), + status_event: shouldNotify([standardRules.get(RuleId.MemberEvent), standardRules.get(RuleId.Tombstone)]), + }, + mentions: { + user: shouldNotify([ + supportsIntentionalMentions && standardRules.get(RuleId.IsUserMention), + standardRules.get(RuleId.ContainsUserName), + standardRules.get(RuleId.ContainsDisplayName), + ]), + room: shouldNotify([ + supportsIntentionalMentions && standardRules.get(RuleId.IsRoomMention), + standardRules.get(RuleId.AtRoomNotification), + ]), + keywords: shouldNotify(contentRules), + }, + keywords: contentRules.map((it) => it.pattern!), + }; +} diff --git a/src/notifications/NotificationUtils.ts b/src/notifications/NotificationUtils.ts index 5eadcf81dc..7f0088afdd 100644 --- a/src/notifications/NotificationUtils.ts +++ b/src/notifications/NotificationUtils.ts @@ -16,7 +16,7 @@ limitations under the License. import { PushRuleAction, PushRuleActionName, TweakHighlight, TweakSound } from "matrix-js-sdk/src/@types/PushRules"; -interface IEncodedActions { +export interface PushRuleActions { notify: boolean; sound?: string; highlight?: boolean; @@ -29,7 +29,7 @@ export class NotificationUtils { // "highlight: true/false, // } // to a list of push actions. - public static encodeActions(action: IEncodedActions): PushRuleAction[] { + public static encodeActions(action: PushRuleActions): PushRuleAction[] { const notify = action.notify; const sound = action.sound; const highlight = action.highlight; @@ -55,13 +55,12 @@ export class NotificationUtils { // "highlight: true/false, // } // If the actions couldn't be decoded then returns null. - public static decodeActions(actions: PushRuleAction[]): IEncodedActions | null { + public static decodeActions(actions: PushRuleAction[]): PushRuleActions | null { let notify = false; let sound: string | undefined; let highlight: boolean | undefined = false; - for (let i = 0; i < actions.length; ++i) { - const action = actions[i]; + for (const action of actions) { if (action === PushRuleActionName.Notify) { notify = true; } else if (action === PushRuleActionName.DontNotify) { @@ -86,7 +85,7 @@ export class NotificationUtils { highlight = true; } - const result: IEncodedActions = { notify, highlight }; + const result: PushRuleActions = { notify, highlight }; if (sound !== undefined) { result.sound = sound; } diff --git a/test/hooks/useNotificationSettings-test.tsx b/test/hooks/useNotificationSettings-test.tsx new file mode 100644 index 0000000000..5b0e490426 --- /dev/null +++ b/test/hooks/useNotificationSettings-test.tsx @@ -0,0 +1,166 @@ +/* +Copyright 2023 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 { act } from "@testing-library/react"; +import { renderHook } from "@testing-library/react-hooks/dom"; +import { IPushRules, MatrixClient, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix"; + +import { useNotificationSettings } from "../../src/hooks/useNotificationSettings"; +import { MatrixClientPeg } from "../../src/MatrixClientPeg"; +import { + DefaultNotificationSettings, + NotificationSettings, +} from "../../src/models/notificationsettings/NotificationSettings"; +import { StandardActions } from "../../src/notifications/StandardActions"; +import { RoomNotifState } from "../../src/RoomNotifs"; +import { stubClient } from "../test-utils"; + +const expectedModel: NotificationSettings = { + globalMute: false, + defaultLevels: { + dm: RoomNotifState.AllMessages, + room: RoomNotifState.MentionsOnly, + }, + sound: { + calls: "ring", + mentions: "default", + people: undefined, + }, + activity: { + bot_notices: false, + invite: true, + status_event: false, + }, + mentions: { + user: true, + room: true, + keywords: true, + }, + keywords: ["justjann3", "justj4nn3", "justj4nne", "Janne", "J4nne", "Jann3", "jann3", "j4nne", "janne"], +}; + +describe("useNotificationSettings", () => { + let cli: MatrixClient; + let pushRules: IPushRules; + + beforeAll(async () => { + pushRules = (await import("../models/notificationsettings/pushrules_sample.json")) as IPushRules; + }); + + beforeEach(() => { + stubClient(); + cli = MatrixClientPeg.safeGet(); + cli.getPushRules = jest.fn(cli.getPushRules).mockResolvedValue(pushRules); + cli.supportsIntentionalMentions = jest.fn(cli.supportsIntentionalMentions).mockReturnValue(false); + }); + + it("correctly parses model", async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useNotificationSettings(cli)); + expect(result.current.model).toEqual(null); + await waitForNextUpdate(); + expect(result.current.model).toEqual(expectedModel); + expect(result.current.hasPendingChanges).toBeFalsy(); + }); + }); + + it("correctly generates change calls", async () => { + await act(async () => { + const addPushRule = jest.fn(cli.addPushRule); + cli.addPushRule = addPushRule; + const deletePushRule = jest.fn(cli.deletePushRule); + cli.deletePushRule = deletePushRule; + const setPushRuleEnabled = jest.fn(cli.setPushRuleEnabled); + cli.setPushRuleEnabled = setPushRuleEnabled; + const setPushRuleActions = jest.fn(cli.setPushRuleActions); + cli.setPushRuleActions = setPushRuleActions; + + const { result, waitForNextUpdate } = renderHook(() => useNotificationSettings(cli)); + expect(result.current.model).toEqual(null); + await waitForNextUpdate(); + expect(result.current.model).toEqual(expectedModel); + expect(result.current.hasPendingChanges).toBeFalsy(); + await result.current.reconcile(DefaultNotificationSettings); + await waitForNextUpdate(); + expect(result.current.hasPendingChanges).toBeFalsy(); + expect(addPushRule).toHaveBeenCalledTimes(0); + expect(deletePushRule).toHaveBeenCalledTimes(9); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justjann3"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nn3"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nne"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "Janne"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "J4nne"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "Jann3"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "jann3"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "j4nne"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "janne"); + expect(setPushRuleEnabled).toHaveBeenCalledTimes(6); + expect(setPushRuleEnabled).toHaveBeenCalledWith( + "global", + PushRuleKind.Underride, + RuleId.EncryptedMessage, + true, + ); + expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true); + expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.EncryptedDM, true); + expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, true); + expect(setPushRuleEnabled).toHaveBeenCalledWith( + "global", + PushRuleKind.Override, + RuleId.SuppressNotices, + false, + ); + expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.InviteToSelf, true); + expect(setPushRuleActions).toHaveBeenCalledTimes(6); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Underride, + RuleId.EncryptedMessage, + StandardActions.ACTION_NOTIFY, + ); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Underride, + RuleId.Message, + StandardActions.ACTION_NOTIFY, + ); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Underride, + RuleId.EncryptedDM, + StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + ); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Underride, + RuleId.DM, + StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + ); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Override, + RuleId.SuppressNotices, + StandardActions.ACTION_DONT_NOTIFY, + ); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Override, + RuleId.InviteToSelf, + StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + ); + }); + }); +}); diff --git a/test/models/notificationsettings/NotificationSettings-test.ts b/test/models/notificationsettings/NotificationSettings-test.ts new file mode 100644 index 0000000000..d2e2d7c876 --- /dev/null +++ b/test/models/notificationsettings/NotificationSettings-test.ts @@ -0,0 +1,160 @@ +/* +Copyright 2023 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 { IPushRules, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix"; + +import { + DefaultNotificationSettings, + NotificationSettings, +} from "../../../src/models/notificationsettings/NotificationSettings"; +import { reconcileNotificationSettings } from "../../../src/models/notificationsettings/reconcileNotificationSettings"; +import { toNotificationSettings } from "../../../src/models/notificationsettings/toNotificationSettings"; +import { StandardActions } from "../../../src/notifications/StandardActions"; +import { RoomNotifState } from "../../../src/RoomNotifs"; + +describe("NotificationSettings", () => { + it("parses a typical pushrules setup correctly", async () => { + const pushRules = (await import("./pushrules_sample.json")) as IPushRules; + const model = toNotificationSettings(pushRules, false); + const pendingChanges = reconcileNotificationSettings(pushRules, model, false); + const expectedModel: NotificationSettings = { + globalMute: false, + defaultLevels: { + dm: RoomNotifState.AllMessages, + room: RoomNotifState.MentionsOnly, + }, + sound: { + calls: "ring", + mentions: "default", + people: undefined, + }, + activity: { + bot_notices: false, + invite: true, + status_event: false, + }, + mentions: { + user: true, + room: true, + keywords: true, + }, + keywords: ["justjann3", "justj4nn3", "justj4nne", "Janne", "J4nne", "Jann3", "jann3", "j4nne", "janne"], + }; + expect(model).toEqual(expectedModel); + expect(pendingChanges.added).toHaveLength(0); + expect(pendingChanges.deleted).toHaveLength(0); + expect(pendingChanges.updated).toHaveLength(0); + }); + + it("generates correct mutations for a changed model", async () => { + const pushRules = (await import("./pushrules_sample.json")) as IPushRules; + const pendingChanges = reconcileNotificationSettings(pushRules, DefaultNotificationSettings, false); + expect(pendingChanges.added).toHaveLength(0); + expect(pendingChanges.deleted).toEqual([ + { kind: PushRuleKind.ContentSpecific, rule_id: "justjann3" }, + { kind: PushRuleKind.ContentSpecific, rule_id: "justj4nn3" }, + { kind: PushRuleKind.ContentSpecific, rule_id: "justj4nne" }, + { kind: PushRuleKind.ContentSpecific, rule_id: "Janne" }, + { kind: PushRuleKind.ContentSpecific, rule_id: "J4nne" }, + { kind: PushRuleKind.ContentSpecific, rule_id: "Jann3" }, + { kind: PushRuleKind.ContentSpecific, rule_id: "jann3" }, + { kind: PushRuleKind.ContentSpecific, rule_id: "j4nne" }, + { kind: PushRuleKind.ContentSpecific, rule_id: "janne" }, + ]); + expect(pendingChanges.updated).toEqual([ + { + kind: PushRuleKind.Underride, + rule_id: RuleId.EncryptedMessage, + enabled: true, + actions: StandardActions.ACTION_NOTIFY, + }, + { + kind: PushRuleKind.Underride, + rule_id: RuleId.Message, + enabled: true, + actions: StandardActions.ACTION_NOTIFY, + }, + { + kind: PushRuleKind.Underride, + rule_id: RuleId.EncryptedDM, + enabled: true, + actions: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + }, + { + kind: PushRuleKind.Underride, + rule_id: RuleId.DM, + enabled: true, + actions: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + }, + { + kind: PushRuleKind.Override, + rule_id: RuleId.SuppressNotices, + enabled: false, + actions: StandardActions.ACTION_DONT_NOTIFY, + }, + { + kind: PushRuleKind.Override, + rule_id: RuleId.InviteToSelf, + enabled: true, + actions: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + }, + ]); + }); + + it("correctly migrates old settings to the new model", async () => { + const pushRules = (await import("./pushrules_default.json")) as IPushRules; + const newPushRules = (await import("./pushrules_default_new.json")) as IPushRules; + const model = toNotificationSettings(pushRules, false); + const expectedModel: NotificationSettings = { + globalMute: false, + defaultLevels: { + dm: RoomNotifState.AllMessages, + room: RoomNotifState.MentionsOnly, + }, + sound: { + calls: "ring", + mentions: "default", + people: "default", + }, + activity: { + bot_notices: false, + invite: true, + status_event: true, + }, + mentions: { + user: true, + room: true, + keywords: true, + }, + keywords: [], + }; + expect(model).toEqual(expectedModel); + const pendingChanges = reconcileNotificationSettings(pushRules, model, false); + expect(pendingChanges.added).toHaveLength(0); + expect(pendingChanges.updated).toEqual([ + { + kind: PushRuleKind.Override, + rule_id: RuleId.MemberEvent, + enabled: true, + actions: StandardActions.ACTION_NOTIFY, + }, + ]); + const roundtripPendingChanges = reconcileNotificationSettings(newPushRules, model, false); + expect(roundtripPendingChanges.added).toHaveLength(0); + expect(roundtripPendingChanges.deleted).toHaveLength(0); + expect(roundtripPendingChanges.updated).toHaveLength(0); + }); +}); diff --git a/test/models/notificationsettings/pushrules_default.json b/test/models/notificationsettings/pushrules_default.json new file mode 100644 index 0000000000..1f6252410a --- /dev/null +++ b/test/models/notificationsettings/pushrules_default.json @@ -0,0 +1,423 @@ +{ + "global": { + "underride": [ + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.call.invite" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "ring" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.call", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.message" + }, + { + "kind": "room_member_count", + "is": "2" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.room_one_to_one", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.encrypted" + }, + { + "kind": "room_member_count", + "is": "2" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.encrypted_room_one_to_one", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.message" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.message", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.encrypted" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.encrypted", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "im.vector.modular.widgets" + }, + { + "kind": "event_match", + "key": "content.type", + "pattern": "jitsi" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "*" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".im.vector.jitsi", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "rule_id": ".org.matrix.msc3914.rule.room.call", + "default": true, + "enabled": true, + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "org.matrix.msc3401.call" + }, + { + "kind": "call_started" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + } + ], + "kind": "underride" + } + ], + "sender": [], + "room": [], + "content": [ + { + "actions": [ + "notify", + { + "set_tweak": "highlight" + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "rule_id": ".m.rule.contains_user_name", + "default": true, + "pattern": "jannetestuser", + "enabled": true, + "kind": "content" + } + ], + "override": [ + { + "conditions": [], + "actions": ["dont_notify"], + "rule_id": ".m.rule.master", + "default": true, + "enabled": false, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "content.msgtype", + "pattern": "m.notice" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.suppress_notices", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.member" + }, + { + "kind": "event_match", + "key": "content.membership", + "pattern": "invite" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "@jannetestuser:beta.matrix.org" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "rule_id": ".m.rule.invite_for_me", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.member" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.member_event", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_property_contains", + "key": "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + "value": "@jannetestuser:beta.matrix.org" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight" + } + ], + "rule_id": ".org.matrix.msc3952.is_user_mention", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "contains_display_name" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight" + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "rule_id": ".m.rule.contains_display_name", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_property_is", + "key": "content.org\\.matrix\\.msc3952\\.mentions.room", + "value": true + }, + { + "kind": "sender_notification_permission", + "key": "room" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight" + } + ], + "rule_id": ".org.matrix.msc3952.is_room_mention", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "sender_notification_permission", + "key": "room" + }, + { + "kind": "event_match", + "key": "content.body", + "pattern": "@room" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.roomnotif", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.tombstone" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight" + } + ], + "rule_id": ".m.rule.tombstone", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.reaction" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.reaction", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.server_acl" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "" + } + ], + "actions": [], + "rule_id": ".m.rule.room.server_acl", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "rule_id": ".org.matrix.msc3786.rule.room.server_acl", + "default": true, + "enabled": true, + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.server_acl" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "" + } + ], + "actions": [], + "kind": "override" + } + ] + } +} diff --git a/test/models/notificationsettings/pushrules_default_new.json b/test/models/notificationsettings/pushrules_default_new.json new file mode 100644 index 0000000000..379e2d222d --- /dev/null +++ b/test/models/notificationsettings/pushrules_default_new.json @@ -0,0 +1,429 @@ +{ + "global": { + "underride": [ + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.call.invite" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "ring" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.call", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.message" + }, + { + "kind": "room_member_count", + "is": "2" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.room_one_to_one", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.encrypted" + }, + { + "kind": "room_member_count", + "is": "2" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.encrypted_room_one_to_one", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.message" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.message", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.encrypted" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.encrypted", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "im.vector.modular.widgets" + }, + { + "kind": "event_match", + "key": "content.type", + "pattern": "jitsi" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "*" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".im.vector.jitsi", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "rule_id": ".org.matrix.msc3914.rule.room.call", + "default": true, + "enabled": true, + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "org.matrix.msc3401.call" + }, + { + "kind": "call_started" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + } + ], + "kind": "underride" + } + ], + "sender": [], + "room": [], + "content": [ + { + "actions": [ + "notify", + { + "set_tweak": "highlight" + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "rule_id": ".m.rule.contains_user_name", + "default": true, + "pattern": "jannetestuser", + "enabled": true, + "kind": "content" + } + ], + "override": [ + { + "conditions": [], + "actions": ["dont_notify"], + "rule_id": ".m.rule.master", + "default": true, + "enabled": false, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "content.msgtype", + "pattern": "m.notice" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.suppress_notices", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.member" + }, + { + "kind": "event_match", + "key": "content.membership", + "pattern": "invite" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "@jannetestuser:beta.matrix.org" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.invite_for_me", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.member" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.member_event", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_property_contains", + "key": "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + "value": "@jannetestuser:beta.matrix.org" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight" + } + ], + "rule_id": ".org.matrix.msc3952.is_user_mention", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "contains_display_name" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight" + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "rule_id": ".m.rule.contains_display_name", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_property_is", + "key": "content.org\\.matrix\\.msc3952\\.mentions.room", + "value": true + }, + { + "kind": "sender_notification_permission", + "key": "room" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight" + } + ], + "rule_id": ".org.matrix.msc3952.is_room_mention", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "sender_notification_permission", + "key": "room" + }, + { + "kind": "event_match", + "key": "content.body", + "pattern": "@room" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.roomnotif", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.tombstone" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight" + } + ], + "rule_id": ".m.rule.tombstone", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.reaction" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.reaction", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.server_acl" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "" + } + ], + "actions": [], + "rule_id": ".m.rule.room.server_acl", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "rule_id": ".org.matrix.msc3786.rule.room.server_acl", + "default": true, + "enabled": true, + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.server_acl" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "" + } + ], + "actions": [], + "kind": "override" + } + ] + } +} diff --git a/test/models/notificationsettings/pushrules_sample.json b/test/models/notificationsettings/pushrules_sample.json new file mode 100644 index 0000000000..2c9f4b2af7 --- /dev/null +++ b/test/models/notificationsettings/pushrules_sample.json @@ -0,0 +1,566 @@ +{ + "global": { + "underride": [ + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.call.invite" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "ring" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.call", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.message" + }, + { + "kind": "room_member_count", + "is": "2" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.room_one_to_one", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.encrypted" + }, + { + "kind": "room_member_count", + "is": "2" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.encrypted_room_one_to_one", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.message" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.message", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.encrypted" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.encrypted", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "im.vector.modular.widgets" + }, + { + "kind": "event_match", + "key": "content.type", + "pattern": "jitsi" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "*" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".im.vector.jitsi", + "default": true, + "enabled": true, + "kind": "underride" + }, + { + "rule_id": ".org.matrix.msc3914.rule.room.call", + "default": true, + "enabled": true, + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "org.matrix.msc3401.call" + }, + { + "kind": "call_started" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + } + ], + "kind": "underride" + } + ], + "sender": [], + "room": [], + "content": [ + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "pattern": "justjann3", + "rule_id": "justjann3", + "default": false, + "enabled": true, + "kind": "content" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "pattern": "justj4nn3", + "rule_id": "justj4nn3", + "default": false, + "enabled": true, + "kind": "content" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "pattern": "justj4nne", + "rule_id": "justj4nne", + "default": false, + "enabled": true, + "kind": "content" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight", + "value": true + } + ], + "pattern": "Janne", + "rule_id": "Janne", + "default": false, + "enabled": true, + "kind": "content" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "pattern": "J4nne", + "rule_id": "J4nne", + "default": false, + "enabled": true, + "kind": "content" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "pattern": "Jann3", + "rule_id": "Jann3", + "default": false, + "enabled": true, + "kind": "content" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "pattern": "jann3", + "rule_id": "jann3", + "default": false, + "enabled": true, + "kind": "content" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "pattern": "j4nne", + "rule_id": "j4nne", + "default": false, + "enabled": true, + "kind": "content" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "pattern": "janne", + "rule_id": "janne", + "default": false, + "enabled": true, + "kind": "content" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "rule_id": ".m.rule.contains_user_name", + "default": true, + "pattern": "jannemk", + "enabled": true, + "kind": "content" + } + ], + "override": [ + { + "conditions": [], + "actions": ["dont_notify"], + "rule_id": ".m.rule.master", + "default": true, + "enabled": false, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "content.msgtype", + "pattern": "m.notice" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.suppress_notices", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.member" + }, + { + "kind": "event_match", + "key": "content.membership", + "pattern": "invite" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "@jannemk:element.io" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.invite_for_me", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.member" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.member_event", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "contains_display_name" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "rule_id": ".m.rule.contains_display_name", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "sender_notification_permission", + "key": "room" + }, + { + "kind": "event_match", + "key": "content.body", + "pattern": "@room" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "rule_id": ".m.rule.roomnotif", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.tombstone" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": true + } + ], + "rule_id": ".m.rule.tombstone", + "default": true, + "enabled": false, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.reaction" + } + ], + "actions": ["dont_notify"], + "rule_id": ".m.rule.reaction", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.server_acl" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "" + } + ], + "actions": [], + "rule_id": ".m.rule.room.server_acl", + "default": true, + "enabled": true, + "kind": "override" + }, + { + "rule_id": ".org.matrix.msc3952.is_user_mention", + "default": true, + "enabled": true, + "conditions": [ + { + "kind": "event_property_contains", + "key": "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + "value": "@jannemk:element.io" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight" + } + ], + "kind": "override" + }, + { + "rule_id": ".org.matrix.msc3952.is_room_mention", + "default": true, + "enabled": true, + "conditions": [ + { + "kind": "event_property_is", + "key": "content.org\\.matrix\\.msc3952\\.mentions.room", + "value": true + }, + { + "kind": "sender_notification_permission", + "key": "room" + } + ], + "actions": [ + "notify", + { + "set_tweak": "highlight" + } + ], + "kind": "override" + }, + { + "rule_id": ".org.matrix.msc3786.rule.room.server_acl", + "default": true, + "enabled": true, + "conditions": [ + { + "kind": "event_match", + "key": "type", + "pattern": "m.room.server_acl" + }, + { + "kind": "event_match", + "key": "state_key", + "pattern": "" + } + ], + "actions": [], + "kind": "override" + } + ] + } +}