Polls push rules: synchronise poll rules with message rules (#10263)
* basic sync setup * formatting * get loudest value for synced rules * more types * test synced rules in notifications settings * type fixes * noimplicitany fixes * remove debug * tidyingpull/28788/head^2
parent
e5291c195d
commit
10a765472b
|
@ -15,10 +15,18 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { IAnnotatedPushRule, IPusher, PushRuleAction, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules";
|
import {
|
||||||
|
IAnnotatedPushRule,
|
||||||
|
IPusher,
|
||||||
|
PushRuleAction,
|
||||||
|
IPushRule,
|
||||||
|
PushRuleKind,
|
||||||
|
RuleId,
|
||||||
|
} from "matrix-js-sdk/src/@types/PushRules";
|
||||||
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
|
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
|
||||||
|
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
|
||||||
|
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
@ -92,6 +100,9 @@ interface IVectorPushRule {
|
||||||
rule?: IAnnotatedPushRule;
|
rule?: IAnnotatedPushRule;
|
||||||
description: TranslatedString | string;
|
description: TranslatedString | string;
|
||||||
vectorState: VectorState;
|
vectorState: VectorState;
|
||||||
|
// loudest vectorState of a rule and its synced rules
|
||||||
|
// undefined when rule has no synced rules
|
||||||
|
syncedVectorState?: VectorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
@ -115,9 +126,68 @@ interface IState {
|
||||||
|
|
||||||
clearingNotifications: boolean;
|
clearingNotifications: boolean;
|
||||||
}
|
}
|
||||||
|
const findInDefaultRules = (
|
||||||
|
ruleId: RuleId | string,
|
||||||
|
defaultRules: {
|
||||||
|
[k in RuleClass]: IAnnotatedPushRule[];
|
||||||
|
},
|
||||||
|
): IAnnotatedPushRule | undefined => {
|
||||||
|
for (const category in defaultRules) {
|
||||||
|
const rule: IAnnotatedPushRule | undefined = defaultRules[category as RuleClass].find(
|
||||||
|
(rule) => rule.rule_id === ruleId,
|
||||||
|
);
|
||||||
|
if (rule) {
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vector notification states ordered by loudness in ascending order
|
||||||
|
const OrderedVectorStates = [VectorState.Off, VectorState.On, VectorState.Loud];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the 'loudest' vector state assigned to a rule
|
||||||
|
* and it's synced rules
|
||||||
|
* If rules have fallen out of sync,
|
||||||
|
* the loudest rule can determine the display value
|
||||||
|
* @param defaultRules
|
||||||
|
* @param rule - parent rule
|
||||||
|
* @param definition - definition of parent rule
|
||||||
|
* @returns VectorState - the maximum/loudest state for the parent and synced rules
|
||||||
|
*/
|
||||||
|
const maximumVectorState = (
|
||||||
|
defaultRules: {
|
||||||
|
[k in RuleClass]: IAnnotatedPushRule[];
|
||||||
|
},
|
||||||
|
rule: IAnnotatedPushRule,
|
||||||
|
definition: VectorPushRuleDefinition,
|
||||||
|
): VectorState | undefined => {
|
||||||
|
if (!definition.syncedRuleIds?.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const vectorState = definition.syncedRuleIds.reduce<VectorState>((maxVectorState, ruleId) => {
|
||||||
|
// already set to maximum
|
||||||
|
if (maxVectorState === VectorState.Loud) {
|
||||||
|
return maxVectorState;
|
||||||
|
}
|
||||||
|
const syncedRule = findInDefaultRules(ruleId, defaultRules);
|
||||||
|
if (syncedRule) {
|
||||||
|
const syncedRuleVectorState = definition.ruleToVectorState(syncedRule);
|
||||||
|
// if syncedRule is 'louder' than current maximum
|
||||||
|
// set maximum to louder vectorState
|
||||||
|
if (OrderedVectorStates.indexOf(syncedRuleVectorState) > OrderedVectorStates.indexOf(maxVectorState)) {
|
||||||
|
return syncedRuleVectorState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxVectorState;
|
||||||
|
}, definition.ruleToVectorState(rule));
|
||||||
|
|
||||||
|
return vectorState;
|
||||||
|
};
|
||||||
|
|
||||||
export default class Notifications extends React.PureComponent<IProps, IState> {
|
export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
private settingWatchers: string[];
|
private settingWatchers: string[];
|
||||||
|
private pushProcessor: PushProcessor;
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -145,6 +215,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
this.setState({ audioNotifications: value as boolean }),
|
this.setState({ audioNotifications: value as boolean }),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
this.pushProcessor = new PushProcessor(MatrixClientPeg.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
private get isInhibited(): boolean {
|
private get isInhibited(): boolean {
|
||||||
|
@ -281,6 +353,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
ruleId: rule.rule_id,
|
ruleId: rule.rule_id,
|
||||||
rule,
|
rule,
|
||||||
vectorState,
|
vectorState,
|
||||||
|
syncedVectorState: maximumVectorState(defaultRules, rule, definition),
|
||||||
description: _t(definition.description),
|
description: _t(definition.description),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -388,6 +461,43 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
await SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, checked);
|
await SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private setPushRuleActions = async (
|
||||||
|
ruleId: IPushRule["rule_id"],
|
||||||
|
kind: PushRuleKind,
|
||||||
|
actions?: PushRuleAction[],
|
||||||
|
): Promise<void> => {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
if (!actions) {
|
||||||
|
await cli.setPushRuleEnabled("global", kind, ruleId, false);
|
||||||
|
} else {
|
||||||
|
await cli.setPushRuleActions("global", kind, ruleId, actions);
|
||||||
|
await cli.setPushRuleEnabled("global", kind, ruleId, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updated syncedRuleIds from rule definition
|
||||||
|
* If a rule does not exist it is ignored
|
||||||
|
* Synced rules are updated sequentially
|
||||||
|
* and stop at first error
|
||||||
|
*/
|
||||||
|
private updateSyncedRules = async (
|
||||||
|
syncedRuleIds: VectorPushRuleDefinition["syncedRuleIds"],
|
||||||
|
actions?: PushRuleAction[],
|
||||||
|
): Promise<void> => {
|
||||||
|
// get synced rules that exist for user
|
||||||
|
const syncedRules: ReturnType<PushProcessor["getPushRuleAndKindById"]>[] = syncedRuleIds
|
||||||
|
?.map((ruleId) => this.pushProcessor.getPushRuleAndKindById(ruleId))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (!syncedRules?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const { kind, rule: syncedRule } of syncedRules) {
|
||||||
|
await this.setPushRuleActions(syncedRule.rule_id, kind, actions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState): Promise<void> => {
|
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState): Promise<void> => {
|
||||||
this.setState({ phase: Phase.Persisting });
|
this.setState({ phase: Phase.Persisting });
|
||||||
|
|
||||||
|
@ -428,12 +538,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
} else {
|
} else {
|
||||||
const definition: VectorPushRuleDefinition = VectorPushRulesDefinitions[rule.ruleId];
|
const definition: VectorPushRuleDefinition = VectorPushRulesDefinitions[rule.ruleId];
|
||||||
const actions = definition.vectorStateToActions[checkedState];
|
const actions = definition.vectorStateToActions[checkedState];
|
||||||
if (!actions) {
|
await this.setPushRuleActions(rule.rule.rule_id, rule.rule.kind, actions);
|
||||||
await cli.setPushRuleEnabled("global", rule.rule.kind, rule.rule.rule_id, false);
|
await this.updateSyncedRules(definition.syncedRuleIds, actions);
|
||||||
} else {
|
|
||||||
await cli.setPushRuleActions("global", rule.rule.kind, rule.rule.rule_id, actions);
|
|
||||||
await cli.setPushRuleEnabled("global", rule.rule.kind, rule.rule.rule_id, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.refreshFromServer();
|
await this.refreshFromServer();
|
||||||
|
@ -684,7 +790,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
<StyledRadioButton
|
<StyledRadioButton
|
||||||
key={r.ruleId + s}
|
key={r.ruleId + s}
|
||||||
name={r.ruleId}
|
name={r.ruleId}
|
||||||
checked={r.vectorState === s}
|
checked={(r.syncedVectorState ?? r.vectorState) === s}
|
||||||
onChange={this.onRadioChecked.bind(this, r, s)}
|
onChange={this.onRadioChecked.bind(this, r, s)}
|
||||||
disabled={this.state.phase === Phase.Persisting}
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
aria-label={VectorStateToLabel[s]}
|
aria-label={VectorStateToLabel[s]}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IAnnotatedPushRule, PushRuleAction } from "matrix-js-sdk/src/@types/PushRules";
|
import { IAnnotatedPushRule, PushRuleAction, RuleId } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { _td } from "../languageHandler";
|
import { _td } from "../languageHandler";
|
||||||
|
@ -29,15 +29,22 @@ type StateToActionsMap = {
|
||||||
interface IVectorPushRuleDefinition {
|
interface IVectorPushRuleDefinition {
|
||||||
description: string;
|
description: string;
|
||||||
vectorStateToActions: StateToActionsMap;
|
vectorStateToActions: StateToActionsMap;
|
||||||
|
/**
|
||||||
|
* Rules that should be updated to be kept in sync
|
||||||
|
* when this rule changes
|
||||||
|
*/
|
||||||
|
syncedRuleIds?: (RuleId | string)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class VectorPushRuleDefinition {
|
class VectorPushRuleDefinition {
|
||||||
public readonly description: string;
|
public readonly description: string;
|
||||||
public readonly vectorStateToActions: StateToActionsMap;
|
public readonly vectorStateToActions: StateToActionsMap;
|
||||||
|
public readonly syncedRuleIds?: (RuleId | string)[];
|
||||||
|
|
||||||
public constructor(opts: IVectorPushRuleDefinition) {
|
public constructor(opts: IVectorPushRuleDefinition) {
|
||||||
this.description = opts.description;
|
this.description = opts.description;
|
||||||
this.vectorStateToActions = opts.vectorStateToActions;
|
this.vectorStateToActions = opts.vectorStateToActions;
|
||||||
|
this.syncedRuleIds = opts.syncedRuleIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate the rule actions and its enabled value into vector state
|
// Translate the rule actions and its enabled value into vector state
|
||||||
|
@ -125,6 +132,12 @@ export const VectorPushRulesDefinitions: Record<string, VectorPushRuleDefinition
|
||||||
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
|
syncedRuleIds: [
|
||||||
|
RuleId.PollStartOneToOne,
|
||||||
|
RuleId.PollStartOneToOneUnstable,
|
||||||
|
RuleId.PollEndOneToOne,
|
||||||
|
RuleId.PollEndOneToOneUnstable,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Encrypted messages just sent to the user in a 1:1 room
|
// Encrypted messages just sent to the user in a 1:1 room
|
||||||
|
@ -147,6 +160,7 @@ export const VectorPushRulesDefinitions: Record<string, VectorPushRuleDefinition
|
||||||
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
|
syncedRuleIds: [RuleId.PollStart, RuleId.PollStartUnstable, RuleId.PollEnd, RuleId.PollEndUnstable],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Encrypted messages just sent to a group chat room
|
// Encrypted messages just sent to a group chat room
|
||||||
|
|
|
@ -23,6 +23,9 @@ import {
|
||||||
Room,
|
Room,
|
||||||
NotificationCountType,
|
NotificationCountType,
|
||||||
PushRuleActionName,
|
PushRuleActionName,
|
||||||
|
TweakName,
|
||||||
|
ConditionKind,
|
||||||
|
IPushRuleCondition,
|
||||||
} 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, fireEvent, getByTestId, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, getByTestId, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
@ -47,27 +50,42 @@ const masterRule: IPushRule = {
|
||||||
};
|
};
|
||||||
const oneToOneRule: IPushRule = {
|
const oneToOneRule: IPushRule = {
|
||||||
conditions: [
|
conditions: [
|
||||||
{ kind: "room_member_count", is: "2" },
|
{ kind: ConditionKind.RoomMemberCount, is: "2" },
|
||||||
{ kind: "event_match", key: "type", pattern: "m.room.message" },
|
{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.message" },
|
||||||
],
|
],
|
||||||
actions: ["notify", { set_tweak: "highlight", value: false }],
|
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
|
||||||
rule_id: ".m.rule.room_one_to_one",
|
rule_id: ".m.rule.room_one_to_one",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
} as IPushRule;
|
} as IPushRule;
|
||||||
const encryptedOneToOneRule: IPushRule = {
|
const encryptedOneToOneRule: IPushRule = {
|
||||||
conditions: [
|
conditions: [
|
||||||
{ kind: "room_member_count", is: "2" },
|
{ kind: ConditionKind.RoomMemberCount, is: "2" },
|
||||||
{ kind: "event_match", key: "type", pattern: "m.room.encrypted" },
|
{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.encrypted" },
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
PushRuleActionName.Notify,
|
||||||
|
{ set_tweak: TweakName.Sound, value: "default" },
|
||||||
|
{ set_tweak: TweakName.Highlight, value: false },
|
||||||
],
|
],
|
||||||
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
|
|
||||||
rule_id: ".m.rule.encrypted_room_one_to_one",
|
rule_id: ".m.rule.encrypted_room_one_to_one",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
} as IPushRule;
|
} as IPushRule;
|
||||||
|
const groupRule = {
|
||||||
|
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.message" }],
|
||||||
|
actions: [
|
||||||
|
PushRuleActionName.Notify,
|
||||||
|
{ set_tweak: TweakName.Sound, value: "default" },
|
||||||
|
{ set_tweak: TweakName.Highlight, value: false },
|
||||||
|
],
|
||||||
|
rule_id: ".m.rule.message",
|
||||||
|
default: true,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
const encryptedGroupRule: IPushRule = {
|
const encryptedGroupRule: IPushRule = {
|
||||||
conditions: [{ kind: "event_match", key: "type", pattern: "m.room.encrypted" }],
|
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.encrypted" }],
|
||||||
actions: ["dont_notify"],
|
actions: [PushRuleActionName.DontNotify],
|
||||||
rule_id: ".m.rule.encrypted",
|
rule_id: ".m.rule.encrypted",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -76,46 +94,55 @@ const pushRules: IPushRules = {
|
||||||
global: {
|
global: {
|
||||||
underride: [
|
underride: [
|
||||||
{
|
{
|
||||||
conditions: [{ kind: "event_match", key: "type", pattern: "m.call.invite" }],
|
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.call.invite" }],
|
||||||
actions: ["notify", { set_tweak: "sound", value: "ring" }, { set_tweak: "highlight", value: false }],
|
actions: [
|
||||||
|
PushRuleActionName.Notify,
|
||||||
|
{ set_tweak: TweakName.Sound, value: "ring" },
|
||||||
|
{ set_tweak: TweakName.Highlight, value: false },
|
||||||
|
],
|
||||||
rule_id: ".m.rule.call",
|
rule_id: ".m.rule.call",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
oneToOneRule,
|
oneToOneRule,
|
||||||
encryptedOneToOneRule,
|
encryptedOneToOneRule,
|
||||||
{
|
groupRule,
|
||||||
conditions: [{ kind: "event_match", key: "type", pattern: "m.room.message" }],
|
|
||||||
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
|
|
||||||
rule_id: ".m.rule.message",
|
|
||||||
default: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
encryptedGroupRule,
|
encryptedGroupRule,
|
||||||
{
|
{
|
||||||
conditions: [
|
conditions: [
|
||||||
{ kind: "event_match", key: "type", pattern: "im.vector.modular.widgets" },
|
{ kind: ConditionKind.EventMatch, key: "type", pattern: "im.vector.modular.widgets" },
|
||||||
{ kind: "event_match", key: "content.type", pattern: "jitsi" },
|
{ kind: ConditionKind.EventMatch, key: "content.type", pattern: "jitsi" },
|
||||||
{ kind: "event_match", key: "state_key", pattern: "*" },
|
{ kind: ConditionKind.EventMatch, key: "state_key", pattern: "*" },
|
||||||
],
|
],
|
||||||
actions: ["notify", { set_tweak: "highlight", value: false }],
|
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
|
||||||
rule_id: ".im.vector.jitsi",
|
rule_id: ".im.vector.jitsi",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sender: [],
|
sender: [],
|
||||||
room: [{ actions: ["dont_notify"], rule_id: "!zJPyWqpMorfCcWObge:matrix.org", default: false, enabled: true }],
|
room: [
|
||||||
|
{
|
||||||
|
actions: [PushRuleActionName.DontNotify],
|
||||||
|
rule_id: "!zJPyWqpMorfCcWObge:matrix.org",
|
||||||
|
default: false,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
actions: ["notify", { set_tweak: "highlight", value: false }],
|
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
|
||||||
pattern: "banana",
|
pattern: "banana",
|
||||||
rule_id: "banana",
|
rule_id: "banana",
|
||||||
default: false,
|
default: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight" }],
|
actions: [
|
||||||
|
PushRuleActionName.Notify,
|
||||||
|
{ set_tweak: TweakName.Sound, value: "default" },
|
||||||
|
{ set_tweak: TweakName.Highlight },
|
||||||
|
],
|
||||||
pattern: "kadev1",
|
pattern: "kadev1",
|
||||||
rule_id: ".m.rule.contains_user_name",
|
rule_id: ".m.rule.contains_user_name",
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -123,62 +150,76 @@ const pushRules: IPushRules = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
override: [
|
override: [
|
||||||
{ conditions: [], actions: ["dont_notify"], rule_id: ".m.rule.master", default: true, enabled: false },
|
|
||||||
{
|
{
|
||||||
conditions: [{ kind: "event_match", key: "content.msgtype", pattern: "m.notice" }],
|
conditions: [],
|
||||||
actions: ["dont_notify"],
|
actions: [PushRuleActionName.DontNotify],
|
||||||
|
rule_id: ".m.rule.master",
|
||||||
|
default: true,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditions: [{ kind: ConditionKind.EventMatch, key: "content.msgtype", pattern: "m.notice" }],
|
||||||
|
actions: [PushRuleActionName.DontNotify],
|
||||||
rule_id: ".m.rule.suppress_notices",
|
rule_id: ".m.rule.suppress_notices",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
conditions: [
|
conditions: [
|
||||||
{ kind: "event_match", key: "type", pattern: "m.room.member" },
|
{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.member" },
|
||||||
{ kind: "event_match", key: "content.membership", pattern: "invite" },
|
{ kind: ConditionKind.EventMatch, key: "content.membership", pattern: "invite" },
|
||||||
{ kind: "event_match", key: "state_key", pattern: "@kadev1:matrix.org" },
|
{ kind: ConditionKind.EventMatch, key: "state_key", pattern: "@kadev1:matrix.org" },
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
PushRuleActionName.Notify,
|
||||||
|
{ set_tweak: TweakName.Sound, value: "default" },
|
||||||
|
{ set_tweak: TweakName.Highlight, value: false },
|
||||||
],
|
],
|
||||||
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
|
|
||||||
rule_id: ".m.rule.invite_for_me",
|
rule_id: ".m.rule.invite_for_me",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
conditions: [{ kind: "event_match", key: "type", pattern: "m.room.member" }],
|
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.member" }],
|
||||||
actions: ["dont_notify"],
|
actions: [PushRuleActionName.DontNotify],
|
||||||
rule_id: ".m.rule.member_event",
|
rule_id: ".m.rule.member_event",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
conditions: [{ kind: "contains_display_name" }],
|
conditions: [{ kind: "contains_display_name" }],
|
||||||
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight" }],
|
actions: [
|
||||||
|
PushRuleActionName.Notify,
|
||||||
|
{ set_tweak: TweakName.Sound, value: "default" },
|
||||||
|
{ set_tweak: TweakName.Highlight },
|
||||||
|
],
|
||||||
rule_id: ".m.rule.contains_display_name",
|
rule_id: ".m.rule.contains_display_name",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
conditions: [
|
conditions: [
|
||||||
{ kind: "event_match", key: "content.body", pattern: "@room" },
|
{ kind: ConditionKind.EventMatch, key: "content.body", pattern: "@room" },
|
||||||
{ kind: "sender_notification_permission", key: "room" },
|
{ kind: "sender_notification_permission", key: "room" },
|
||||||
],
|
],
|
||||||
actions: ["notify", { set_tweak: "highlight", value: true }],
|
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
|
||||||
rule_id: ".m.rule.roomnotif",
|
rule_id: ".m.rule.roomnotif",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
conditions: [
|
conditions: [
|
||||||
{ kind: "event_match", key: "type", pattern: "m.room.tombstone" },
|
{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.tombstone" },
|
||||||
{ kind: "event_match", key: "state_key", pattern: "" },
|
{ kind: ConditionKind.EventMatch, key: "state_key", pattern: "" },
|
||||||
],
|
],
|
||||||
actions: ["notify", { set_tweak: "highlight", value: true }],
|
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
|
||||||
rule_id: ".m.rule.tombstone",
|
rule_id: ".m.rule.tombstone",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
conditions: [{ kind: "event_match", key: "type", pattern: "m.reaction" }],
|
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.reaction" }],
|
||||||
actions: ["dont_notify"],
|
actions: [PushRuleActionName.DontNotify],
|
||||||
rule_id: ".m.rule.reaction",
|
rule_id: ".m.rule.reaction",
|
||||||
default: true,
|
default: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -231,6 +272,8 @@ describe("<Notifications />", () => {
|
||||||
mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] });
|
mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] });
|
||||||
mockClient.getThreePids.mockClear().mockResolvedValue({ threepids: [] });
|
mockClient.getThreePids.mockClear().mockResolvedValue({ threepids: [] });
|
||||||
mockClient.setPusher.mockClear().mockResolvedValue({});
|
mockClient.setPusher.mockClear().mockResolvedValue({});
|
||||||
|
mockClient.setPushRuleActions.mockClear().mockResolvedValue({});
|
||||||
|
mockClient.pushRules = pushRules;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders spinner while loading", async () => {
|
it("renders spinner while loading", async () => {
|
||||||
|
@ -429,6 +472,196 @@ describe("<Notifications />", () => {
|
||||||
StandardActions.ACTION_DONT_NOTIFY,
|
StandardActions.ACTION_DONT_NOTIFY,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("synced rules", () => {
|
||||||
|
const pollStartOneToOne = {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
kind: ConditionKind.RoomMemberCount,
|
||||||
|
is: "2",
|
||||||
|
} as IPushRuleCondition<ConditionKind.RoomMemberCount>,
|
||||||
|
{
|
||||||
|
kind: ConditionKind.EventMatch,
|
||||||
|
key: "type",
|
||||||
|
pattern: "org.matrix.msc3381.poll.start",
|
||||||
|
} as IPushRuleCondition<ConditionKind.EventMatch>,
|
||||||
|
],
|
||||||
|
actions: [PushRuleActionName.DontNotify],
|
||||||
|
rule_id: ".org.matrix.msc3930.rule.poll_start_one_to_one",
|
||||||
|
default: true,
|
||||||
|
enabled: true,
|
||||||
|
} as IPushRule;
|
||||||
|
const pollStartGroup = {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
kind: ConditionKind.EventMatch,
|
||||||
|
key: "type",
|
||||||
|
pattern: "org.matrix.msc3381.poll.start",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [PushRuleActionName.Notify],
|
||||||
|
rule_id: ".org.matrix.msc3930.rule.poll_start",
|
||||||
|
default: true,
|
||||||
|
enabled: true,
|
||||||
|
} as IPushRule;
|
||||||
|
const pollEndOneToOne = {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
kind: ConditionKind.RoomMemberCount,
|
||||||
|
is: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: ConditionKind.EventMatch,
|
||||||
|
key: "type",
|
||||||
|
pattern: "org.matrix.msc3381.poll.end",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
PushRuleActionName.Notify,
|
||||||
|
{ set_tweak: TweakName.Highlight, value: false },
|
||||||
|
{ set_tweak: TweakName.Sound, value: "default" },
|
||||||
|
],
|
||||||
|
rule_id: ".org.matrix.msc3930.rule.poll_end_one_to_one",
|
||||||
|
default: true,
|
||||||
|
enabled: true,
|
||||||
|
} as IPushRule;
|
||||||
|
|
||||||
|
const setPushRuleMock = (rules: IPushRule[] = []): void => {
|
||||||
|
const combinedRules = {
|
||||||
|
...pushRules,
|
||||||
|
global: {
|
||||||
|
...pushRules.global,
|
||||||
|
underride: [...pushRules.global.underride!, ...rules],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockClient.getPushRules.mockClear().mockResolvedValue(combinedRules);
|
||||||
|
mockClient.pushRules = combinedRules;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ".m.rule.room_one_to_one" and ".m.rule.message" have synced rules
|
||||||
|
it("succeeds when no synced rules exist for user", async () => {
|
||||||
|
await getComponentAndWait();
|
||||||
|
const section = "vector_global";
|
||||||
|
|
||||||
|
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||||
|
|
||||||
|
const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
|
||||||
|
fireEvent.click(offToggle);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// didnt attempt to update any non-existant rules
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// no error
|
||||||
|
expect(screen.queryByTestId("error-message")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates synced rules when they exist for user", async () => {
|
||||||
|
setPushRuleMock([pollStartOneToOne, pollStartGroup]);
|
||||||
|
await getComponentAndWait();
|
||||||
|
const section = "vector_global";
|
||||||
|
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||||
|
|
||||||
|
const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
|
||||||
|
fireEvent.click(offToggle);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// updated synced rule
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||||
|
"global",
|
||||||
|
"underride",
|
||||||
|
oneToOneRule.rule_id,
|
||||||
|
[PushRuleActionName.DontNotify],
|
||||||
|
);
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||||
|
"global",
|
||||||
|
"underride",
|
||||||
|
pollStartOneToOne.rule_id,
|
||||||
|
[PushRuleActionName.DontNotify],
|
||||||
|
);
|
||||||
|
// only called for parent rule and one existing synced rule
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
// no error
|
||||||
|
expect(screen.queryByTestId("error-message")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not update synced rules when main rule update fails", async () => {
|
||||||
|
setPushRuleMock([pollStartOneToOne]);
|
||||||
|
await getComponentAndWait();
|
||||||
|
const section = "vector_global";
|
||||||
|
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||||
|
// have main rule update fail
|
||||||
|
mockClient.setPushRuleActions.mockRejectedValue("oups");
|
||||||
|
|
||||||
|
const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
|
||||||
|
fireEvent.click(offToggle);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||||
|
"global",
|
||||||
|
"underride",
|
||||||
|
oneToOneRule.rule_id,
|
||||||
|
[PushRuleActionName.DontNotify],
|
||||||
|
);
|
||||||
|
// only called for parent rule
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("error-message")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the UI toggle to rule value when no synced rule exist for the user", async () => {
|
||||||
|
setPushRuleMock([]);
|
||||||
|
await getComponentAndWait();
|
||||||
|
const section = "vector_global";
|
||||||
|
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||||
|
|
||||||
|
// loudest state of synced rules should be the toggle value
|
||||||
|
expect(oneToOneRuleElement.querySelector('input[aria-label="On"]')).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the UI toggle to the loudest synced rule value", async () => {
|
||||||
|
// oneToOneRule is set to 'On'
|
||||||
|
// pollEndOneToOne is set to 'Loud'
|
||||||
|
setPushRuleMock([pollStartOneToOne, pollEndOneToOne]);
|
||||||
|
await getComponentAndWait();
|
||||||
|
const section = "vector_global";
|
||||||
|
const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
|
||||||
|
|
||||||
|
// loudest state of synced rules should be the toggle value
|
||||||
|
expect(oneToOneRuleElement.querySelector('input[aria-label="Noisy"]')).toBeChecked();
|
||||||
|
|
||||||
|
const onToggle = oneToOneRuleElement.querySelector('input[aria-label="On"]')!;
|
||||||
|
fireEvent.click(onToggle);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// called for all 3 rules
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(3);
|
||||||
|
const expectedActions = [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }];
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||||
|
"global",
|
||||||
|
"underride",
|
||||||
|
oneToOneRule.rule_id,
|
||||||
|
expectedActions,
|
||||||
|
);
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||||
|
"global",
|
||||||
|
"underride",
|
||||||
|
pollStartOneToOne.rule_id,
|
||||||
|
expectedActions,
|
||||||
|
);
|
||||||
|
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||||
|
"global",
|
||||||
|
"underride",
|
||||||
|
pollEndOneToOne.rule_id,
|
||||||
|
expectedActions,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("clear all notifications", () => {
|
describe("clear all notifications", () => {
|
||||||
|
|
Loading…
Reference in New Issue