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

Commit

Permalink
Polls push rules: synchronise poll rules with message rules (#10263)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Kerry authored Mar 2, 2023
1 parent e5291c1 commit 10a7654
Show file tree
Hide file tree
Showing 3 changed files with 403 additions and 50 deletions.
122 changes: 114 additions & 8 deletions src/components/views/settings/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {}
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
});
}
Expand Down Expand Up @@ -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 });

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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]}
Expand Down
16 changes: 15 additions & 1 deletion src/notifications/VectorPushRulesDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 10a7654

Please sign in to comment.