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

Polls push rules: synchronise poll rules with message rules #10263

Merged
merged 11 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 117 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,65 @@ 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;
}
}
};
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 = (
robintown marked this conversation as resolved.
Show resolved Hide resolved
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 +212,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 @@ -191,6 +260,7 @@ export default class Notifications extends React.PureComponent<IProps, IState> {
phase: Phase.Ready,
});
} catch (e) {
console.log(e);
logger.error("Error setting up notifications for settings: ", e);
robintown marked this conversation as resolved.
Show resolved Hide resolved
this.setState({ phase: Phase.Error });
}
Expand Down Expand Up @@ -281,6 +351,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 +459,48 @@ 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> => {
console.log(
robintown marked this conversation as resolved.
Show resolved Hide resolved
"updatesynedce",
syncedRuleIds,
syncedRuleIds?.map((ruleId) => this.pushProcessor.getPushRuleAndKindById(ruleId)),
);
// 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 +541,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 +793,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