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

Commit cef821c

Browse files
author
Kerry
authored
Polls: sync push rules on changes to account_data (#10287)
* basic sync setup * formatting * get loudest value for synced rules * more types * test synced rules in notifications settings * type fixes * noimplicitany fixes * remove debug * tidying * extract updatePushRuleActions fn to utils * extract update synced rules * just synchronise in one place? * monitor account data changes AND trigger changes sync in notifications form * lint * setup LoggedInView test with enough mocks * test rule syncing in LoggedInView * strict fixes * more comments * one more comment
1 parent 4c6f8ad commit cef821c

File tree

5 files changed

+525
-51
lines changed

5 files changed

+525
-51
lines changed

src/components/structures/LoggedInView.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { IConfigOptions } from "../../IConfigOptions";
7070
import LeftPanelLiveShareWarning from "../views/beacon/LeftPanelLiveShareWarning";
7171
import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage";
7272
import { PipContainer } from "./PipContainer";
73+
import { monitorSyncedPushRules } from "../../utils/pushRules/monitorSyncedPushRules";
7374

7475
// We need to fetch each pinned message individually (if we don't already have it)
7576
// so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -165,6 +166,8 @@ class LoggedInView extends React.Component<IProps, IState> {
165166
this.updateServerNoticeEvents();
166167

167168
this._matrixClient.on(ClientEvent.AccountData, this.onAccountData);
169+
// check push rules on start up as well
170+
monitorSyncedPushRules(this._matrixClient.getAccountData("m.push_rules"), this._matrixClient);
168171
this._matrixClient.on(ClientEvent.Sync, this.onSync);
169172
// Call `onSync` with the current state as well
170173
this.onSync(this._matrixClient.getSyncState(), null, this._matrixClient.getSyncStateData());
@@ -279,6 +282,7 @@ class LoggedInView extends React.Component<IProps, IState> {
279282
if (event.getType() === "m.ignored_user_list") {
280283
dis.dispatch({ action: "ignore_state_changed" });
281284
}
285+
monitorSyncedPushRules(event, this._matrixClient);
282286
};
283287

284288
private onCompactLayoutChanged = (): void => {

src/components/views/settings/Notifications.tsx

Lines changed: 12 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,10 @@ limitations under the License.
1515
*/
1616

1717
import React, { ReactNode } from "react";
18-
import {
19-
IAnnotatedPushRule,
20-
IPusher,
21-
PushRuleAction,
22-
IPushRule,
23-
PushRuleKind,
24-
RuleId,
25-
} from "matrix-js-sdk/src/@types/PushRules";
18+
import { IAnnotatedPushRule, IPusher, PushRuleAction, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules";
2619
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
2720
import { logger } from "matrix-js-sdk/src/logger";
2821
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
29-
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
3022

3123
import Spinner from "../elements/Spinner";
3224
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@@ -51,6 +43,10 @@ import TagComposer from "../elements/TagComposer";
5143
import { objectClone } from "../../../utils/objects";
5244
import { arrayDiff } from "../../../utils/arrays";
5345
import { clearAllNotifications, getLocalNotificationAccountDataEventType } from "../../../utils/notifications";
46+
import {
47+
updateExistingPushRulesWithActions,
48+
updatePushRuleActions,
49+
} from "../../../utils/pushRules/updatePushRuleActions";
5450

5551
// TODO: this "view" component still has far too much application logic in it,
5652
// which should be factored out to other files.
@@ -187,7 +183,6 @@ const maximumVectorState = (
187183

188184
export default class Notifications extends React.PureComponent<IProps, IState> {
189185
private settingWatchers: string[];
190-
private pushProcessor: PushProcessor;
191186

192187
public constructor(props: IProps) {
193188
super(props);
@@ -215,8 +210,6 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
215210
this.setState({ audioNotifications: value as boolean }),
216211
),
217212
];
218-
219-
this.pushProcessor = new PushProcessor(MatrixClientPeg.get());
220213
}
221214

222215
private get isInhibited(): boolean {
@@ -461,43 +454,6 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
461454
await SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, checked);
462455
};
463456

464-
private setPushRuleActions = async (
465-
ruleId: IPushRule["rule_id"],
466-
kind: PushRuleKind,
467-
actions?: PushRuleAction[],
468-
): Promise<void> => {
469-
const cli = MatrixClientPeg.get();
470-
if (!actions) {
471-
await cli.setPushRuleEnabled("global", kind, ruleId, false);
472-
} else {
473-
await cli.setPushRuleActions("global", kind, ruleId, actions);
474-
await cli.setPushRuleEnabled("global", kind, ruleId, true);
475-
}
476-
};
477-
478-
/**
479-
* Updated syncedRuleIds from rule definition
480-
* If a rule does not exist it is ignored
481-
* Synced rules are updated sequentially
482-
* and stop at first error
483-
*/
484-
private updateSyncedRules = async (
485-
syncedRuleIds: VectorPushRuleDefinition["syncedRuleIds"],
486-
actions?: PushRuleAction[],
487-
): Promise<void> => {
488-
// get synced rules that exist for user
489-
const syncedRules: ReturnType<PushProcessor["getPushRuleAndKindById"]>[] = syncedRuleIds
490-
?.map((ruleId) => this.pushProcessor.getPushRuleAndKindById(ruleId))
491-
.filter(Boolean);
492-
493-
if (!syncedRules?.length) {
494-
return;
495-
}
496-
for (const { kind, rule: syncedRule } of syncedRules) {
497-
await this.setPushRuleActions(syncedRule.rule_id, kind, actions);
498-
}
499-
};
500-
501457
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState): Promise<void> => {
502458
this.setState({ phase: Phase.Persisting });
503459

@@ -538,8 +494,13 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
538494
} else {
539495
const definition: VectorPushRuleDefinition = VectorPushRulesDefinitions[rule.ruleId];
540496
const actions = definition.vectorStateToActions[checkedState];
541-
await this.setPushRuleActions(rule.rule.rule_id, rule.rule.kind, actions);
542-
await this.updateSyncedRules(definition.syncedRuleIds, actions);
497+
// we should not encounter this
498+
// satisfies types
499+
if (!rule.rule) {
500+
throw new Error("Cannot update rule: push rule data is incomplete.");
501+
}
502+
await updatePushRuleActions(cli, rule.rule.rule_id, rule.rule.kind, actions);
503+
await updateExistingPushRulesWithActions(cli, definition.syncedRuleIds, actions);
543504
}
544505

545506
await this.refreshFromServer();
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixClient } from "matrix-js-sdk/src/client";
18+
import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
19+
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
20+
import { RuleId, IAnnotatedPushRule } from "matrix-js-sdk/src/@types/PushRules";
21+
import { logger } from "matrix-js-sdk/src/logger";
22+
23+
import { VectorPushRulesDefinitions, VectorPushRuleDefinition } from "../../notifications";
24+
import { updateExistingPushRulesWithActions } from "./updatePushRuleActions";
25+
26+
const pushRuleAndKindToAnnotated = (
27+
ruleAndKind: ReturnType<PushProcessor["getPushRuleAndKindById"]>,
28+
): IAnnotatedPushRule | undefined =>
29+
ruleAndKind
30+
? {
31+
...ruleAndKind.rule,
32+
kind: ruleAndKind.kind,
33+
}
34+
: undefined;
35+
36+
/**
37+
* Checks that any synced rules that exist a given rule are in sync
38+
* And updates any that are out of sync
39+
* Ignores ruleIds that do not exist for the user
40+
* @param matrixClient - cli
41+
* @param pushProcessor - processor used to retrieve current state of rules
42+
* @param ruleId - primary rule
43+
* @param definition - VectorPushRuleDefinition of the primary rule
44+
*/
45+
const monitorSyncedRule = async (
46+
matrixClient: MatrixClient,
47+
pushProcessor: PushProcessor,
48+
ruleId: RuleId | string,
49+
definition: VectorPushRuleDefinition,
50+
): Promise<void> => {
51+
const primaryRule = pushRuleAndKindToAnnotated(pushProcessor.getPushRuleAndKindById(ruleId));
52+
53+
if (!primaryRule) {
54+
return;
55+
}
56+
const syncedRules: IAnnotatedPushRule[] | undefined = definition.syncedRuleIds
57+
?.map((ruleId) => pushRuleAndKindToAnnotated(pushProcessor.getPushRuleAndKindById(ruleId)))
58+
.filter((n?: IAnnotatedPushRule): n is IAnnotatedPushRule => Boolean(n));
59+
60+
// no synced rules to manage
61+
if (!syncedRules?.length) {
62+
return;
63+
}
64+
65+
const primaryRuleVectorState = definition.ruleToVectorState(primaryRule);
66+
67+
const outOfSyncRules = syncedRules.filter(
68+
(syncedRule) => definition.ruleToVectorState(syncedRule) !== primaryRuleVectorState,
69+
);
70+
71+
if (outOfSyncRules.length) {
72+
await updateExistingPushRulesWithActions(
73+
matrixClient,
74+
// eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
75+
outOfSyncRules.map(({ rule_id }) => rule_id),
76+
primaryRule.actions,
77+
);
78+
}
79+
};
80+
81+
/**
82+
* On changes to m.push_rules account data,
83+
* check that synced push rules are in sync with their primary rule,
84+
* and update any out of sync rules.
85+
* synced rules are defined in VectorPushRulesDefinitions
86+
* If updating a rule fails for any reason,
87+
* the error is caught and handled silently
88+
* @param accountDataEvent - MatrixEvent
89+
* @param matrixClient - cli
90+
* @returns Resolves when updates are complete
91+
*/
92+
export const monitorSyncedPushRules = async (
93+
accountDataEvent: MatrixEvent | undefined,
94+
matrixClient: MatrixClient,
95+
): Promise<void> => {
96+
if (accountDataEvent?.getType() !== EventType.PushRules) {
97+
return;
98+
}
99+
const pushProcessor = new PushProcessor(matrixClient);
100+
101+
Object.entries(VectorPushRulesDefinitions).forEach(async ([ruleId, definition]) => {
102+
try {
103+
await monitorSyncedRule(matrixClient, pushProcessor, ruleId, definition);
104+
} catch (error) {
105+
logger.error(`Failed to fully synchronise push rules for ${ruleId}`, error);
106+
}
107+
});
108+
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixClient } from "matrix-js-sdk/src/client";
18+
import { IPushRule, PushRuleAction, PushRuleKind } from "matrix-js-sdk/src/matrix";
19+
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
20+
21+
/**
22+
* Sets the actions for a given push rule id and kind
23+
* When actions are falsy, disables the rule
24+
* @param matrixClient - cli
25+
* @param ruleId - rule id to update
26+
* @param kind - PushRuleKind
27+
* @param actions - push rule actions to set for rule
28+
*/
29+
export const updatePushRuleActions = async (
30+
matrixClient: MatrixClient,
31+
ruleId: IPushRule["rule_id"],
32+
kind: PushRuleKind,
33+
actions?: PushRuleAction[],
34+
): Promise<void> => {
35+
if (!actions) {
36+
await matrixClient.setPushRuleEnabled("global", kind, ruleId, false);
37+
} else {
38+
await matrixClient.setPushRuleActions("global", kind, ruleId, actions);
39+
await matrixClient.setPushRuleEnabled("global", kind, ruleId, true);
40+
}
41+
};
42+
43+
interface PushRuleAndKind {
44+
rule: IPushRule;
45+
kind: PushRuleKind;
46+
}
47+
48+
/**
49+
* Update push rules with given actions
50+
* Where they already exist for current user
51+
* Rules are updated sequentially and stop at first error
52+
* @param matrixClient - cli
53+
* @param ruleIds - RuleIds of push rules to attempt to set actions for
54+
* @param actions - push rule actions to set for rule
55+
* @returns resolves when all rules have been updated
56+
* @returns rejects when a rule update fails
57+
*/
58+
export const updateExistingPushRulesWithActions = async (
59+
matrixClient: MatrixClient,
60+
ruleIds?: IPushRule["rule_id"][],
61+
actions?: PushRuleAction[],
62+
): Promise<void> => {
63+
const pushProcessor = new PushProcessor(matrixClient);
64+
65+
const rules: PushRuleAndKind[] | undefined = ruleIds
66+
?.map((ruleId) => pushProcessor.getPushRuleAndKindById(ruleId))
67+
.filter((n: PushRuleAndKind | null): n is PushRuleAndKind => Boolean(n));
68+
69+
if (!rules?.length) {
70+
return;
71+
}
72+
for (const { kind, rule } of rules) {
73+
await updatePushRuleActions(matrixClient, rule.rule_id, kind, actions);
74+
}
75+
};

0 commit comments

Comments
 (0)