From 59e48ee0ba5e7fb7461dae1a032ccd07267a8ac4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Jul 2021 21:42:56 -0600 Subject: [PATCH 01/16] Convert NotificationUserSettingsTab to TS --- ...serSettingsTab.js => NotificationUserSettingsTab.tsx} | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) rename src/components/views/settings/tabs/user/{NotificationUserSettingsTab.js => NotificationUserSettingsTab.tsx} (86%) diff --git a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx similarity index 86% rename from src/components/views/settings/tabs/user/NotificationUserSettingsTab.js rename to src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx index 0aabdd24e25..a0f4e330bb8 100644 --- a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,17 +16,12 @@ limitations under the License. import React from 'react'; import { _t } from "../../../../../languageHandler"; -import * as sdk from "../../../../../index"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import Notifications from "../../Notifications"; @replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab") export default class NotificationUserSettingsTab extends React.Component { - constructor() { - super(); - } - render() { - const Notifications = sdk.getComponent("views.settings.Notifications"); return (
{_t("Notifications")}
From 436563be7b90a7a021d8e027654bb39095829ae4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Jul 2021 21:43:52 -0600 Subject: [PATCH 02/16] Change label on notification dropdown for a room by request of design, to reduce mental load --- src/components/views/rooms/RoomTile.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 9be0274dd51..580ea010734 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -408,7 +408,7 @@ export default class RoomTile extends React.PureComponent { > Date: Thu, 1 Jul 2021 21:49:36 -0600 Subject: [PATCH 03/16] Convert Spinner to TS --- src/components/views/elements/Spinner.js | 39 -------------------- src/components/views/elements/Spinner.tsx | 45 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 39 deletions(-) delete mode 100644 src/components/views/elements/Spinner.js create mode 100644 src/components/views/elements/Spinner.tsx diff --git a/src/components/views/elements/Spinner.js b/src/components/views/elements/Spinner.js deleted file mode 100644 index 75f85d04412..00000000000 --- a/src/components/views/elements/Spinner.js +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import PropTypes from "prop-types"; -import { _t } from "../../../languageHandler"; - -const Spinner = ({ w = 32, h = 32, message }) => ( -
- { message &&
{ message }
 
} -
-
-); - -Spinner.propTypes = { - w: PropTypes.number, - h: PropTypes.number, - message: PropTypes.node, -}; - -export default Spinner; diff --git a/src/components/views/elements/Spinner.tsx b/src/components/views/elements/Spinner.tsx new file mode 100644 index 00000000000..93c8f9e5d43 --- /dev/null +++ b/src/components/views/elements/Spinner.tsx @@ -0,0 +1,45 @@ +/* +Copyright 2015-2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { _t } from "../../../languageHandler"; + +interface IProps { + w?: number; + h?: number; + message?: string; +} + +export default class Spinner extends React.PureComponent { + public static defaultProps: Partial = { + w: 32, + h: 32, + }; + + public render() { + const { w, h, message } = this.props; + return ( +
+ { message &&
{ message }
 
} +
+
+ ); + } +} From 9556b610415d60e2a95fc69a7b98206a3dbf6292 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Jul 2021 21:58:03 -0600 Subject: [PATCH 04/16] Crude conversion of Notifications.js to TS + cut out legacy code This is to make the file clearer during development and serves no practical purpose --- .../{Notifications.js => Notifications.tsx} | 78 +++---------------- 1 file changed, 9 insertions(+), 69 deletions(-) rename src/components/views/settings/{Notifications.js => Notifications.tsx} (92%) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.tsx similarity index 92% rename from src/components/views/settings/Notifications.js rename to src/components/views/settings/Notifications.tsx index c263ff50c8b..9f1929a35f9 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.tsx @@ -22,7 +22,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import SettingsStore from '../../../settings/SettingsStore'; import Modal from '../../../Modal'; import { - NotificationUtils, VectorPushRulesDefinitions, PushRuleVectorState, ContentRules, @@ -40,31 +39,6 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; // TODO: this component also does a lot of direct poking into this.state, which // is VERY NAUGHTY. -/** - * Rules that Vector used to set in order to override the actions of default rules. - * These are used to port peoples existing overrides to match the current API. - * These can be removed and forgotten once everyone has moved to the new client. - */ -const LEGACY_RULES = { - "im.vector.rule.contains_display_name": ".m.rule.contains_display_name", - "im.vector.rule.room_one_to_one": ".m.rule.room_one_to_one", - "im.vector.rule.room_message": ".m.rule.message", - "im.vector.rule.invite_for_me": ".m.rule.invite_for_me", - "im.vector.rule.call": ".m.rule.call", - "im.vector.rule.notices": ".m.rule.suppress_notices", -}; - -function portLegacyActions(actions) { - const decoded = NotificationUtils.decodeActions(actions); - if (decoded !== null) { - return NotificationUtils.encodeActions(decoded); - } else { - // We don't recognise one of the actions here, so we don't try to - // canonicalise them. - return actions; - } -} - @replaceableComponent("views.settings.Notifications") export default class Notifications extends React.Component { static phases = { @@ -84,6 +58,7 @@ export default class Notifications extends React.Component { externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI externalContentRules: [], // Keyword push rules that have been defined outside Vector UI threepids: [], // used for email notifications + pushers: undefined, }; componentDidMount() { @@ -199,7 +174,7 @@ export default class Notifications extends React.Component { onKeywordsClicked = (event) => { // Compute the keywords list to display - let keywords = []; + let keywords: any[]|string = []; for (const i in this.state.vectorContentRules.rules) { const rule = this.state.vectorContentRules.rules[i]; keywords.push(rule.pattern); @@ -448,48 +423,9 @@ export default class Notifications extends React.Component { ); } - // Check if any legacy im.vector rules need to be ported to the new API - // for overriding the actions of default rules. - _portRulesToNewAPI(rulesets) { - const needsUpdate = []; - const cli = MatrixClientPeg.get(); - - for (const kind in rulesets.global) { - const ruleset = rulesets.global[kind]; - for (let i = 0; i < ruleset.length; ++i) { - const rule = ruleset[i]; - if (rule.rule_id in LEGACY_RULES) { - console.log("Porting legacy rule", rule); - needsUpdate.push( function(kind, rule) { - return cli.setPushRuleActions( - 'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions), - ).then(() => - cli.deletePushRule('global', kind, rule.rule_id), - ).catch( (e) => { - console.warn(`Error when porting legacy rule: ${e}`); - }); - }(kind, rule)); - } - } - } - - if (needsUpdate.length > 0) { - // If some of the rules need to be ported then wait for the porting - // to happen and then fetch the rules again. - return Promise.all(needsUpdate).then(() => - cli.getPushRules(), - ); - } else { - // Otherwise return the rules that we already have. - return rulesets; - } - } - _refreshFromServer = () => { const self = this; - const pushRulesPromise = MatrixClientPeg.get().getPushRules().then( - self._portRulesToNewAPI, - ).then(function(rulesets) { + const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(function(rulesets) { /// XXX seriously? wtf is this? MatrixClientPeg.get().pushRules = rulesets; @@ -803,7 +739,7 @@ export default class Notifications extends React.Component { } // Show keywords not displayed by the vector UI as a single external push rule - let externalKeywords = []; + let externalKeywords: any[]|string = []; for (const i in this.state.externalContentRules) { const rule = this.state.externalContentRules[i]; externalKeywords.push(rule.pattern); @@ -890,9 +826,13 @@ export default class Notifications extends React.Component { - + {/* @ts-ignore*/} + + {/* @ts-ignore*/} + {/* @ts-ignore*/} From 5b9fca3b91964d294e3d0f69bd5a93ae75dc3809 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Jul 2021 20:03:07 -0600 Subject: [PATCH 05/16] Migrate to js-sdk types for push rules --- src/notifications/ContentRules.ts | 19 ++-- src/notifications/NotificationUtils.ts | 21 ++--- src/notifications/PushRuleVectorState.ts | 5 +- src/notifications/types.ts | 114 ----------------------- 4 files changed, 21 insertions(+), 138 deletions(-) delete mode 100644 src/notifications/types.ts diff --git a/src/notifications/ContentRules.ts b/src/notifications/ContentRules.ts index 5f1281e58c9..fe27bfd67b4 100644 --- a/src/notifications/ContentRules.ts +++ b/src/notifications/ContentRules.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,12 +15,12 @@ limitations under the License. */ import { PushRuleVectorState, State } from "./PushRuleVectorState"; -import { IExtendedPushRule, IRuleSets } from "./types"; +import { IAnnotatedPushRule, IPushRules, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules"; export interface IContentRules { vectorState: State; - rules: IExtendedPushRule[]; - externalRules: IExtendedPushRule[]; + rules: IAnnotatedPushRule[]; + externalRules: IAnnotatedPushRule[]; } export const SCOPE = "global"; @@ -39,9 +38,9 @@ export class ContentRules { * externalRules: a list of other keyword rules, with states other than * vectorState */ - static parseContentRules(rulesets: IRuleSets): IContentRules { + public static parseContentRules(rulesets: IPushRules): IContentRules { // first categorise the keyword rules in terms of their actions - const contentRules = this._categoriseContentRules(rulesets); + const contentRules = ContentRules.categoriseContentRules(rulesets); // Decide which content rules to display in Vector UI. // Vector displays a single global rule for a list of keywords @@ -95,8 +94,8 @@ export class ContentRules { } } - static _categoriseContentRules(rulesets: IRuleSets) { - const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IExtendedPushRule[]> = { + private static categoriseContentRules(rulesets: IPushRules) { + const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IAnnotatedPushRule[]> = { on: [], on_but_disabled: [], loud: [], @@ -109,7 +108,7 @@ export class ContentRules { const r = rulesets.global[kind][i]; // check it's not a default rule - if (r.rule_id[0] === '.' || kind !== "content") { + if (r.rule_id[0] === '.' || kind !== PushRuleKind.ContentSpecific) { continue; } diff --git a/src/notifications/NotificationUtils.ts b/src/notifications/NotificationUtils.ts index 1d5356e16bc..fa7aa1186d9 100644 --- a/src/notifications/NotificationUtils.ts +++ b/src/notifications/NotificationUtils.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Action, Actions } from "./types"; +import { PushRuleAction, PushRuleActionName, TweakHighlight, TweakSound } from "matrix-js-sdk/src/@types/PushRules"; interface IEncodedActions { notify: boolean; @@ -35,18 +34,18 @@ export class NotificationUtils { const sound = action.sound; const highlight = action.highlight; if (notify) { - const actions: Action[] = [Actions.Notify]; + const actions: PushRuleAction[] = [PushRuleActionName.Notify]; if (sound) { - actions.push({ "set_tweak": "sound", "value": sound }); + actions.push({ "set_tweak": "sound", "value": sound } as TweakSound); } if (highlight) { - actions.push({ "set_tweak": "highlight" }); + actions.push({ "set_tweak": "highlight" } as TweakHighlight); } else { - actions.push({ "set_tweak": "highlight", "value": false }); + actions.push({ "set_tweak": "highlight", "value": false } as TweakHighlight); } return actions; } else { - return [Actions.DontNotify]; + return [PushRuleActionName.DontNotify]; } } @@ -56,16 +55,16 @@ export class NotificationUtils { // "highlight: true/false, // } // If the actions couldn't be decoded then returns null. - static decodeActions(actions: Action[]): IEncodedActions { + static decodeActions(actions: PushRuleAction[]): IEncodedActions { let notify = false; let sound = null; let highlight = false; for (let i = 0; i < actions.length; ++i) { const action = actions[i]; - if (action === Actions.Notify) { + if (action === PushRuleActionName.Notify) { notify = true; - } else if (action === Actions.DontNotify) { + } else if (action === PushRuleActionName.DontNotify) { notify = false; } else if (typeof action === "object") { if (action.set_tweak === "sound") { diff --git a/src/notifications/PushRuleVectorState.ts b/src/notifications/PushRuleVectorState.ts index 78c7e4b43be..c0855af0b99 100644 --- a/src/notifications/PushRuleVectorState.ts +++ b/src/notifications/PushRuleVectorState.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,7 +16,7 @@ limitations under the License. import { StandardActions } from "./StandardActions"; import { NotificationUtils } from "./NotificationUtils"; -import { IPushRule } from "./types"; +import { IPushRule } from "matrix-js-sdk/src/@types/PushRules"; export enum State { /** The push rule is disabled */ diff --git a/src/notifications/types.ts b/src/notifications/types.ts deleted file mode 100644 index ea465529470..00000000000 --- a/src/notifications/types.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -export enum NotificationSetting { - AllMessages = "all_messages", // .m.rule.message = notify - DirectMessagesMentionsKeywords = "dm_mentions_keywords", // .m.rule.message = mark_unread. This is the new default. - MentionsKeywordsOnly = "mentions_keywords", // .m.rule.message = mark_unread; .m.rule.room_one_to_one = mark_unread - Never = "never", // .m.rule.master = enabled (dont_notify) -} - -export interface ISoundTweak { - // eslint-disable-next-line camelcase - set_tweak: "sound"; - value: string; -} -export interface IHighlightTweak { - // eslint-disable-next-line camelcase - set_tweak: "highlight"; - value?: boolean; -} - -export type Tweak = ISoundTweak | IHighlightTweak; - -export enum Actions { - Notify = "notify", - DontNotify = "dont_notify", // no-op - Coalesce = "coalesce", // unused - MarkUnread = "mark_unread", // new -} - -export type Action = Actions | Tweak; - -// Push rule kinds in descending priority order -export enum Kind { - Override = "override", - ContentSpecific = "content", - RoomSpecific = "room", - SenderSpecific = "sender", - Underride = "underride", -} - -export interface IEventMatchCondition { - kind: "event_match"; - key: string; - pattern: string; -} - -export interface IContainsDisplayNameCondition { - kind: "contains_display_name"; -} - -export interface IRoomMemberCountCondition { - kind: "room_member_count"; - is: string; -} - -export interface ISenderNotificationPermissionCondition { - kind: "sender_notification_permission"; - key: string; -} - -export type Condition = - IEventMatchCondition | - IContainsDisplayNameCondition | - IRoomMemberCountCondition | - ISenderNotificationPermissionCondition; - -export enum RuleIds { - MasterRule = ".m.rule.master", // The master rule (all notifications disabling) - MessageRule = ".m.rule.message", - EncryptedMessageRule = ".m.rule.encrypted", - RoomOneToOneRule = ".m.rule.room_one_to_one", - EncryptedRoomOneToOneRule = ".m.rule.room_one_to_one", -} - -export interface IPushRule { - enabled: boolean; - // eslint-disable-next-line camelcase - rule_id: RuleIds | string; - actions: Action[]; - default: boolean; - conditions?: Condition[]; // only applicable to `underride` and `override` rules - pattern?: string; // only applicable to `content` rules -} - -// push rule extended with kind, used by ContentRules and js-sdk's pushprocessor -export interface IExtendedPushRule extends IPushRule { - kind: Kind; -} - -export interface IPushRuleSet { - override: IPushRule[]; - content: IPushRule[]; - room: IPushRule[]; - sender: IPushRule[]; - underride: IPushRule[]; -} - -export interface IRuleSets { - global: IPushRuleSet; -} From 0e749e32ac3824c885fe529fa8294de09de83879 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Jul 2021 20:53:12 -0600 Subject: [PATCH 06/16] Clarify that vectorState is a VectorState --- src/notifications/ContentRules.ts | 18 +++++++++--------- src/notifications/PushRuleVectorState.ts | 22 +++++++++++----------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/notifications/ContentRules.ts b/src/notifications/ContentRules.ts index fe27bfd67b4..2b45065568b 100644 --- a/src/notifications/ContentRules.ts +++ b/src/notifications/ContentRules.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { PushRuleVectorState, State } from "./PushRuleVectorState"; +import { PushRuleVectorState, VectorState } from "./PushRuleVectorState"; import { IAnnotatedPushRule, IPushRules, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules"; export interface IContentRules { - vectorState: State; + vectorState: VectorState; rules: IAnnotatedPushRule[]; externalRules: IAnnotatedPushRule[]; } @@ -58,7 +58,7 @@ export class ContentRules { if (contentRules.loud.length) { return { - vectorState: State.Loud, + vectorState: VectorState.Loud, rules: contentRules.loud, externalRules: [ ...contentRules.loud_but_disabled, @@ -69,25 +69,25 @@ export class ContentRules { }; } else if (contentRules.loud_but_disabled.length) { return { - vectorState: State.Off, + vectorState: VectorState.Off, rules: contentRules.loud_but_disabled, externalRules: [...contentRules.on, ...contentRules.on_but_disabled, ...contentRules.other], }; } else if (contentRules.on.length) { return { - vectorState: State.On, + vectorState: VectorState.On, rules: contentRules.on, externalRules: [...contentRules.on_but_disabled, ...contentRules.other], }; } else if (contentRules.on_but_disabled.length) { return { - vectorState: State.Off, + vectorState: VectorState.Off, rules: contentRules.on_but_disabled, externalRules: contentRules.other, }; } else { return { - vectorState: State.On, + vectorState: VectorState.On, rules: [], externalRules: contentRules.other, }; @@ -116,14 +116,14 @@ export class ContentRules { r.kind = kind; switch (PushRuleVectorState.contentRuleVectorStateKind(r)) { - case State.On: + case VectorState.On: if (r.enabled) { contentRules.on.push(r); } else { contentRules.on_but_disabled.push(r); } break; - case State.Loud: + case VectorState.Loud: if (r.enabled) { contentRules.loud.push(r); } else { diff --git a/src/notifications/PushRuleVectorState.ts b/src/notifications/PushRuleVectorState.ts index c0855af0b99..34f7dcf786a 100644 --- a/src/notifications/PushRuleVectorState.ts +++ b/src/notifications/PushRuleVectorState.ts @@ -18,7 +18,7 @@ import { StandardActions } from "./StandardActions"; import { NotificationUtils } from "./NotificationUtils"; import { IPushRule } from "matrix-js-sdk/src/@types/PushRules"; -export enum State { +export enum VectorState { /** The push rule is disabled */ Off = "off", /** The user will receive push notification for this rule */ @@ -30,26 +30,26 @@ export enum State { export class PushRuleVectorState { // Backwards compatibility (things should probably be using the enum above instead) - static OFF = State.Off; - static ON = State.On; - static LOUD = State.Loud; + static OFF = VectorState.Off; + static ON = VectorState.On; + static LOUD = VectorState.Loud; /** * Enum for state of a push rule as defined by the Vector UI. * @readonly * @enum {string} */ - static states = State; + static states = VectorState; /** * Convert a PushRuleVectorState to a list of actions * * @return [object] list of push-rule actions */ - static actionsFor(pushRuleVectorState: State) { - if (pushRuleVectorState === State.On) { + static actionsFor(pushRuleVectorState: VectorState) { + if (pushRuleVectorState === VectorState.On) { return StandardActions.ACTION_NOTIFY; - } else if (pushRuleVectorState === State.Loud) { + } else if (pushRuleVectorState === VectorState.Loud) { return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND; } } @@ -61,7 +61,7 @@ export class PushRuleVectorState { * category or in PushRuleVectorState.LOUD, regardless of its enabled * state. Returns null if it does not match these categories. */ - static contentRuleVectorStateKind(rule: IPushRule): State { + static contentRuleVectorStateKind(rule: IPushRule): VectorState { const decoded = NotificationUtils.decodeActions(rule.actions); if (!decoded) { @@ -79,10 +79,10 @@ export class PushRuleVectorState { let stateKind = null; switch (tweaks) { case 0: - stateKind = State.On; + stateKind = VectorState.On; break; case 2: - stateKind = State.Loud; + stateKind = VectorState.Loud; break; } return stateKind; From fd5a36fd0cf6131b25008d02fa0e6769b3e3633d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Jul 2021 21:48:20 -0600 Subject: [PATCH 07/16] Fix more types around notifications --- src/notifications/NotificationUtils.ts | 2 +- .../VectorPushRulesDefinitions.ts | 117 ++++++++---------- 2 files changed, 56 insertions(+), 63 deletions(-) diff --git a/src/notifications/NotificationUtils.ts b/src/notifications/NotificationUtils.ts index fa7aa1186d9..3f07c56972d 100644 --- a/src/notifications/NotificationUtils.ts +++ b/src/notifications/NotificationUtils.ts @@ -29,7 +29,7 @@ export class NotificationUtils { // "highlight: true/false, // } // to a list of push actions. - static encodeActions(action: IEncodedActions) { + static encodeActions(action: IEncodedActions): PushRuleAction[] { const notify = action.notify; const sound = action.sound; const highlight = action.highlight; diff --git a/src/notifications/VectorPushRulesDefinitions.ts b/src/notifications/VectorPushRulesDefinitions.ts index 38dd88e6c6a..a8c617e786b 100644 --- a/src/notifications/VectorPushRulesDefinitions.ts +++ b/src/notifications/VectorPushRulesDefinitions.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,19 +16,24 @@ limitations under the License. import { _td } from '../languageHandler'; import { StandardActions } from "./StandardActions"; -import { PushRuleVectorState } from "./PushRuleVectorState"; +import { PushRuleVectorState, VectorState } from "./PushRuleVectorState"; import { NotificationUtils } from "./NotificationUtils"; +import { PushRuleAction, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules"; + +type StateToActionsMap = { + [state in VectorState]?: PushRuleAction[]; +}; interface IProps { - kind: Kind; + kind: PushRuleKind; description: string; - vectorStateToActions: Action; + vectorStateToActions: StateToActionsMap; } class VectorPushRuleDefinition { - private kind: Kind; + private kind: PushRuleKind; private description: string; - private vectorStateToActions: Action; + public readonly vectorStateToActions: StateToActionsMap; constructor(opts: IProps) { this.kind = opts.kind; @@ -73,73 +77,62 @@ class VectorPushRuleDefinition { } } -enum Kind { - Override = "override", - Underride = "underride", -} - -interface Action { - on: StandardActions; - loud: StandardActions; - off: StandardActions; -} - /** * The descriptions of rules managed by the Vector UI. */ export const VectorPushRulesDefinitions = { // Messages containing user's display name ".m.rule.contains_display_name": new VectorPushRuleDefinition({ - kind: Kind.Override, + kind: PushRuleKind.Override, description: _td("Messages containing my display name"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), // Messages containing user's username (localpart/MXID) ".m.rule.contains_user_name": new VectorPushRuleDefinition({ - kind: Kind.Override, + kind: PushRuleKind.Override, description: _td("Messages containing my username"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), // Messages containing @room ".m.rule.roomnotif": new VectorPushRuleDefinition({ - kind: Kind.Override, + kind: PushRuleKind.Override, description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_HIGHLIGHT, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), // Messages just sent to the user in a 1:1 room ".m.rule.room_one_to_one": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("Messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY, }, }), // Encrypted messages just sent to the user in a 1:1 room ".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY, }, }), @@ -147,12 +140,12 @@ export const VectorPushRulesDefinitions = { // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. ".m.rule.message": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("Messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY, }, }), @@ -160,57 +153,57 @@ export const VectorPushRulesDefinitions = { // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. ".m.rule.encrypted": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY, }, }), // Invitation for the user ".m.rule.invite_for_me": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("When I'm invited to a room"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), // Incoming call ".m.rule.call": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("Call invitation"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_RING_SOUND, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_RING_SOUND, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), // Notifications from bots ".m.rule.suppress_notices": new VectorPushRuleDefinition({ - kind: Kind.Override, + kind: PushRuleKind.Override, description: _td("Messages sent by bot"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI - on: StandardActions.ACTION_DISABLED, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, + [VectorState.On]: StandardActions.ACTION_DISABLED, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY, }, }), // Room upgrades (tombstones) ".m.rule.tombstone": new VectorPushRuleDefinition({ - kind: Kind.Override, + kind: PushRuleKind.Override, description: _td("When rooms are upgraded"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_HIGHLIGHT, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), }; From 3ae76c84f6fae2292df8fb678f6034c07652e292 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Jul 2021 23:55:08 -0600 Subject: [PATCH 08/16] Add a simple TagComposer for the keywords entry --- res/css/_components.scss | 3 +- res/css/views/elements/_TagComposer.scss | 77 ++++++++++++++++ res/img/subtract.svg | 3 + src/components/views/elements/TagComposer.tsx | 91 +++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 res/css/views/elements/_TagComposer.scss create mode 100644 res/img/subtract.svg create mode 100644 src/components/views/elements/TagComposer.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 8f80f1bf97a..c623eba9d84 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -148,6 +148,7 @@ @import "./views/elements/_StyledCheckbox.scss"; @import "./views/elements/_StyledRadioButton.scss"; @import "./views/elements/_SyntaxHighlight.scss"; +@import "./views/elements/_TagComposer.scss"; @import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_Tooltip.scss"; @@ -260,9 +261,9 @@ @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallContainer.scss"; +@import "./views/voip/_CallPreview.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_CallViewForRoom.scss"; -@import "./views/voip/_CallPreview.scss"; @import "./views/voip/_DialPad.scss"; @import "./views/voip/_DialPadContextMenu.scss"; @import "./views/voip/_DialPadModal.scss"; diff --git a/res/css/views/elements/_TagComposer.scss b/res/css/views/elements/_TagComposer.scss new file mode 100644 index 00000000000..2ffd6017655 --- /dev/null +++ b/res/css/views/elements/_TagComposer.scss @@ -0,0 +1,77 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_TagComposer { + .mx_TagComposer_input { + display: flex; + + .mx_Field { + flex: 1; + margin: 0; // override from field styles + } + + .mx_AccessibleButton { + min-width: 70px; + padding: 0; // override from button styles + margin-left: 16px; // distance from + } + + .mx_Field, .mx_Field input, .mx_AccessibleButton { + // So they look related to each other by feeling the same + border-radius: 8px; + } + } + + .mx_TagComposer_tags { + display: flex; + flex-wrap: wrap; + margin-top: 12px; // this plus 12px from the tags makes 24px from the input + + .mx_TagComposer_tag { + padding: 6px 8px 8px 12px; + position: relative; + margin-right: 12px; + margin-top: 12px; + + // Cheaty way to get an opacified variable colour background + &::before { + content: ''; + border-radius: 20px; + background-color: $tertiary-fg-color; + opacity: 0.15; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + // Pass through the pointer otherwise we have effectively put a whole div + // on top of the component, which makes it hard to interact with buttons. + pointer-events: none; + } + } + + .mx_AccessibleButton { + background-image: url('$(res)/img/subtract.svg'); + width: 16px; + height: 16px; + margin-left: 8px; + display: inline-block; + vertical-align: middle; + cursor: pointer; + } + } +} diff --git a/res/img/subtract.svg b/res/img/subtract.svg new file mode 100644 index 00000000000..55e25831ef5 --- /dev/null +++ b/res/img/subtract.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/views/elements/TagComposer.tsx b/src/components/views/elements/TagComposer.tsx new file mode 100644 index 00000000000..ff104748a07 --- /dev/null +++ b/src/components/views/elements/TagComposer.tsx @@ -0,0 +1,91 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { ChangeEvent, FormEvent } from "react"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Field from "./Field"; +import { _t } from "../../../languageHandler"; +import AccessibleButton from "./AccessibleButton"; + +interface IProps { + tags: string[]; + onAdd: (tag: string) => void; + onRemove: (tag: string) => void; + disabled?: boolean; + label?: string; + placeholder?: string; +} + +interface IState { + newTag: string; +} + +/** + * A simple, controlled, composer for entering string tags. Contains a simple + * input, add button, and per-tag remove button. + */ +@replaceableComponent("views.elements.TagComposer") +export default class TagComposer extends React.PureComponent { + public constructor(props: IProps) { + super(props); + + this.state = { + newTag: "", + }; + } + + private onInputChange = (ev: ChangeEvent) => { + this.setState({ newTag: ev.target.value }); + }; + + private onAdd = (ev: FormEvent) => { + ev.preventDefault(); + if (!this.state.newTag) return; + + this.props.onAdd(this.state.newTag); + this.setState({ newTag: "" }); + }; + + private onRemove = (tag: string) => { + // We probably don't need to proxy this, but for + // sanity of `this` we'll do so anyways. + this.props.onRemove(tag); + }; + + public render() { + return
+
+ + + { _t("Add") } + + +
+ { this.props.tags.map((t, i) => (
+ { t } + +
)) } +
+
; + } +} From ff7a18da562ae6559769e4a2f3ecb637c293ddf1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Jul 2021 23:57:54 -0600 Subject: [PATCH 09/16] Rewrite Notifications component for modern UI & processing --- res/css/views/settings/_Notifications.scss | 155 +- .../views/settings/Notifications.tsx | 1248 +++++++---------- src/i18n/strings/en_EN.json | 11 +- 3 files changed, 605 insertions(+), 809 deletions(-) diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss index 77a7bc5b68e..2ec9f3fbeac 100644 --- a/res/css/views/settings/_Notifications.scss +++ b/res/css/views/settings/_Notifications.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,82 +14,79 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_UserNotifSettings_tableRow { - display: table-row; -} - -.mx_UserNotifSettings_inputCell { - display: table-cell; - padding-bottom: 8px; - padding-right: 8px; - width: 16px; -} - -.mx_UserNotifSettings_labelCell { - padding-bottom: 8px; - width: 400px; - display: table-cell; -} - -.mx_UserNotifSettings_pushRulesTableWrapper { - padding-bottom: 8px; -} - -.mx_UserNotifSettings_pushRulesTable { - width: 100%; - table-layout: fixed; -} - -.mx_UserNotifSettings_pushRulesTable thead { - font-weight: bold; -} - -.mx_UserNotifSettings_pushRulesTable tbody th { - font-weight: 400; -} - -.mx_UserNotifSettings_pushRulesTable tbody th:first-child { - text-align: left; -} - -.mx_UserNotifSettings_keywords { - cursor: pointer; - color: $accent-color; -} - -.mx_UserNotifSettings_devicesTable td { - padding-left: 20px; - padding-right: 20px; -} - -.mx_UserNotifSettings_notifTable { - display: table; - position: relative; -} - -.mx_UserNotifSettings_notifTable .mx_Spinner { - position: absolute; -} - -.mx_NotificationSound_soundUpload { - display: none; -} - -.mx_NotificationSound_browse { - color: $accent-color; - border: 1px solid $accent-color; - background-color: transparent; -} - -.mx_NotificationSound_save { - margin-left: 5px; - color: white; - background-color: $accent-color; -} - -.mx_NotificationSound_resetSound { - margin-top: 5px; - color: white; - border: $warning-color; - background-color: $warning-color; +.mx_UserNotifSettings { + color: $primary-fg-color; // override from default settings page styles + + .mx_UserNotifSettings_pushRulesTable { + width: calc(100% + 12px); // +12px to line up center of 'Noisy' column with toggle switches + table-layout: fixed; + border-collapse: collapse; + border-spacing: 0; + margin-top: 40px; + + tr > th { + font-weight: 600; // semi bold + } + + tr > th:first-child { + text-align: left; + font-size: $font-18px; + } + + tr > th:nth-child(n + 2) { + color: $secondary-fg-color; + font-size: $font-12px; + vertical-align: middle; + width: 66px; + } + + tr > td:nth-child(n + 2) { + text-align: center; + } + + tr > td { + padding-top: 8px; + } + + // Override StyledRadioButton default styles + .mx_RadioButton { + justify-content: center; + + .mx_RadioButton_content { + display: none; + } + + .mx_RadioButton_spacer { + display: none; + } + } + } + + .mx_UserNotifSettings_floatingSection { + margin-top: 40px; + + & > div:first-child { // section header + font-size: $font-18px; + font-weight: 600; // semi bold + } + + > table { + border-collapse: collapse; + border-spacing: 0; + margin-top: 8px; + + tr > td:first-child { + // Just for a bit of spacing + padding-right: 8px; + } + } + } + + .mx_UserNotifSettings_clearNotifsButton { + margin-top: 8px; + } + + .mx_TagComposer { + margin-top: 35px; // lots of distance from the last line of the table + } } diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 9f1929a35f9..4a733d7bf58 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,843 +14,634 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import SettingsStore from '../../../settings/SettingsStore'; -import Modal from '../../../Modal'; +import React from "react"; +import Spinner from "../elements/Spinner"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { IAnnotatedPushRule, IPusher, PushRuleKind, RuleId, } from "matrix-js-sdk/src/@types/PushRules"; import { - VectorPushRulesDefinitions, - PushRuleVectorState, ContentRules, -} from '../../../notifications'; -import SdkConfig from "../../../SdkConfig"; + IContentRules, + PushRuleVectorState, + VectorPushRulesDefinitions, + VectorState, +} from "../../../notifications"; +import { _t, TranslatedString } from "../../../languageHandler"; +import { IThirdPartyIdentifier, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; -import AccessibleButton from "../elements/AccessibleButton"; +import SettingsStore from "../../../settings/SettingsStore"; +import StyledRadioButton from "../elements/StyledRadioButton"; import { SettingLevel } from "../../../settings/SettingLevel"; -import { UIFeature } from "../../../settings/UIFeature"; -import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Modal from "../../../Modal"; +import ErrorDialog from "../dialogs/ErrorDialog"; +import SdkConfig from "../../../SdkConfig"; +import AccessibleButton from "../elements/AccessibleButton"; +import TagComposer from "../elements/TagComposer"; +import { objectClone } from "../../../utils/objects"; +import { arrayDiff } from "../../../utils/arrays"; // TODO: this "view" component still has far too much application logic in it, // which should be factored out to other files. -// TODO: this component also does a lot of direct poking into this.state, which -// is VERY NAUGHTY. +enum Phase { + Loading = "loading", + Ready = "ready", + Persisting = "persisting", // technically a meta-state for Ready, but whatever + Error = "error", +} -@replaceableComponent("views.settings.Notifications") -export default class Notifications extends React.Component { - static phases = { - LOADING: "LOADING", // The component is loading or sending data to the hs - DISPLAY: "DISPLAY", // The component is ready and display data - ERROR: "ERROR", // There was an error - }; +enum RuleClass { + Master = "master", - state = { - phase: Notifications.phases.LOADING, - masterPushRule: undefined, // The master rule ('.m.rule.master') - vectorPushRules: [], // HS default push rules displayed in Vector UI - vectorContentRules: { // Keyword push rules displayed in Vector UI - vectorState: PushRuleVectorState.ON, - rules: [], - }, - externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI - externalContentRules: [], // Keyword push rules that have been defined outside Vector UI - threepids: [], // used for email notifications - pushers: undefined, - }; - - componentDidMount() { - this._refreshFromServer(); - } + // The vector sections map approximately to UI sections + VectorGlobal = "vector_global", + VectorMentions = "vector_mentions", + VectorOther = "vector_other", + Other = "other", // unknown rules, essentially +} - onEnableNotificationsChange = (checked) => { - const self = this; - this.setState({ - phase: Notifications.phases.LOADING, - }); +const KEYWORD_RULE_ID = "_keywords"; // used as a placeholder "Rule ID" throughout this component +const KEYWORD_RULE_CATEGORY = RuleClass.VectorMentions; + +// This array doesn't care about categories: it's just used for a simple sort +const RULE_DISPLAY_ORDER: string[] = [ + // Global + RuleId.DM, + RuleId.EncryptedDM, + RuleId.Message, + RuleId.EncryptedMessage, + + // Mentions + RuleId.ContainsDisplayName, + RuleId.ContainsUserName, + RuleId.AtRoomNotification, + + // Other + RuleId.InviteToSelf, + RuleId.IncomingCall, + RuleId.SuppressNotices, + RuleId.Tombstone, +] + +interface IVectorPushRule { + ruleId: RuleId | typeof KEYWORD_RULE_ID | string; + rule?: IAnnotatedPushRule; + description: TranslatedString | string; + vectorState: VectorState; +} - MatrixClientPeg.get().setPushRuleEnabled( - 'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked, - ).then(function() { - self._refreshFromServer(); - }); - }; +interface IProps {} - onEnableDesktopNotificationsChange = (checked) => { - SettingsStore.setValue( - "notificationsEnabled", null, - SettingLevel.DEVICE, - checked, - ).finally(() => { - this.forceUpdate(); - }); - }; +interface IState { + phase: Phase; - onEnableDesktopNotificationBodyChange = (checked) => { - SettingsStore.setValue( - "notificationBodyEnabled", null, - SettingLevel.DEVICE, - checked, - ).finally(() => { - this.forceUpdate(); - }); + // Optional stuff is required when `phase === Ready` + masterPushRule?: IAnnotatedPushRule; + vectorKeywordRuleInfo?: IContentRules; + vectorPushRules?: { + [category in RuleClass]?: IVectorPushRule[]; }; + pushers?: IPusher[]; + threepids?: IThirdPartyIdentifier[]; +} - onEnableAudioNotificationsChange = (checked) => { - SettingsStore.setValue( - "audioNotificationsEnabled", null, - SettingLevel.DEVICE, - checked, - ).finally(() => { - this.forceUpdate(); - }); - }; +export default class Notifications extends React.PureComponent { + public constructor(props: IProps) { + super(props); - /* - * Returns the email pusher (pusher of type 'email') for a given - * email address. Email pushers all have the same app ID, so since - * pushers are unique over (app ID, pushkey), there will be at most - * one such pusher. - */ - getEmailPusher(pushers, address) { - if (pushers === undefined) { - return undefined; - } - for (let i = 0; i < pushers.length; ++i) { - if (pushers[i].kind === 'email' && pushers[i].pushkey === address) { - return pushers[i]; - } - } - return undefined; + this.state = { + phase: Phase.Loading, + }; } - onEnableEmailNotificationsChange = (address, checked) => { - let emailPusherPromise; - if (checked) { - const data = {}; - data['brand'] = SdkConfig.get().brand; - emailPusherPromise = MatrixClientPeg.get().setPusher({ - kind: 'email', - app_id: 'm.email', - pushkey: address, - app_display_name: 'Email Notifications', - device_display_name: address, - lang: navigator.language, - data: data, - append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address - }); - } else { - const emailPusher = this.getEmailPusher(this.state.pushers, address); - emailPusher.kind = null; - emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher); - } - emailPusherPromise.then(() => { - this._refreshFromServer(); - }, (error) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Error saving email notification preferences', '', ErrorDialog, { - title: _t('Error saving email notification preferences'), - description: _t('An error occurred whilst saving your email notification preferences.'), - }); - }); - }; + private get isInhibited(): boolean { + // Caution: The master rule's enabled state is inverted from expectation. When + // the master rule is *enabled* it means all other rules are *disabled* (or + // inhibited). Conversely, when the master rule is *disabled* then all other rules + // are *enabled* (or operate fine). + return this.state.masterPushRule?.enabled; + } - onNotifStateButtonClicked = (event) => { - // FIXME: use .bind() rather than className metadata here surely - const vectorRuleId = event.target.className.split("-")[0]; - const newPushRuleVectorState = event.target.className.split("-")[1]; + public componentDidMount() { + // noinspection JSIgnoredPromiseFromCall + this.refreshFromServer(); + } - if ("_keywords" === vectorRuleId) { - this._setKeywordsPushRuleVectorState(newPushRuleVectorState); - } else { - const rule = this.getRule(vectorRuleId); - if (rule) { - this._setPushRuleVectorState(rule, newPushRuleVectorState); - } - } - }; + private async refreshFromServer() { + try { + const newState = (await Promise.all([ + this.refreshRules(), + this.refreshPushers(), + this.refreshThreepids(), + ])).reduce((p, c) => Object.assign(c, p), {}); - onKeywordsClicked = (event) => { - // Compute the keywords list to display - let keywords: any[]|string = []; - for (const i in this.state.vectorContentRules.rules) { - const rule = this.state.vectorContentRules.rules[i]; - keywords.push(rule.pattern); + this.setState({ + ...newState, + phase: Phase.Ready, + }); + } catch (e) { + console.error("Error setting up notifications for settings: ", e); + this.setState({ phase: Phase.Error }); } - if (keywords.length) { - // As keeping the order of per-word push rules hs side is a bit tricky to code, - // display the keywords in alphabetical order to the user - keywords.sort(); + } - keywords = keywords.join(", "); - } else { - keywords = ""; - } + private async refreshRules(): Promise> { + const ruleSets = await MatrixClientPeg.get().getPushRules(); - const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); - Modal.createTrackedDialog('Keywords Dialog', '', TextInputDialog, { - title: _t('Keywords'), - description: _t('Enter keywords separated by a comma:'), - button: _t('OK'), - value: keywords, - onFinished: (shouldLeave, newValue) => { - if (shouldLeave && newValue !== keywords) { - let newKeywords = newValue.split(','); - for (const i in newKeywords) { - newKeywords[i] = newKeywords[i].trim(); - } + const categories = { + [RuleId.Master]: RuleClass.Master, - // Remove duplicates and empty - newKeywords = newKeywords.reduce(function(array, keyword) { - if (keyword !== "" && array.indexOf(keyword) < 0) { - array.push(keyword); - } - return array; - }, []); + [RuleId.DM]: RuleClass.VectorGlobal, + [RuleId.EncryptedDM]: RuleClass.VectorGlobal, + [RuleId.Message]: RuleClass.VectorGlobal, + [RuleId.EncryptedMessage]: RuleClass.VectorGlobal, - this._setKeywords(newKeywords); - } - }, - }); - }; + [RuleId.ContainsDisplayName]: RuleClass.VectorMentions, + [RuleId.ContainsUserName]: RuleClass.VectorMentions, + [RuleId.AtRoomNotification]: RuleClass.VectorMentions, - getRule(vectorRuleId) { - for (const i in this.state.vectorPushRules) { - const rule = this.state.vectorPushRules[i]; - if (rule.vectorRuleId === vectorRuleId) { - return rule; - } - } - } + [RuleId.InviteToSelf]: RuleClass.VectorOther, + [RuleId.IncomingCall]: RuleClass.VectorOther, + [RuleId.SuppressNotices]: RuleClass.VectorOther, + [RuleId.Tombstone]: RuleClass.VectorOther, - _setPushRuleVectorState(rule, newPushRuleVectorState) { - if (rule && rule.vectorState !== newPushRuleVectorState) { - this.setState({ - phase: Notifications.phases.LOADING, - }); + // Everything maps to a generic "other" (unknown rule) + }; - const self = this; - const cli = MatrixClientPeg.get(); - const deferreds = []; - const ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId]; + const defaultRules: { + [k in RuleClass]: IAnnotatedPushRule[]; + } = { + [RuleClass.Master]: [], + [RuleClass.VectorGlobal]: [], + [RuleClass.VectorMentions]: [], + [RuleClass.VectorOther]: [], + [RuleClass.Other]: [], + }; - if (rule.rule) { - const actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState]; + for (const k in ruleSets.global) { + // noinspection JSUnfilteredForInLoop + const kind = k as PushRuleKind; + for (const r of ruleSets.global[kind]) { + const rule: IAnnotatedPushRule = Object.assign(r, {kind}); + const category = categories[rule.rule_id] ?? RuleClass.Other; - if (!actions) { - // The new state corresponds to disabling the rule. - deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false)); - } else { - // The new state corresponds to enabling the rule and setting specific actions - deferreds.push(this._updatePushRuleActions(rule.rule, actions, true)); + if (rule.rule_id[0] === '.') { + defaultRules[category].push(rule); } } - - Promise.all(deferreds).then(function() { - self._refreshFromServer(); - }, function(error) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to change settings: " + error); - Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, { - title: _t('Failed to change settings'), - description: ((error && error.message) ? error.message : _t('Operation failed')), - onFinished: self._refreshFromServer, - }); - }); } - } - _setKeywordsPushRuleVectorState(newPushRuleVectorState) { - // Is there really a change? - if (this.state.vectorContentRules.vectorState === newPushRuleVectorState - || this.state.vectorContentRules.rules.length === 0) { - return; + const preparedNewState: Partial = {}; + if (defaultRules.master.length > 0) { + preparedNewState.masterPushRule = defaultRules.master[0]; + } else { + // XXX: Can this even happen? How do we safely recover? + throw new Error("Failed to locate a master push rule"); } - const self = this; - const cli = MatrixClientPeg.get(); - - this.setState({ - phase: Notifications.phases.LOADING, - }); - - // Update all rules in self.state.vectorContentRules - const deferreds = []; - for (const i in this.state.vectorContentRules.rules) { - const rule = this.state.vectorContentRules.rules[i]; + // Parse keyword rules + preparedNewState.vectorKeywordRuleInfo = ContentRules.parseContentRules(ruleSets); + + // Prepare rendering for all of our known rules + preparedNewState.vectorPushRules = {}; + const vectorCategories = [RuleClass.VectorGlobal, RuleClass.VectorMentions, RuleClass.VectorOther]; + for (const category of vectorCategories) { + preparedNewState.vectorPushRules[category] = []; + for (const rule of defaultRules[category]) { + const definition = VectorPushRulesDefinitions[rule.rule_id]; + const vectorState = definition.ruleToVectorState(rule); + preparedNewState.vectorPushRules[category].push({ + ruleId: rule.rule_id, + rule, vectorState, + description: _t(definition.description), + }); - let enabled; let actions; - switch (newPushRuleVectorState) { - case PushRuleVectorState.ON: - if (rule.actions.length !== 1) { - actions = PushRuleVectorState.actionsFor(PushRuleVectorState.ON); + // XXX: Do we need this block from the previous component? + /* + // if there was a rule which we couldn't parse, add it to the external list + if (rule && !vectorState) { + rule.description = ruleDefinition.description; + self.state.externalPushRules.push(rule); } + */ + } - if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { - enabled = true; - } - break; + // Quickly sort the rules for display purposes + preparedNewState.vectorPushRules[category].sort((a, b) => { + let idxA = RULE_DISPLAY_ORDER.indexOf(a.ruleId); + let idxB = RULE_DISPLAY_ORDER.indexOf(b.ruleId); - case PushRuleVectorState.LOUD: - if (rule.actions.length !== 3) { - actions = PushRuleVectorState.actionsFor(PushRuleVectorState.LOUD); - } + // Assume unknown things go at the end + if (idxA < 0) idxA = RULE_DISPLAY_ORDER.length; + if (idxB < 0) idxB = RULE_DISPLAY_ORDER.length; - if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { - enabled = true; - } - break; + return idxA - idxB; + }); - case PushRuleVectorState.OFF: - enabled = false; - break; + if (category === KEYWORD_RULE_CATEGORY) { + preparedNewState.vectorPushRules[category].push({ + ruleId: KEYWORD_RULE_ID, + description: _t("Messages containing keywords"), + vectorState: preparedNewState.vectorKeywordRuleInfo.vectorState, + }); } + } - if (actions) { - // Note that the workaround in _updatePushRuleActions will automatically - // enable the rule - deferreds.push(this._updatePushRuleActions(rule, actions, enabled)); - } else if (enabled != undefined) { - deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled)); + // XXX: Do we need this block from the previous component? + /* + // Build the rules not managed by Vector UI + const otherRulesDescriptions = { + '.m.rule.message': _t('Notify for all other messages/rooms'), + '.m.rule.fallback': _t('Notify me for anything else'), + }; + + for (const i in defaultRules.others) { + const rule = defaultRules.others[i]; + const ruleDescription = otherRulesDescriptions[rule.rule_id]; + + // Show enabled default rules that was modified by the user + if (ruleDescription && rule.enabled && !rule.default) { + rule.description = ruleDescription; + self.state.externalPushRules.push(rule); + } } - } + */ - Promise.all(deferreds).then(function(resps) { - self._refreshFromServer(); - }, function(error) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Can't update user notification settings: " + error); - Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, { - title: _t('Can\'t update user notification settings'), - description: ((error && error.message) ? error.message : _t('Operation failed')), - onFinished: self._refreshFromServer, - }); - }); + return preparedNewState; } - _setKeywords(newKeywords) { - this.setState({ - phase: Notifications.phases.LOADING, - }); + private async refreshPushers(): Promise> { + return { ...(await MatrixClientPeg.get().getPushers()) }; + } - const self = this; - const cli = MatrixClientPeg.get(); - const removeDeferreds = []; + private async refreshThreepids(): Promise> { + return { ...(await MatrixClientPeg.get().getThreePids()) }; + } - // Remove per-word push rules of keywords that are no more in the list - const vectorContentRulesPatterns = []; - for (const i in self.state.vectorContentRules.rules) { - const rule = self.state.vectorContentRules.rules[i]; + private showSaveError() { + Modal.createTrackedDialog('Error saving notification preferences', '', ErrorDialog, { + title: _t('Error saving notification preferences'), + description: _t('An error occurred whilst saving your notification preferences.'), + }); + } - vectorContentRulesPatterns.push(rule.pattern); + private onMasterRuleChanged = async (checked: boolean) => { + this.setState({ phase: Phase.Persisting }); + + try { + const masterRule = this.state.masterPushRule; + await MatrixClientPeg.get().setPushRuleEnabled('global', masterRule.kind, masterRule.rule_id, !checked); + await this.refreshFromServer(); + } catch (e) { + this.setState({ phase: Phase.Error }); + console.error("Error updating master push rule:", e); + this.showSaveError(); + } + }; - if (newKeywords.indexOf(rule.pattern) < 0) { - removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); + private onEmailNotificationsChanged = async (email: string, checked: boolean) => { + this.setState({ phase: Phase.Persisting }); + + try { + if (checked) { + await MatrixClientPeg.get().setPusher({ + kind: "email", + app_id: "m.email", + pushkey: email, + app_display_name: "Email Notifications", + device_display_name: email, + lang: navigator.language, + data: { + brand: SdkConfig.get().brand, + }, + + // We always append for email pushers since we don't want to stop other + // accounts notifying to the same email address + append: true, + }); + } else { + const pusher = this.state.pushers.find(p => p.kind === "email" && p.pushkey === email); + pusher.kind = null; // flag for delete + await MatrixClientPeg.get().setPusher(pusher); } + + await this.refreshFromServer(); + } catch (e) { + this.setState({ phase: Phase.Error }); + console.error("Error updating email pusher:", e); + this.showSaveError(); } + }; - // If the keyword is part of `externalContentRules`, remove the rule - // before recreating it in the right Vector path - for (const i in self.state.externalContentRules) { - const rule = self.state.externalContentRules[i]; + private onDesktopNotificationsChanged = async (checked: boolean) => { + await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, checked); + this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue() + }; - if (newKeywords.indexOf(rule.pattern) >= 0) { - removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); - } - } + private onDesktopShowBodyChanged = async (checked: boolean) => { + await SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, checked); + this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue() + }; - const onError = function(error) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to update keywords: " + error); - Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, { - title: _t('Failed to update keywords'), - description: ((error && error.message) ? error.message : _t('Operation failed')), - onFinished: self._refreshFromServer, - }); - }; + private onAudioNotificationsChanged = async (checked: boolean) => { + await SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, checked); + this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue() + }; - // Then, add the new ones - Promise.all(removeDeferreds).then(function(resps) { - const deferreds = []; + private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState) => { + this.setState({ phase: Phase.Persisting }); - let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState; - if (pushRuleVectorStateKind === PushRuleVectorState.OFF) { - // When the current global keywords rule is OFF, we need to look at - // the flavor of rules in 'vectorContentRules' to apply the same actions - // when creating the new rule. - // Thus, this new rule will join the 'vectorContentRules' set. - if (self.state.vectorContentRules.rules.length) { - pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind( - self.state.vectorContentRules.rules[0], - ); + try { + if (rule.ruleId === KEYWORD_RULE_ID) { + console.log("@@ KEYWORDS"); + } else { + const definition = VectorPushRulesDefinitions[rule.ruleId]; + const actions = definition.vectorStateToActions[checkedState]; + if (!actions) { + await MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false); } else { - // ON is default - pushRuleVectorStateKind = PushRuleVectorState.ON; + await MatrixClientPeg.get().setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions); + await MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true); } } - for (const i in newKeywords) { - const keyword = newKeywords[i]; - - if (vectorContentRulesPatterns.indexOf(keyword) < 0) { - if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) { - deferreds.push(cli.addPushRule('global', 'content', keyword, { - actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind), - pattern: keyword, - })); - } else { - deferreds.push(self._addDisabledPushRule('global', 'content', keyword, { - actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind), - pattern: keyword, - })); - } + await this.refreshFromServer(); + } catch (e) { + this.setState({ phase: Phase.Error }); + console.error("Error updating push rule:", e); + this.showSaveError(); + } + }; + + private onClearNotificationsClicked = () => { + MatrixClientPeg.get().getRooms().forEach(r => { + if (r.getUnreadNotificationCount() > 0) { + const events = r.getLiveTimeline().getEvents(); + if (events.length) { + // noinspection JSIgnoredPromiseFromCall + MatrixClientPeg.get().sendReadReceipt(events[events.length - 1]); } } + }); + }; - Promise.all(deferreds).then(function(resps) { - self._refreshFromServer(); - }, onError); - }, onError); - } + private async setKeywords(keywords: string[], originalRules: IAnnotatedPushRule[]) { + try { + // De-duplicate and remove empties + keywords = Array.from(new Set(keywords)).filter(k => !!k); + const oldKeywords = Array.from(new Set(originalRules.map(r => r.pattern))).filter(k => !!k); - // Create a push rule but disabled - _addDisabledPushRule(scope, kind, ruleId, body) { - const cli = MatrixClientPeg.get(); - return cli.addPushRule(scope, kind, ruleId, body).then(() => - cli.setPushRuleEnabled(scope, kind, ruleId, false), - ); - } + // Note: Technically because of the UI interaction (at the time of writing), the diff + // will only ever be +/-1 so we don't really have to worry about efficiently handling + // tons of keyword changes. - _refreshFromServer = () => { - const self = this; - const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(function(rulesets) { - /// XXX seriously? wtf is this? - MatrixClientPeg.get().pushRules = rulesets; - - // Get homeserver default rules and triage them by categories - const ruleCategories = { - // The master rule (all notifications disabling) - '.m.rule.master': 'master', - - // The default push rules displayed by Vector UI - '.m.rule.contains_display_name': 'vector', - '.m.rule.contains_user_name': 'vector', - '.m.rule.roomnotif': 'vector', - '.m.rule.room_one_to_one': 'vector', - '.m.rule.encrypted_room_one_to_one': 'vector', - '.m.rule.message': 'vector', - '.m.rule.encrypted': 'vector', - '.m.rule.invite_for_me': 'vector', - //'.m.rule.member_event': 'vector', - '.m.rule.call': 'vector', - '.m.rule.suppress_notices': 'vector', - '.m.rule.tombstone': 'vector', - - // Others go to others - }; + const diff = arrayDiff(oldKeywords, keywords); - // HS default rules - const defaultRules = { master: [], vector: {}, others: [] }; - - for (const kind in rulesets.global) { - for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { - const r = rulesets.global[kind][i]; - const cat = ruleCategories[r.rule_id]; - r.kind = kind; - - if (r.rule_id[0] === '.') { - if (cat === 'vector') { - defaultRules.vector[r.rule_id] = r; - } else if (cat === 'master') { - defaultRules.master.push(r); - } else { - defaultRules['others'].push(r); - } - } + for (const word of diff.removed) { + for (const rule of originalRules.filter(r => r.pattern === word)) { + await MatrixClientPeg.get().deletePushRule('global', rule.kind, rule.rule_id); } } - // Get the master rule if any defined by the hs - if (defaultRules.master.length > 0) { - self.state.masterPushRule = defaultRules.master[0]; - } - - // parse the keyword rules into our state - const contentRules = ContentRules.parseContentRules(rulesets); - self.state.vectorContentRules = { - vectorState: contentRules.vectorState, - rules: contentRules.rules, - }; - self.state.externalContentRules = contentRules.externalRules; - - // Build the rules displayed in the Vector UI matrix table - self.state.vectorPushRules = []; - self.state.externalPushRules = []; - - const vectorRuleIds = [ - '.m.rule.contains_display_name', - '.m.rule.contains_user_name', - '.m.rule.roomnotif', - '_keywords', - '.m.rule.room_one_to_one', - '.m.rule.encrypted_room_one_to_one', - '.m.rule.message', - '.m.rule.encrypted', - '.m.rule.invite_for_me', - //'im.vector.rule.member_event', - '.m.rule.call', - '.m.rule.suppress_notices', - '.m.rule.tombstone', - ]; - for (const i in vectorRuleIds) { - const vectorRuleId = vectorRuleIds[i]; - - if (vectorRuleId === '_keywords') { - // keywords needs a special handling - // For Vector UI, this is a single global push rule but translated in Matrix, - // it corresponds to all content push rules (stored in self.state.vectorContentRule) - self.state.vectorPushRules.push({ - "vectorRuleId": "_keywords", - "description": ( - - { _t('Messages containing keywords', - {}, - { 'span': (sub) => - {sub}, - }, - )} - - ), - "vectorState": self.state.vectorContentRules.vectorState, - }); + let ruleVectorState = this.state.vectorKeywordRuleInfo.vectorState; + if (ruleVectorState === VectorState.Off) { + // When the current global keywords rule is OFF, we need to look at + // the flavor of existing rules to apply the same actions + // when creating the new rule. + if (originalRules.length) { + ruleVectorState = PushRuleVectorState.contentRuleVectorStateKind(originalRules[0]); } else { - const ruleDefinition = VectorPushRulesDefinitions[vectorRuleId]; - const rule = defaultRules.vector[vectorRuleId]; - - const vectorState = ruleDefinition.ruleToVectorState(rule); - - //console.log("Refreshing vectorPushRules for " + vectorRuleId +", "+ ruleDefinition.description +", " + rule +", " + vectorState); - - self.state.vectorPushRules.push({ - "vectorRuleId": vectorRuleId, - "description": _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js - "rule": rule, - "vectorState": vectorState, - }); - - // if there was a rule which we couldn't parse, add it to the external list - if (rule && !vectorState) { - rule.description = ruleDefinition.description; - self.state.externalPushRules.push(rule); - } + ruleVectorState = VectorState.On; // default } } - - // Build the rules not managed by Vector UI - const otherRulesDescriptions = { - '.m.rule.message': _t('Notify for all other messages/rooms'), - '.m.rule.fallback': _t('Notify me for anything else'), - }; - - for (const i in defaultRules.others) { - const rule = defaultRules.others[i]; - const ruleDescription = otherRulesDescriptions[rule.rule_id]; - - // Show enabled default rules that was modified by the user - if (ruleDescription && rule.enabled && !rule.default) { - rule.description = ruleDescription; - self.state.externalPushRules.push(rule); + const kind = PushRuleKind.ContentSpecific; + for (const word of diff.added) { + await MatrixClientPeg.get().addPushRule('global', kind, word, { + actions: PushRuleVectorState.actionsFor(ruleVectorState), + pattern: word, + }); + if (ruleVectorState === VectorState.Off) { + await MatrixClientPeg.get().setPushRuleEnabled('global', kind, word, false); } } - }); - const pushersPromise = MatrixClientPeg.get().getPushers().then(function(resp) { - self.setState({ pushers: resp.pushers }); - }); + await this.refreshFromServer(); + } catch (e) { + this.setState({ phase: Phase.Error }); + console.error("Error updating keyword push rules:", e); + this.showSaveError(); + } + } - Promise.all([pushRulesPromise, pushersPromise]).then(function() { - self.setState({ - phase: Notifications.phases.DISPLAY, - }); - }, function(error) { - console.error(error); - self.setState({ - phase: Notifications.phases.ERROR, - }); - }).finally(() => { - // actually explicitly update our state having been deep-manipulating it - self.setState({ - masterPushRule: self.state.masterPushRule, - vectorContentRules: self.state.vectorContentRules, - vectorPushRules: self.state.vectorPushRules, - externalContentRules: self.state.externalContentRules, - externalPushRules: self.state.externalPushRules, - }); - }); + private onKeywordAdd = (keyword: string) => { + const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules); - MatrixClientPeg.get().getThreePids().then((r) => this.setState({ threepids: r.threepids })); + // We add the keyword immediately as a sort of local echo effect + this.setState({ + phase: Phase.Persisting, + vectorKeywordRuleInfo: { + ...this.state.vectorKeywordRuleInfo, + rules: [ + ...this.state.vectorKeywordRuleInfo.rules, + + // XXX: Horrible assumption that we don't need the remaining fields + { pattern: keyword } as IAnnotatedPushRule, + ], + }, + }, async () => { + await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules); + }); }; - _onClearNotifications = () => { - const cli = MatrixClientPeg.get(); + private onKeywordRemove = (keyword: string) => { + const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules); - cli.getRooms().forEach(r => { - if (r.getUnreadNotificationCount() > 0) { - const events = r.getLiveTimeline().getEvents(); - if (events.length) cli.sendReadReceipt(events.pop()); - } + // We remove the keyword immediately as a sort of local echo effect + this.setState({ + phase: Phase.Persisting, + vectorKeywordRuleInfo: { + ...this.state.vectorKeywordRuleInfo, + rules: this.state.vectorKeywordRuleInfo.rules.filter(r => r.pattern !== keyword), + }, + }, async () => { + await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules); }); }; - _updatePushRuleActions(rule, actions, enabled) { - const cli = MatrixClientPeg.get(); - - return cli.setPushRuleActions( - 'global', rule.kind, rule.rule_id, actions, - ).then( function() { - // Then, if requested, enabled or disabled the rule - if (undefined != enabled) { - return cli.setPushRuleEnabled( - 'global', rule.kind, rule.rule_id, enabled, - ); - } - }); - } - - renderNotifRulesTableRow(title, className, pushRuleVectorState) { - return ( -
- - - - - - - - - ); - } - - renderNotifRulesTableRows() { - const rows = []; - for (const i in this.state.vectorPushRules) { - const rule = this.state.vectorPushRules[i]; - if (rule.rule === undefined && rule.vectorRuleId.startsWith(".m.")) { - console.warn(`Skipping render of rule ${rule.vectorRuleId} due to no underlying rule`); - continue; - } - //console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState); - rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState)); - } - return rows; - } - - hasEmailPusher(pushers, address) { - if (pushers === undefined) { - return false; - } - for (let i = 0; i < pushers.length; ++i) { - if (pushers[i].kind === 'email' && pushers[i].pushkey === address) { - return true; - } + private renderTopSection() { + const masterSwitch = ; + + // If all the rules are inhibited, don't show anything. + if (this.isInhibited) { + return masterSwitch; } - return false; - } - emailNotificationsRow(address, label) { - return ; + const emailSwitches = this.state.threepids.filter(t => t.medium === ThreepidMedium.Email) + .map(e => p.kind === "email" && p.pushkey === e.address)} + label={_t("Enable email notifications for %(email)s", { email: e.address })} + onChange={this.onEmailNotificationsChanged.bind(this, e.address)} + disabled={this.state.phase === Phase.Persisting} + />); + + return <> + { masterSwitch } + + + + + + + + { emailSwitches } + ; } - render() { - let spinner; - if (this.state.phase === Notifications.phases.LOADING) { - const Loader = sdk.getComponent("elements.Spinner"); - spinner = ; - } - - let masterPushRuleDiv; - if (this.state.masterPushRule) { - masterPushRuleDiv = ; + private renderCategory(category: RuleClass) { + if (category !== RuleClass.VectorOther && this.isInhibited) { + return null; // nothing to show for the section } - let clearNotificationsButton; - if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) { - clearNotificationsButton = - {_t("Clear notifications")} - ; + let clearNotifsButton: JSX.Element; + if ( + category === RuleClass.VectorOther + && MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0) + ) { + clearNotifsButton = { _t("Clear notifications") }; } - // When enabled, the master rule inhibits all existing rules - // So do not show all notification settings - if (this.state.masterPushRule && this.state.masterPushRule.enabled) { - return ( -
- {masterPushRuleDiv} - -
- { _t('All notifications are currently disabled for all targets.') } -
- - {clearNotificationsButton} -
- ); + if (category === RuleClass.VectorOther && this.isInhibited) { + // only render the utility buttons (if needed) + if (clearNotifsButton) { + return
+
{ _t("Other") }
+ { clearNotifsButton } +
; + } + return null; } - const emailThreepids = this.state.threepids.filter((tp) => tp.medium === "email"); - let emailNotificationsRows; - if (emailThreepids.length > 0) { - emailNotificationsRows = emailThreepids.map((threePid) => this.emailNotificationsRow( - threePid.address, `${_t('Enable email notifications')} (${threePid.address})`, - )); - } else if (SettingsStore.getValue(UIFeature.ThirdPartyID)) { - emailNotificationsRows =
- { _t('Add an email address to configure email notifications') } -
; + let keywordComposer: JSX.Element; + if (category === RuleClass.VectorMentions) { + keywordComposer = r.pattern)} + onAdd={this.onKeywordAdd} + onRemove={this.onKeywordRemove} + disabled={this.state.phase === Phase.Persisting} + label={_t("Keyword")} + placeholder={_t("New keyword")} + />; } - // Build external push rules - const externalRules = []; - for (const i in this.state.externalPushRules) { - const rule = this.state.externalPushRules[i]; - externalRules.push(
  • { _t(rule.description) }
  • ); - } + const makeRadio = (r: IVectorPushRule, s: VectorState) => ( + + ); - // Show keywords not displayed by the vector UI as a single external push rule - let externalKeywords: any[]|string = []; - for (const i in this.state.externalContentRules) { - const rule = this.state.externalContentRules[i]; - externalKeywords.push(rule.pattern); - } - if (externalKeywords.length) { - externalKeywords = externalKeywords.join(", "); - externalRules.push(
  • - {_t('Notifications on the following keywords follow rules which can’t be displayed here:') } - { externalKeywords } -
  • ); + const rows = this.state.vectorPushRules[category].map(r =>
    + + + + + ); + + let sectionName: TranslatedString; + switch (category) { + case RuleClass.VectorGlobal: + sectionName = _t("Global"); + break; + case RuleClass.VectorMentions: + sectionName = _t("Mentions & keywords"); + break; + case RuleClass.VectorOther: + sectionName = _t("Other"); + break; + default: + throw new Error("Developer error: Unnamed notifications section: " + category); } - let devicesSection; - if (this.state.pushers === undefined) { - devicesSection =
    { _t('Unable to fetch notification target list') }
    ; - } else if (this.state.pushers.length === 0) { - devicesSection = null; - } else { - // TODO: It would be great to be able to delete pushers from here too, - // and this wouldn't be hard to add. - const rows = []; - for (let i = 0; i < this.state.pushers.length; ++i) { - rows.push( - - - ); - } - devicesSection = (
    + {/* @ts-ignore*/} { _t('Off') }{ _t('On') }{ _t('Noisy') }
    - { title } - - - - - - -
    { r.description }{ makeRadio(r, VectorState.On) }{ makeRadio(r, VectorState.Off) }{ makeRadio(r, VectorState.Loud) }
    {this.state.pushers[i].app_display_name}{this.state.pushers[i].device_display_name}
    + return <> +
    + + + + + + + + - {rows} + { rows } -
    { sectionName }{ _t("On") }{ _t("Off") }{ _t("Noisy") }
    ); - } - if (devicesSection) { - devicesSection = (
    -

    { _t('Notification targets') }

    - { devicesSection } -
    ); - } - - let advancedSettings; - if (externalRules.length) { - const brand = SdkConfig.get().brand; - advancedSettings = ( -
    -

    { _t('Advanced notification settings') }

    - { _t('There are advanced notifications which are not shown here.') }
    - {_t( - 'You might have configured them in a client other than %(brand)s. ' + - 'You cannot tune them in %(brand)s but they still apply.', - { brand }, - )} -
      - { externalRules } -
    -
    - ); - } - - return ( -
    - - {masterPushRuleDiv} - -
    - - { spinner } - - - - - - - - { emailNotificationsRows } - -
    - - - - {/* @ts-ignore*/} - - {/* @ts-ignore*/} - - {/* @ts-ignore*/} - - - - +
    - {/* @ts-ignore*/} - { _t('Off') }{ _t('On') }{ _t('Noisy') }
    + { clearNotifsButton } + { keywordComposer } + ; + } - { this.renderNotifRulesTableRows() } + private renderTargets() { + if (this.isInhibited) return null; // no targets if there's no notifications - - -
    + const rows = this.state.pushers.map(p => + { p.app_display_name } + { p.device_display_name } + ); - { advancedSettings } + if (!rows.length) return null; // no targets to show - { devicesSection } + return
    +
    { _t("Notification targets") }
    + + + { rows } + +
    +
    ; + } - { clearNotificationsButton } -
    + public render() { + if (this.state.phase === Phase.Loading) { + // Ends up default centered + return ; + } else if (this.state.phase === Phase.Error) { + return

    { _t("There was an error loading your notification settings.") }

    ; + } -
    - ); + return
    + { this.renderTopSection() } + { this.renderCategory(RuleClass.VectorGlobal) } + { this.renderCategory(RuleClass.VectorMentions) } + { this.renderCategory(RuleClass.VectorOther) } + { this.renderTargets() } +
    ; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 761d48e51bc..cfee47e3619 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1158,6 +1158,16 @@ "Off": "Off", "On": "On", "Noisy": "Noisy", + "Messages containing keywords": "Messages containing keywords", + "Error saving notification preferences": "Error saving notification preferences", + "An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.", + "Enable for this account": "Enable for this account", + "Enable email notifications for %(email)s": "Enable email notifications for %(email)s", + "Keyword": "Keyword", + "New keyword": "New keyword", + "Global": "Global", + "Mentions & keywords": "Mentions & keywords", + "There was an error loading your notification settings.": "There was an error loading your notification settings.", "Failed to save your profile": "Failed to save your profile", "The operation could not be completed": "The operation could not be completed", "Upgrade to your own domain": "Upgrade to your own domain", @@ -1656,7 +1666,6 @@ "Show %(count)s more|other": "Show %(count)s more", "Show %(count)s more|one": "Show %(count)s more", "Show less": "Show less", - "Global": "Global", "All messages": "All messages", "Mentions & Keywords": "Mentions & Keywords", "Notification options": "Notification options", From 4444ccb0794f77b60937282bbd9f78b8a3b100c9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 13 Jul 2021 00:02:44 -0600 Subject: [PATCH 10/16] Appease the linter --- src/components/views/elements/Spinner.tsx | 2 +- src/components/views/settings/Notifications.tsx | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/Spinner.tsx b/src/components/views/elements/Spinner.tsx index 93c8f9e5d43..ee43a5bf0e5 100644 --- a/src/components/views/elements/Spinner.tsx +++ b/src/components/views/elements/Spinner.tsx @@ -36,7 +36,7 @@ export default class Spinner extends React.PureComponent { { message &&
    { message }
     
    }
    diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 4a733d7bf58..6d74e19ab14 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import Spinner from "../elements/Spinner"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { IAnnotatedPushRule, IPusher, PushRuleKind, RuleId, } from "matrix-js-sdk/src/@types/PushRules"; +import { IAnnotatedPushRule, IPusher, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules"; import { ContentRules, IContentRules, @@ -80,7 +80,7 @@ const RULE_DISPLAY_ORDER: string[] = [ RuleId.IncomingCall, RuleId.SuppressNotices, RuleId.Tombstone, -] +]; interface IVectorPushRule { ruleId: RuleId | typeof KEYWORD_RULE_ID | string; @@ -181,7 +181,7 @@ export default class Notifications extends React.PureComponent { // noinspection JSUnfilteredForInLoop const kind = k as PushRuleKind; for (const r of ruleSets.global[kind]) { - const rule: IAnnotatedPushRule = Object.assign(r, {kind}); + const rule: IAnnotatedPushRule = Object.assign(r, { kind }); const category = categories[rule.rule_id] ?? RuleClass.Other; if (rule.rule_id[0] === '.') { @@ -356,11 +356,12 @@ export default class Notifications extends React.PureComponent { } else { const definition = VectorPushRulesDefinitions[rule.ruleId]; const actions = definition.vectorStateToActions[checkedState]; + const cli = MatrixClientPeg.get(); if (!actions) { - await MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false); + await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false); } else { - await MatrixClientPeg.get().setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions); - await MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true); + await cli.setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions); + await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true); } } From 9d60d29368290fa33dfc2eb8a4129ac99f136bab Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 13 Jul 2021 00:04:07 -0600 Subject: [PATCH 11/16] Clean up i18n --- src/i18n/strings/en_EN.json | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cfee47e3619..ed794068e0b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1131,42 +1131,23 @@ "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", - "Error saving email notification preferences": "Error saving email notification preferences", - "An error occurred whilst saving your email notification preferences.": "An error occurred whilst saving your email notification preferences.", - "Keywords": "Keywords", - "Enter keywords separated by a comma:": "Enter keywords separated by a comma:", - "Failed to change settings": "Failed to change settings", - "Can't update user notification settings": "Can't update user notification settings", - "Failed to update keywords": "Failed to update keywords", - "Messages containing keywords": "Messages containing keywords", - "Notify for all other messages/rooms": "Notify for all other messages/rooms", - "Notify me for anything else": "Notify me for anything else", - "Enable notifications for this account": "Enable notifications for this account", - "Clear notifications": "Clear notifications", - "All notifications are currently disabled for all targets.": "All notifications are currently disabled for all targets.", - "Enable email notifications": "Enable email notifications", - "Add an email address to configure email notifications": "Add an email address to configure email notifications", - "Notifications on the following keywords follow rules which can’t be displayed here:": "Notifications on the following keywords follow rules which can’t be displayed here:", - "Unable to fetch notification target list": "Unable to fetch notification target list", - "Notification targets": "Notification targets", - "Advanced notification settings": "Advanced notification settings", - "There are advanced notifications which are not shown here.": "There are advanced notifications which are not shown here.", - "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.", - "Enable desktop notifications for this session": "Enable desktop notifications for this session", - "Show message in desktop notification": "Show message in desktop notification", - "Enable audible notifications for this session": "Enable audible notifications for this session", - "Off": "Off", - "On": "On", - "Noisy": "Noisy", "Messages containing keywords": "Messages containing keywords", "Error saving notification preferences": "Error saving notification preferences", "An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.", "Enable for this account": "Enable for this account", "Enable email notifications for %(email)s": "Enable email notifications for %(email)s", + "Enable desktop notifications for this session": "Enable desktop notifications for this session", + "Show message in desktop notification": "Show message in desktop notification", + "Enable audible notifications for this session": "Enable audible notifications for this session", + "Clear notifications": "Clear notifications", "Keyword": "Keyword", "New keyword": "New keyword", "Global": "Global", "Mentions & keywords": "Mentions & keywords", + "On": "On", + "Off": "Off", + "Noisy": "Noisy", + "Notification targets": "Notification targets", "There was an error loading your notification settings.": "There was an error loading your notification settings.", "Failed to save your profile": "Failed to save your profile", "The operation could not be completed": "The operation could not be completed", From 8278b2273d332707daf683c2a57fcec76801fb5a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 13 Jul 2021 00:23:56 -0600 Subject: [PATCH 12/16] Copy over the whole feature of changing the state for keywords entirely --- .../views/settings/Notifications.tsx | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 6d74e19ab14..6baac8892eb 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import Spinner from "../elements/Spinner"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { IAnnotatedPushRule, IPusher, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules"; +import { IAnnotatedPushRule, IPusher, PushRuleAction, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules"; import { ContentRules, IContentRules, @@ -351,12 +351,40 @@ export default class Notifications extends React.PureComponent { this.setState({ phase: Phase.Persisting }); try { + const cli = MatrixClientPeg.get(); if (rule.ruleId === KEYWORD_RULE_ID) { - console.log("@@ KEYWORDS"); + // Update all the keywords + for (const rule of this.state.vectorKeywordRuleInfo.rules) { + let enabled: boolean; + let actions: PushRuleAction[]; + if (checkedState === VectorState.On) { + if (rule.actions.length !== 1) { // XXX: Magic number + actions = PushRuleVectorState.actionsFor(checkedState); + } + if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) { + enabled = true; + } + } else if (checkedState === VectorState.Loud) { + if (rule.actions.length !== 3) { // XXX: Magic number + actions = PushRuleVectorState.actionsFor(checkedState); + } + if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) { + enabled = true; + } + } else { + enabled = false; + } + + if (actions) { + await cli.setPushRuleActions('global', rule.kind, rule.rule_id, actions); + } + if (enabled !== undefined) { + await cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled); + } + } } else { const definition = VectorPushRulesDefinitions[rule.ruleId]; const actions = definition.vectorStateToActions[checkedState]; - const cli = MatrixClientPeg.get(); if (!actions) { await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false); } else { From 2690bb56f9f08c114d56c8e25a88b1af36285e2c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Jul 2021 13:39:54 -0600 Subject: [PATCH 13/16] Remove code we don't seem to need --- .../views/settings/Notifications.tsx | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 6baac8892eb..0cfcdd61aff 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -214,15 +214,6 @@ export default class Notifications extends React.PureComponent { rule, vectorState, description: _t(definition.description), }); - - // XXX: Do we need this block from the previous component? - /* - // if there was a rule which we couldn't parse, add it to the external list - if (rule && !vectorState) { - rule.description = ruleDefinition.description; - self.state.externalPushRules.push(rule); - } - */ } // Quickly sort the rules for display purposes @@ -246,26 +237,6 @@ export default class Notifications extends React.PureComponent { } } - // XXX: Do we need this block from the previous component? - /* - // Build the rules not managed by Vector UI - const otherRulesDescriptions = { - '.m.rule.message': _t('Notify for all other messages/rooms'), - '.m.rule.fallback': _t('Notify me for anything else'), - }; - - for (const i in defaultRules.others) { - const rule = defaultRules.others[i]; - const ruleDescription = otherRulesDescriptions[rule.rule_id]; - - // Show enabled default rules that was modified by the user - if (ruleDescription && rule.enabled && !rule.default) { - rule.description = ruleDescription; - self.state.externalPushRules.push(rule); - } - } - */ - return preparedNewState; } From 60bcdd3bf8f9b075266b9e88c57429aefa0c7736 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 16 Jul 2021 16:29:25 -0600 Subject: [PATCH 14/16] Fix types from js-sdk --- src/components/views/settings/Notifications.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 0cfcdd61aff..e0e2467240c 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -26,7 +26,7 @@ import { VectorState, } from "../../../notifications"; import { _t, TranslatedString } from "../../../languageHandler"; -import { IThirdPartyIdentifier, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids"; +import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import SettingsStore from "../../../settings/SettingsStore"; import StyledRadioButton from "../elements/StyledRadioButton"; @@ -101,7 +101,7 @@ interface IState { [category in RuleClass]?: IVectorPushRule[]; }; pushers?: IPusher[]; - threepids?: IThirdPartyIdentifier[]; + threepids?: IThreepid[]; } export default class Notifications extends React.PureComponent { From d2de9b432c577842c1ca4592067de3778316014b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 16 Jul 2021 23:50:06 -0600 Subject: [PATCH 15/16] Apply suggestions from code review Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/settings/_Notifications.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss index 2ec9f3fbeac..f93e0a53a89 100644 --- a/res/css/views/settings/_Notifications.scss +++ b/res/css/views/settings/_Notifications.scss @@ -25,7 +25,7 @@ limitations under the License. margin-top: 40px; tr > th { - font-weight: 600; // semi bold + font-weight: $font-semi-bold; } tr > th:first-child { @@ -67,7 +67,7 @@ limitations under the License. & > div:first-child { // section header font-size: $font-18px; - font-weight: 600; // semi bold + font-weight: $font-semi-bold; } > table { From e3e7d945fdc3c65aba1a19a7f015751e9545ab94 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 16 Jul 2021 23:51:44 -0600 Subject: [PATCH 16/16] Remove useless spread operator --- src/components/views/elements/TagComposer.tsx | 4 ++-- src/components/views/settings/Notifications.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/TagComposer.tsx b/src/components/views/elements/TagComposer.tsx index ff104748a07..03f501f02c9 100644 --- a/src/components/views/elements/TagComposer.tsx +++ b/src/components/views/elements/TagComposer.tsx @@ -59,11 +59,11 @@ export default class TagComposer extends React.PureComponent { this.setState({ newTag: "" }); }; - private onRemove = (tag: string) => { + private onRemove(tag: string) { // We probably don't need to proxy this, but for // sanity of `this` we'll do so anyways. this.props.onRemove(tag); - }; + } public render() { return
    diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index e0e2467240c..a488145153a 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -240,12 +240,12 @@ export default class Notifications extends React.PureComponent { return preparedNewState; } - private async refreshPushers(): Promise> { - return { ...(await MatrixClientPeg.get().getPushers()) }; + private refreshPushers(): Promise> { + return MatrixClientPeg.get().getPushers(); } - private async refreshThreepids(): Promise> { - return { ...(await MatrixClientPeg.get().getThreePids()) }; + private refreshThreepids(): Promise> { + return MatrixClientPeg.get().getThreePids(); } private showSaveError() {