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

* tidying
This commit is contained in:
Kerry 2023-03-02 18:33:19 +13:00 committed by GitHub
parent e5291c195d
commit 10a765472b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 403 additions and 50 deletions

View file

@ -15,10 +15,18 @@ limitations under the License.
*/
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 { logger } from "matrix-js-sdk/src/logger";
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import Spinner from "../elements/Spinner";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -92,6 +100,9 @@ interface IVectorPushRule {
rule?: IAnnotatedPushRule;
description: TranslatedString | string;
vectorState: VectorState;
// loudest vectorState of a rule and its synced rules
// undefined when rule has no synced rules
syncedVectorState?: VectorState;
}
interface IProps {}
@ -115,9 +126,68 @@ interface IState {
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> {
private settingWatchers: string[];
private pushProcessor: PushProcessor;
public constructor(props: IProps) {
super(props);
@ -145,6 +215,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
this.setState({ audioNotifications: value as boolean }),
),
];
this.pushProcessor = new PushProcessor(MatrixClientPeg.get());
}
private get isInhibited(): boolean {
@ -281,6 +353,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
ruleId: rule.rule_id,
rule,
vectorState,
syncedVectorState: maximumVectorState(defaultRules, rule, definition),
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);
};
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> => {
this.setState({ phase: Phase.Persisting });
@ -428,12 +538,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
} else {
const definition: VectorPushRuleDefinition = VectorPushRulesDefinitions[rule.ruleId];
const actions = definition.vectorStateToActions[checkedState];
if (!actions) {
await cli.setPushRuleEnabled("global", rule.rule.kind, rule.rule.rule_id, false);
} 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.setPushRuleActions(rule.rule.rule_id, rule.rule.kind, actions);
await this.updateSyncedRules(definition.syncedRuleIds, actions);
}
await this.refreshFromServer();
@ -684,7 +790,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
<StyledRadioButton
key={r.ruleId + s}
name={r.ruleId}
checked={r.vectorState === s}
checked={(r.syncedVectorState ?? r.vectorState) === s}
onChange={this.onRadioChecked.bind(this, r, s)}
disabled={this.state.phase === Phase.Persisting}
aria-label={VectorStateToLabel[s]}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
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 { _td } from "../languageHandler";
@ -29,15 +29,22 @@ type StateToActionsMap = {
interface IVectorPushRuleDefinition {
description: string;
vectorStateToActions: StateToActionsMap;
/**
* Rules that should be updated to be kept in sync
* when this rule changes
*/
syncedRuleIds?: (RuleId | string)[];
}
class VectorPushRuleDefinition {
public readonly description: string;
public readonly vectorStateToActions: StateToActionsMap;
public readonly syncedRuleIds?: (RuleId | string)[];
public constructor(opts: IVectorPushRuleDefinition) {
this.description = opts.description;
this.vectorStateToActions = opts.vectorStateToActions;
this.syncedRuleIds = opts.syncedRuleIds;
}
// 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.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
@ -147,6 +160,7 @@ export const VectorPushRulesDefinitions: Record<string, VectorPushRuleDefinition
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
},
syncedRuleIds: [RuleId.PollStart, RuleId.PollStartUnstable, RuleId.PollEnd, RuleId.PollEndUnstable],
}),
// Encrypted messages just sent to a group chat room