From ee13dd7b6c35de26724cead7cebb4f935912452e Mon Sep 17 00:00:00 2001 From: manuroe Date: Mon, 11 Jan 2016 17:24:04 +0100 Subject: [PATCH 01/25] PushRules settings: Added a dedicated component to display them --- src/component-index.js | 7 +- .../views/settings/Notifications.js | 291 ++++++++++++++++++ .../structures/UserSettings.css | 24 +- .../views/settings/Notifications.css | 53 ++++ 4 files changed, 356 insertions(+), 19 deletions(-) create mode 100644 src/components/views/settings/Notifications.js create mode 100644 src/skins/vector/css/vector-web/views/settings/Notifications.css diff --git a/src/component-index.js b/src/component-index.js index e7d9b560f..3d814035e 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -33,9 +33,9 @@ module.exports.components['structures.ViewSource'] = require('./components/struc module.exports.components['views.elements.ImageView'] = require('./components/views/elements/ImageView'); module.exports.components['views.elements.Spinner'] = require('./components/views/elements/Spinner'); module.exports.components['views.globals.MatrixToolbar'] = require('./components/views/globals/MatrixToolbar'); -module.exports.components['views.login.CustomServerDialog'] = require('./components/views/login/VectorCustomServerDialog'); -module.exports.components['views.login.LoginFooter'] = require('./components/views/login/VectorLoginFooter'); -module.exports.components['views.login.LoginHeader'] = require('./components/views/login/VectorLoginHeader'); +module.exports.components['views.login.VectorCustomServerDialog'] = require('./components/views/login/VectorCustomServerDialog'); +module.exports.components['views.login.VectorLoginFooter'] = require('./components/views/login/VectorLoginFooter'); +module.exports.components['views.login.VectorLoginHeader'] = require('./components/views/login/VectorLoginHeader'); module.exports.components['views.messages.DateSeparator'] = require('./components/views/messages/DateSeparator'); module.exports.components['views.messages.MessageTimestamp'] = require('./components/views/messages/MessageTimestamp'); module.exports.components['views.messages.SenderProfile'] = require('./components/views/messages/SenderProfile'); @@ -45,3 +45,4 @@ module.exports.components['views.rooms.RoomDNDView'] = require('./components/vie module.exports.components['views.rooms.RoomDropTarget'] = require('./components/views/rooms/RoomDropTarget'); module.exports.components['views.rooms.RoomTooltip'] = require('./components/views/rooms/RoomTooltip'); module.exports.components['views.rooms.SearchBar'] = require('./components/views/rooms/SearchBar'); +module.exports.components['views.settings.Notifications'] = require('./components/views/settings/Notifications'); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js new file mode 100644 index 000000000..8fc992e3f --- /dev/null +++ b/src/components/views/settings/Notifications.js @@ -0,0 +1,291 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +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. +*/ + +'use strict'; +var React = require('react'); +var sdk = require('matrix-react-sdk'); +var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); +var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore'); + +/** + * Enum for state of a push rule as defined by the Vector UI. + * @readonly + * @enum {string} + */ +var PushRuleState = { + /** The user will receive push notification for this rule */ + ON: "on", + /** The user will receive push notification for this rule with sound and + highlight if this is legitimate */ + STRONG: "strong", + /** The push rule is disabled */ + OFF: "off" +}; + +module.exports = React.createClass({ + displayName: 'Notififications', + + 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 + }, + + getInitialState: function() { + return { + phase: this.phases.LOADING, + vectorPushRules: [] + }; + }, + + componentWillMount: function() { + this._refreshFromServer(); + }, + + onEnableNotificationsChange: function(event) { + UserSettingsStore.setEnableNotifications(event.target.checked); + }, + + onNotifStateButtonClicked: function(event) { + var vectorRuleId = event.target.className.split("-")[0]; + var newPushRuleState = event.target.className.split("-")[1]; + + var rule = this.getRule(vectorRuleId); + + // For now, we support only enabled/disabled. + // Translate ON, STRONG, OFF to one of the 2. + if (rule && rule.state !== newPushRuleState) { + + this.setState({ + phase: this.phases.LOADING + }); + + var self = this; + MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, (newPushRuleState !== PushRuleState.OFF)).done(function() { + + self._refreshFromServer(); + self.forceUpdate(); + }); + } + }, + + getRule: function(vectorRuleId) { + for (var i in this.state.vectorPushRules) { + var rule = this.state.vectorPushRules[i]; + if (rule.vectorRuleId === vectorRuleId) { + return rule; + } + } + }, + + _refreshFromServer: function() { + var self = this; + MatrixClientPeg.get().getPushRules().done(function(rulesets) { + MatrixClientPeg.get().pushRules = rulesets; + + // Get homeserver default rules expected by Vector + var rule_categories = { + '.m.rule.master': 'master', + + '.m.rule.contains_display_name': 'vector', + '.m.rule.room_one_to_one': 'vector', + '.m.rule.invite_for_me': 'vector', + '.m.rule.member_event': 'vector', + '.m.rule.call': 'vector', + }; + + var defaultRules = {master: [], vector: {}, additional: [], fallthrough: [], suppression: []}; + for (var kind in rulesets.global) { + for (var i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { + var r = rulesets.global[kind][i]; + var cat = rule_categories[r.rule_id]; + r.kind = kind; + if (r.rule_id[0] === '.') { + if (cat) { + if (cat === 'vector') + { + defaultRules.vector[r.rule_id] = r; + } + else + { + defaultRules[cat].push(r); + } + } else { + defaultRules.additional.push(r); + } + } + } + } + + // Build the rules displayed by Vector UI + self.state.vectorPushRules = []; + var rule, state; + + rule = defaultRules.vector['.m.rule.contains_display_name']; + state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; + self.state.vectorPushRules.push({ + "vectorRuleId": "contains_display_name", + "description" : "Messages containing my name", + "rule": rule, + "state": state, + "disabled": PushRuleState.ON + }); + + // TODO: Merge contains_user_name + + // TODO: Add "Messages containing keywords" + + rule = defaultRules.vector['.m.rule.room_one_to_one']; + state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; + self.state.vectorPushRules.push({ + "vectorRuleId": "room_one_to_one", + "description" : "Messages just sent to me", + "rule": rule, + "state": state, + "disabled": PushRuleState.ON + }); + + rule = defaultRules.vector['.m.rule.invite_for_me']; + state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; + self.state.vectorPushRules.push({ + "vectorRuleId": "invite_for_me", + "description" : "When I'm invited to a room", + "rule": rule, + "state": state, + "disabled": PushRuleState.ON + }); + + rule = defaultRules.vector['.m.rule.member_event']; + state = (rule && rule.enabled) ? PushRuleState.ON : PushRuleState.OFF; + self.state.vectorPushRules.push({ + "vectorRuleId": "member_event", + "description" : "When people join or leave a room", + "rule": rule, + "state": state, + "disabled": PushRuleState.STRONG + }); + + rule = defaultRules.vector['.m.rule.call']; + state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; + self.state.vectorPushRules.push({ + "vectorRuleId": "call", + "description" : "Call invitation", + "rule": rule, + "state": state, + "disabled": PushRuleState.ON + }); + + self.setState({ + phase: self.phases.DISPLAY + }); + + self.forceUpdate(); + }); + }, + + renderNotifRulesTableRow: function(title, className, pushRuleState, disabled) { + return ( + + {title} + + + + + + + + + + + + + + ); + }, + + renderNotifRulesTableRows: function() { + var rows = []; + for (var i in this.state.vectorPushRules) { + var rule = this.state.vectorPushRules[i]; + rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.state, rule.disabled)); + } + return rows; + }, + + render: function() { + if (this.state.phase === this.phases.LOADING) { + var Loader = sdk.getComponent("elements.Spinner"); + return ( +
+ +
+ ); + } + + return ( +
+ +
+
+ +
+
+ +
+
+ +

General use

+ +
+ + + + + + + + + + + + { this.renderNotifRulesTableRows() } + + +
NormalStrongOff
+
+ +
+ ); + } +}); diff --git a/src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css b/src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css index a0e9052c8..fac8bf91d 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css +++ b/src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css @@ -56,6 +56,13 @@ limitations under the License. border-bottom: 1px solid #eee; } +.mx_UserSettings h3 { + font-weight: bold; + font-size: 15px; + margin-top: 4px; + margin-bottom: 4px; +} + .mx_UserSettings_section { margin-left: 63px; margin-top: 28px; @@ -74,8 +81,7 @@ limitations under the License. float: left; } -.mx_UserSettings_profileTableRow, -.mx_UserSettings_notifTableRow +.mx_UserSettings_profileTableRow { display: table-row; } @@ -106,20 +112,6 @@ limitations under the License. font-size: 16px; } -.mx_UserSettings_notifInputCell { - display: table-cell; - padding-bottom: 21px; - padding-right: 8px; - width: 16px; -} - -.mx_UserSettings_notifLabelCell -{ - padding-bottom: 21px; - width: 270px; - display: table-cell; -} - .mx_UserSettings_logout { margin-right: 24px; margin-bottom: 24px; diff --git a/src/skins/vector/css/vector-web/views/settings/Notifications.css b/src/skins/vector/css/vector-web/views/settings/Notifications.css new file mode 100644 index 000000000..962af30ed --- /dev/null +++ b/src/skins/vector/css/vector-web/views/settings/Notifications.css @@ -0,0 +1,53 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +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_UserNotifSettings_tableRow +{ + display: table-row; +} + +.mx_UserNotifSettings_inputCell { + display: table-cell; + padding-bottom: 21px; + padding-right: 8px; + width: 16px; +} + +.mx_UserNotifSettings_labelCell +{ + padding-bottom: 21px; + width: 270px; + display: table-cell; +} + +.mx_UserNotifSettings_pushRulesTable { + width: 100%; + table-layout: fixed; +} + +.mx_UserNotifSettings_pushRulesTable thead { + font-weight: bold; + font-size: 15px; +} + +.mx_UserNotifSettings_pushRulesTable tbody th { + font-weight: 400; + font-size: 15px; +} + +.mx_UserNotifSettings_pushRulesTable tbody th:first-child { + text-align: left; +} \ No newline at end of file From 6d510db2db878101fb65ee1d4e2f07c92b9868ec Mon Sep 17 00:00:00 2001 From: manuroe Date: Mon, 11 Jan 2016 17:32:37 +0100 Subject: [PATCH 02/25] PPushRules settings: Fixed React warnings --- src/components/views/settings/Notifications.js | 7 +++---- .../vector/css/vector-web/views/settings/Notifications.css | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 8fc992e3f..f6e48159c 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -77,7 +77,6 @@ module.exports = React.createClass({ MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, (newPushRuleState !== PushRuleState.OFF)).done(function() { self._refreshFromServer(); - self.forceUpdate(); }); } }, @@ -191,15 +190,15 @@ module.exports = React.createClass({ self.setState({ phase: self.phases.DISPLAY }); - - self.forceUpdate(); }); }, renderNotifRulesTableRow: function(title, className, pushRuleState, disabled) { return ( - {title} + + {title} + Date: Tue, 12 Jan 2016 15:12:58 +0100 Subject: [PATCH 03/25] PushRules settings: Translate matrix per-word rules into the global Vector rule for a list of keywords --- .../views/settings/Notifications.js | 239 ++++++++++++++---- 1 file changed, 193 insertions(+), 46 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index f6e48159c..b0474ceac 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -16,9 +16,11 @@ limitations under the License. 'use strict'; var React = require('react'); +var q = require("q"); var sdk = require('matrix-react-sdk'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore'); +var Modal = require('matrix-react-sdk/lib/Modal'); /** * Enum for state of a push rule as defined by the Vector UI. @@ -47,7 +49,12 @@ module.exports = React.createClass({ getInitialState: function() { return { phase: this.phases.LOADING, - vectorPushRules: [] + vectorPushRules: [], // HS default push rules displayed in Vector UI + vectorContentRules: { // Keyword push rules displayed in Vector UI + state: PushRuleState.ON, + rules: [] + }, + externalContentRules: [] // Keyword push rules that have been defined outside Vector UI }; }, @@ -60,25 +67,67 @@ module.exports = React.createClass({ }, onNotifStateButtonClicked: function(event) { + var self = this; + var cli = MatrixClientPeg.get(); var vectorRuleId = event.target.className.split("-")[0]; - var newPushRuleState = event.target.className.split("-")[1]; + var newPushRuleState = event.target.className.split("-")[1]; - var rule = this.getRule(vectorRuleId); - - // For now, we support only enabled/disabled. - // Translate ON, STRONG, OFF to one of the 2. - if (rule && rule.state !== newPushRuleState) { + if ("keywords" === vectorRuleId + && this.state.vectorContentRules.state !== newPushRuleState + && this.state.vectorContentRules.rules.length) { this.setState({ phase: this.phases.LOADING }); - var self = this; - MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, (newPushRuleState !== PushRuleState.OFF)).done(function() { - - self._refreshFromServer(); + var enabled = true; + switch (newPushRuleState) { + case PushRuleState.OFF: + enabled = false; + break + } + + // Update all rules in self.state.vectorContentRules + var deferreds = []; + for (var i in this.state.vectorContentRules.rules) { + var rule = this.state.vectorContentRules.rules[i]; + + + if (enabled) { + deferreds.push(cli.addPushRule('global', rule.kind, rule.rule_id, rule)); + } + else { + deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, false)); + } + } + + q.all(deferreds).done(function(resps) { + self._refreshFromServer(); + }, function(error) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't update user notification settings", + description: error.toString() + }); }); - } + } + else { + var rule = this.getRule(vectorRuleId); + + // For now, we support only enabled/disabled. + // Translate ON, STRONG, OFF to one of the 2. + if (rule && rule.state !== newPushRuleState) { + + this.setState({ + phase: this.phases.LOADING + }); + + cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, (newPushRuleState !== PushRuleState.OFF)).done(function() { + + self._refreshFromServer(); + }); + } + } }, getRule: function(vectorRuleId) { @@ -106,7 +155,11 @@ module.exports = React.createClass({ '.m.rule.call': 'vector', }; + // HS default rules var defaultRules = {master: [], vector: {}, additional: [], fallthrough: [], suppression: []}; + // Content/keyword rules + var contentRules = {on: [], strong: [], off: [], other: []}; + for (var kind in rulesets.global) { for (var i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { var r = rulesets.global[kind][i]; @@ -126,13 +179,78 @@ module.exports = React.createClass({ defaultRules.additional.push(r); } } + else if (kind === 'content') { + if (r.enabled) { + // Count tweaks to determine if it is a ON or STRONG rule + var tweaks = 0; + for (var j in r.actions) { + var action = r.actions[j]; + if (action.set_tweak === 'sound' || + (action.set_tweak === 'highlight' && action.value)) { + tweaks++; + } + } + + switch (tweaks) { + case 0: + contentRules.on.push(r); + break; + case 2: + contentRules.strong.push(r); + break; + default: + contentRules.other.push(r); + break; + } + } + else { + contentRules.off.push(r); + } + } } } + // Decide which content/keyword rules to display in Vector UI. + // Vector displays a single global rule for a list of keywords + // whereas Matrix has a push rule per keyword. + // Vector can set the unique rule in ON, STRONG or OFF state. + // Matrix has enabled/disabled plus a combination of (highlight, sound) tweaks. + + // The code below determines which set of user's content push rules can be + // displayed by the vector UI. + // Push rules that does not fir, ie defined by another Matrix client, ends + // in self.state.externalContentRules. + // There is priority in the determination of which set will be the displayed one. + // The set with rules that have STRONG tweaks is the first choice. Then, the ones + // with ON tweaks (no tweaks). + if (contentRules.strong.length) { + self.state.vectorContentRules = { + state: PushRuleState.STRONG, + rules: contentRules.strong + } + self.state.externalContentRules = [].concat(contentRules.on, contentRules.other, contentRules.off); + } + else if (contentRules.on.length) { + self.state.vectorContentRules = { + state: PushRuleState.ON, + rules: contentRules.on + } + self.state.externalContentRules = [].concat(contentRules.strong, contentRules.other, contentRules.off); + } + else if (contentRules.off.length) { + self.state.vectorContentRules = { + state: PushRuleState.OFF, + rules: contentRules.off + } + self.state.externalContentRules = [].concat(contentRules.strong, contentRules.on, contentRules.other); + } + // Build the rules displayed by Vector UI self.state.vectorPushRules = []; var rule, state; + // Messages containing user's display name + // (skip contains_user_name which is too geeky) rule = defaultRules.vector['.m.rule.contains_display_name']; state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; self.state.vectorPushRules.push({ @@ -143,10 +261,16 @@ module.exports = React.createClass({ "disabled": PushRuleState.ON }); - // TODO: Merge contains_user_name - - // TODO: Add "Messages containing keywords" + // Messages containing keywords + // 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" : "Messages containing keywords", + "state": self.state.vectorContentRules.state, + }); + // Messages just sent to the user rule = defaultRules.vector['.m.rule.room_one_to_one']; state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; self.state.vectorPushRules.push({ @@ -157,6 +281,7 @@ module.exports = React.createClass({ "disabled": PushRuleState.ON }); + // Invitation for the user rule = defaultRules.vector['.m.rule.invite_for_me']; state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; self.state.vectorPushRules.push({ @@ -167,6 +292,7 @@ module.exports = React.createClass({ "disabled": PushRuleState.ON }); + // When people join or leave a room rule = defaultRules.vector['.m.rule.member_event']; state = (rule && rule.enabled) ? PushRuleState.ON : PushRuleState.OFF; self.state.vectorPushRules.push({ @@ -177,6 +303,7 @@ module.exports = React.createClass({ "disabled": PushRuleState.STRONG }); + // Incoming call rule = defaultRules.vector['.m.rule.call']; state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; self.state.vectorPushRules.push({ @@ -245,43 +372,63 @@ module.exports = React.createClass({ ); } + + // Build the list of keywords rules that have been defined outside Vector UI + var externalKeyWords = []; + for (var i in this.state.externalContentRules) { + var rule = this.state.externalContentRules[i]; + externalKeyWords.push(rule.pattern); + } + + if (externalKeyWords.length) { + externalKeyWords = externalKeyWords.join(", "); + } return ( -
- -
-
- +
+
+ +
+
+ +
+
+ +
-
- + +

General use

+ +
+ + + + + + + + + + + + { this.renderNotifRulesTableRows() } + + +
NormalStrongOff
+
-

General use

- -
- - - - - - - - - - - - { this.renderNotifRulesTableRows() } - - -
NormalStrongOff
+
+

+ Warning: Push rules on the following keywords has been defined:
+ { externalKeyWords }
From b9080c770def645ff92bf83e43b625a375cc8881 Mon Sep 17 00:00:00 2001 From: manuroe Date: Tue, 12 Jan 2016 16:46:27 +0100 Subject: [PATCH 04/25] PushRules settings: Fixed triage of matrix content rules into the unique Vector rule --- .../views/settings/Notifications.js | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index b0474ceac..39e0c6f10 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -158,7 +158,7 @@ module.exports = React.createClass({ // HS default rules var defaultRules = {master: [], vector: {}, additional: [], fallthrough: [], suppression: []}; // Content/keyword rules - var contentRules = {on: [], strong: [], off: [], other: []}; + var contentRules = {on: [], on_but_disabled:[], strong: [], strong_but_disabled: [], other: []}; for (var kind in rulesets.global) { for (var i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { @@ -180,31 +180,36 @@ module.exports = React.createClass({ } } else if (kind === 'content') { - if (r.enabled) { - // Count tweaks to determine if it is a ON or STRONG rule - var tweaks = 0; - for (var j in r.actions) { - var action = r.actions[j]; - if (action.set_tweak === 'sound' || - (action.set_tweak === 'highlight' && action.value)) { - tweaks++; - } - } - - switch (tweaks) { - case 0: - contentRules.on.push(r); - break; - case 2: - contentRules.strong.push(r); - break; - default: - contentRules.other.push(r); - break; + // Count tweaks to determine if it is a ON or STRONG rule + var tweaks = 0; + for (var j in r.actions) { + var action = r.actions[j]; + if (action.set_tweak === 'sound' || + (action.set_tweak === 'highlight' && action.value)) { + tweaks++; } } - else { - contentRules.off.push(r); + + switch (tweaks) { + case 0: + if (r.enabled) { + contentRules.on.push(r); + } + else { + contentRules.on_but_disabled.push(r); + } + break; + case 2: + if (r.enabled) { + contentRules.strong.push(r); + } + else { + contentRules.strong_but_disabled.push(r); + } + break; + default: + contentRules.other.push(r); + break; } } } @@ -228,21 +233,31 @@ module.exports = React.createClass({ state: PushRuleState.STRONG, rules: contentRules.strong } - self.state.externalContentRules = [].concat(contentRules.on, contentRules.other, contentRules.off); + self.state.externalContentRules = [].concat(contentRules.strong_but_disabled, contentRules.on, contentRules.on_but_disabled, contentRules.other); + } + else if (contentRules.strong_but_disabled.length) { + self.state.vectorContentRules = { + state: PushRuleState.OFF, + rules: contentRules.strong_but_disabled + } + self.state.externalContentRules = [].concat(contentRules.on, contentRules.on_but_disabled, contentRules.other); } else if (contentRules.on.length) { self.state.vectorContentRules = { state: PushRuleState.ON, rules: contentRules.on } - self.state.externalContentRules = [].concat(contentRules.strong, contentRules.other, contentRules.off); + self.state.externalContentRules = [].concat(contentRules.on_but_disabled, contentRules.other); } - else if (contentRules.off.length) { + else if (contentRules.on_but_disabled.length) { self.state.vectorContentRules = { state: PushRuleState.OFF, - rules: contentRules.off + rules: contentRules.on_but_disabled } - self.state.externalContentRules = [].concat(contentRules.strong, contentRules.on, contentRules.other); + self.state.externalContentRules = contentRules.other; + } + else { + self.state.externalContentRules = contentRules.other; } // Build the rules displayed by Vector UI From 9fb8c9f67acda7ef422d3cda4735cbad61d2bdec Mon Sep 17 00:00:00 2001 From: manuroe Date: Tue, 12 Jan 2016 17:26:41 +0100 Subject: [PATCH 05/25] PushRules settings: Use a workaround for SYN-590 (Push rule update fails) --- .../views/settings/Notifications.js | 75 +++++++++++++++---- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 39e0c6f10..c24937bc8 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -80,24 +80,49 @@ module.exports = React.createClass({ phase: this.phases.LOADING }); - var enabled = true; - switch (newPushRuleState) { - case PushRuleState.OFF: - enabled = false; - break - } - // Update all rules in self.state.vectorContentRules var deferreds = []; for (var i in this.state.vectorContentRules.rules) { var rule = this.state.vectorContentRules.rules[i]; - - if (enabled) { - deferreds.push(cli.addPushRule('global', rule.kind, rule.rule_id, rule)); + var enabled; + var actions; + switch (newPushRuleState) { + case PushRuleState.ON: + if (rule.actions.length !== 1) { + actions = ['notify']; + } + + if (this.state.vectorContentRules.state === PushRuleState.OFF) { + enabled = true; + } + break; + + case PushRuleState.STRONG: + if (rule.actions.length !== 3) { + actions = ['notify', + {'set_tweak': 'sound', 'value': 'default'}, + {'set_tweak': 'highlight', 'value': 'true'} + ]; + } + + if (this.state.vectorContentRules.state === PushRuleState.OFF) { + enabled = true; + } + break; + + case PushRuleState.OFF: + enabled = false; + break; } - else { - deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, false)); + + if (actions) { + // Note that the workaound in _updatePushRuleActions will automatically + // enable the rule + deferreds.push(this._updatePushRuleActions(rule, actions)); + } + else if (enabled != undefined) { + deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled)); } } @@ -114,7 +139,7 @@ module.exports = React.createClass({ else { var rule = this.getRule(vectorRuleId); - // For now, we support only enabled/disabled. + // For now, we support only enabled/disabled for hs default rules // Translate ON, STRONG, OFF to one of the 2. if (rule && rule.state !== newPushRuleState) { @@ -334,7 +359,29 @@ module.exports = React.createClass({ }); }); }, - + + _updatePushRuleActions: function(rule, actions) { + // Workaround for SYN-590 : Push rule update fails + // Remove the rule and recreate it with the new actions + var cli = MatrixClientPeg.get(); + var deferred = q.defer(); + + cli.deletePushRule('global', rule.kind, rule.rule_id).done(function() { + cli.addPushRule('global', rule.kind, rule.rule_id, { + actions: actions, + pattern: rule.pattern + }).done(function() { + deferred.resolve(); + }, function(err) { + deferred.reject(err); + }); + }, function(err) { + deferred.reject(err); + }); + + return deferred.promise; + }, + renderNotifRulesTableRow: function(title, className, pushRuleState, disabled) { return ( From 10d3076d6b4dc437e9a926d1bd47a657db0c9792 Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 13 Jan 2016 09:11:56 +0100 Subject: [PATCH 06/25] PushRules settings: Display keywords modal dialog --- .../views/settings/Notifications.js | 43 ++++++++++++++++++- .../views/settings/Notifications.css | 16 +++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index c24937bc8..b92a79305 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -58,6 +58,8 @@ module.exports = React.createClass({ }; }, + keywordsDialogDiv: "", + componentWillMount: function() { this._refreshFromServer(); }, @@ -155,6 +157,19 @@ module.exports = React.createClass({ } }, + onKeywordsClicked: function(event) { + var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createDialog(QuestionDialog, { + title: "Keywords", + description: this.keywordsDialogDiv, + onFinished: function onFinished(should_leave) { + if (should_leave) { + // TODO + } + } + }); + }, + getRule: function(vectorRuleId) { for (var i in this.state.vectorPushRules) { var rule = this.state.vectorPushRules[i]; @@ -306,7 +321,7 @@ module.exports = React.createClass({ // it corresponds to all content push rules (stored in self.state.vectorContentRule) self.state.vectorPushRules.push({ "vectorRuleId": "keywords", - "description" : "Messages containing keywords", + "description" : (Messages containing keywords), "state": self.state.vectorContentRules.state, }); @@ -435,6 +450,32 @@ module.exports = React.createClass({ ); } + // Prepare keywords dialog here, in a render method, else React complains if + // it is done later from onKeywordsClicked + var keywords = []; + for (var i in this.state.vectorContentRules.rules) { + var rule = this.state.vectorContentRules.rules[i]; + keywords.push(rule.pattern); + } + + if (keywords.length) { + keywords = keywords.join(", "); + } + else { + keywords = ""; + } + + this.keywordsDialogDiv = ( +
+
+ +
+
+ +
+
+ ); + // Build the list of keywords rules that have been defined outside Vector UI var externalKeyWords = []; for (var i in this.state.externalContentRules) { diff --git a/src/skins/vector/css/vector-web/views/settings/Notifications.css b/src/skins/vector/css/vector-web/views/settings/Notifications.css index d6d14f8e5..0a50a691e 100644 --- a/src/skins/vector/css/vector-web/views/settings/Notifications.css +++ b/src/skins/vector/css/vector-web/views/settings/Notifications.css @@ -51,3 +51,19 @@ limitations under the License. .mx_UserNotifSettings_pushRulesTable tbody th:first-child { text-align: left; } + +.mx_UserNotifSettings_keywords { + cursor: pointer; + color: #76cfa6; +} + +.mx_UserNotifSettings_keywordsLabel { + text-align: left; + padding-bottom: 12px; +} + +.mx_UserNotifSettings_keywordsInput { + color: #747474; + font-weight: 300; + font-size: 15px; +} From 1c03c208e134a2485b239f285e9bdc741b3a1aff Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 13 Jan 2016 11:46:13 +0100 Subject: [PATCH 07/25] PushRules settings: update keywords list hs side --- .../views/settings/Notifications.js | 79 +++++++++++++++++-- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index b92a79305..db3e2879b 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -59,6 +59,7 @@ module.exports = React.createClass({ }, keywordsDialogDiv: "", + newKeywords: undefined, componentWillMount: function() { this._refreshFromServer(); @@ -92,7 +93,7 @@ module.exports = React.createClass({ switch (newPushRuleState) { case PushRuleState.ON: if (rule.actions.length !== 1) { - actions = ['notify']; + actions = this._actionsFor(PushRuleState.ON); } if (this.state.vectorContentRules.state === PushRuleState.OFF) { @@ -102,10 +103,7 @@ module.exports = React.createClass({ case PushRuleState.STRONG: if (rule.actions.length !== 3) { - actions = ['notify', - {'set_tweak': 'sound', 'value': 'default'}, - {'set_tweak': 'highlight', 'value': 'true'} - ]; + actions = this._actionsFor(PushRuleState.STRONG); } if (this.state.vectorContentRules.state === PushRuleState.OFF) { @@ -158,13 +156,60 @@ module.exports = React.createClass({ }, onKeywordsClicked: function(event) { + var self = this; + this.newKeywords = undefined; + var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createDialog(QuestionDialog, { title: "Keywords", description: this.keywordsDialogDiv, onFinished: function onFinished(should_leave) { - if (should_leave) { - // TODO + + if (should_leave && self.newKeywords) { + var cli = MatrixClientPeg.get(); + var deferreds = []; + + var newKeywords = self.newKeywords.split(','); + for (var i in newKeywords) { + newKeywords[i] = newKeywords[i].trim(); + } + self.setState({ + phase: self.phases.LOADING + }); + + // Remove per-word push rules of keywords that are no more in the list + var vectorContentRulesPatterns = []; + for (var i in self.state.vectorContentRules.rules) { + var rule = self.state.vectorContentRules.rules[i]; + + vectorContentRulesPatterns.push(rule.pattern); + + if (-1 === newKeywords.indexOf(rule.pattern)) { + deferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); + } + } + + // Add the new ones + for (var i in newKeywords) { + var keyword = newKeywords[i]; + + if (-1 === vectorContentRulesPatterns.indexOf(keyword)) { + deferreds.push(cli.addPushRule('global', 'content', keyword, { + actions: self._actionsFor(self.state.vectorContentRules.state), + pattern: keyword + })); + } + } + + q.all(deferreds).done(function(resps) { + self._refreshFromServer(); + }, function(error) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't update keywords", + description: error.toString() + }); + }); } } }); @@ -178,6 +223,18 @@ module.exports = React.createClass({ } } }, + + _actionsFor: function(pushRuleState) { + if (pushRuleState === PushRuleState.ON) { + return ['notify']; + } + else if (pushRuleState === PushRuleState.STRONG) { + return ['notify', + {'set_tweak': 'sound', 'value': 'default'}, + {'set_tweak': 'highlight', 'value': 'true'} + ];; + } + }, _refreshFromServer: function() { var self = this; @@ -441,6 +498,8 @@ module.exports = React.createClass({ }, render: function() { + var self = this; + if (this.state.phase === this.phases.LOADING) { var Loader = sdk.getComponent("elements.Spinner"); return ( @@ -465,13 +524,17 @@ module.exports = React.createClass({ keywords = ""; } + var onKeywordsChange = function(e) { + self.newKeywords = e.target.value; + }; + this.keywordsDialogDiv = (
- +
); From e5b7a47fee1170c809c50612206bbc0030750bad Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 13 Jan 2016 12:00:04 +0100 Subject: [PATCH 08/25] PushRules settings: if a newly typed keyword was part of a push rule not managed by the Vector UI, delete the rule and create it compliant with Vector parameters --- .../views/settings/Notifications.js | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index db3e2879b..0fee4ac38 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -167,7 +167,7 @@ module.exports = React.createClass({ if (should_leave && self.newKeywords) { var cli = MatrixClientPeg.get(); - var deferreds = []; + var removeDeferreds = []; var newKeywords = self.newKeywords.split(','); for (var i in newKeywords) { @@ -185,31 +185,46 @@ module.exports = React.createClass({ vectorContentRulesPatterns.push(rule.pattern); if (-1 === newKeywords.indexOf(rule.pattern)) { - deferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); + removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); } } - // Add the new ones - for (var i in newKeywords) { - var keyword = newKeywords[i]; + // If the keyword is part of `externalContentRules`, remove the rule + // before recreating it in the right Vector path + for (var i in self.state.externalContentRules) { + var rule = self.state.externalContentRules[i]; - if (-1 === vectorContentRulesPatterns.indexOf(keyword)) { - deferreds.push(cli.addPushRule('global', 'content', keyword, { - actions: self._actionsFor(self.state.vectorContentRules.state), - pattern: keyword - })); + if (-1 !== newKeywords.indexOf(rule.pattern)) { + removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); } } - q.all(deferreds).done(function(resps) { - self._refreshFromServer(); - }, function(error) { + var onError = function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Can't update keywords", description: error.toString() }); - }); + } + + // Then, add the new ones + q.all(removeDeferreds).done(function(resps) { + var deferreds = []; + for (var i in newKeywords) { + var keyword = newKeywords[i]; + + if (-1 === vectorContentRulesPatterns.indexOf(keyword)) { + deferreds.push(cli.addPushRule('global', 'content', keyword, { + actions: self._actionsFor(self.state.vectorContentRules.state), + pattern: keyword + })); + } + } + + q.all(deferreds).done(function(resps) { + self._refreshFromServer(); + }, onError); + }, onError); } } }); From c4cb37606b1efae9d3f9802a5d9fc4c54f207fbf Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 13 Jan 2016 15:47:00 +0100 Subject: [PATCH 09/25] PushRules settings: Added sanity checks on new keywords --- .../views/settings/Notifications.js | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 0fee4ac38..73a7693dd 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -132,7 +132,8 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Can't update user notification settings", - description: error.toString() + description: error.toString(), + onFinished: self._refreshFromServer }); }); } @@ -163,6 +164,7 @@ module.exports = React.createClass({ Modal.createDialog(QuestionDialog, { title: "Keywords", description: this.keywordsDialogDiv, + focus: false, onFinished: function onFinished(should_leave) { if (should_leave && self.newKeywords) { @@ -173,6 +175,15 @@ module.exports = React.createClass({ for (var i in newKeywords) { newKeywords[i] = newKeywords[i].trim(); } + + // Remove duplicates and empty + newKeywords = newKeywords.reduce(function(array, keyword){ + if (keyword !== "" && array.indexOf(keyword) < 0) { + array.push(keyword); + } + return array; + },[]); + self.setState({ phase: self.phases.LOADING }); @@ -184,7 +195,7 @@ module.exports = React.createClass({ vectorContentRulesPatterns.push(rule.pattern); - if (-1 === newKeywords.indexOf(rule.pattern)) { + if (newKeywords.indexOf(rule.pattern) < 0) { removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); } } @@ -194,7 +205,7 @@ module.exports = React.createClass({ for (var i in self.state.externalContentRules) { var rule = self.state.externalContentRules[i]; - if (-1 !== newKeywords.indexOf(rule.pattern)) { + if (newKeywords.indexOf(rule.pattern) >= 0) { removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); } } @@ -203,7 +214,8 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Can't update keywords", - description: error.toString() + description: error.toString(), + onFinished: self._refreshFromServer }); } @@ -213,7 +225,7 @@ module.exports = React.createClass({ for (var i in newKeywords) { var keyword = newKeywords[i]; - if (-1 === vectorContentRulesPatterns.indexOf(keyword)) { + if (vectorContentRulesPatterns.indexOf(keyword) < 0) { deferreds.push(cli.addPushRule('global', 'content', keyword, { actions: self._actionsFor(self.state.vectorContentRules.state), pattern: keyword @@ -394,7 +406,7 @@ module.exports = React.createClass({ self.state.vectorPushRules.push({ "vectorRuleId": "keywords", "description" : (Messages containing keywords), - "state": self.state.vectorContentRules.state, + "state": self.state.vectorContentRules.state }); // Messages just sent to the user @@ -541,6 +553,8 @@ module.exports = React.createClass({ var onKeywordsChange = function(e) { self.newKeywords = e.target.value; + + this.props.onFinished(false); }; this.keywordsDialogDiv = ( @@ -584,7 +598,7 @@ module.exports = React.createClass({
-

General use

+

General use

From 7fc5ab3c6e41abc9bb71ad892c44d183c8325011 Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 13 Jan 2016 16:36:57 +0100 Subject: [PATCH 10/25] PushRules settings: Use the new TextInputDialog to display keywords list. We earn the focus at the end of the keywords list and the management of enter and esc keys --- .../views/login/VectorCustomServerDialog.js | 2 +- .../views/settings/Notifications.js | 61 +++++++------------ src/skins/vector/css/common.css | 14 ++++- .../views/settings/Notifications.css | 11 ---- 4 files changed, 34 insertions(+), 54 deletions(-) diff --git a/src/components/views/login/VectorCustomServerDialog.js b/src/components/views/login/VectorCustomServerDialog.js index d8e07a53b..b3e99a97d 100644 --- a/src/components/views/login/VectorCustomServerDialog.js +++ b/src/components/views/login/VectorCustomServerDialog.js @@ -25,7 +25,7 @@ module.exports = React.createClass({ render: function() { return (
-
+
Custom Server Options
diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 73a7693dd..79ce4f46a 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -160,18 +160,31 @@ module.exports = React.createClass({ var self = this; this.newKeywords = undefined; - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { - title: "Keywords", - description: this.keywordsDialogDiv, - focus: false, - onFinished: function onFinished(should_leave) { + // Compute the keywords list to display + var keywords = []; + for (var i in this.state.vectorContentRules.rules) { + var rule = this.state.vectorContentRules.rules[i]; + keywords.push(rule.pattern); + } + if (keywords.length) { + keywords = keywords.join(", "); + } + else { + keywords = ""; + } - if (should_leave && self.newKeywords) { + var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); + Modal.createDialog(TextInputDialog, { + title: "Keywords", + description: "Enter keywords separated by a comma:", + value: keywords, + onFinished: function onFinished(should_leave, newValue) { + + if (should_leave && newValue !== keywords) { var cli = MatrixClientPeg.get(); var removeDeferreds = []; - var newKeywords = self.newKeywords.split(','); + var newKeywords = newValue.split(','); for (var i in newKeywords) { newKeywords[i] = newKeywords[i].trim(); } @@ -536,38 +549,6 @@ module.exports = React.createClass({ ); } - // Prepare keywords dialog here, in a render method, else React complains if - // it is done later from onKeywordsClicked - var keywords = []; - for (var i in this.state.vectorContentRules.rules) { - var rule = this.state.vectorContentRules.rules[i]; - keywords.push(rule.pattern); - } - - if (keywords.length) { - keywords = keywords.join(", "); - } - else { - keywords = ""; - } - - var onKeywordsChange = function(e) { - self.newKeywords = e.target.value; - - this.props.onFinished(false); - }; - - this.keywordsDialogDiv = ( -
-
- -
-
- -
-
- ); - // Build the list of keywords rules that have been defined outside Vector UI var externalKeyWords = []; for (var i in this.state.externalContentRules) { diff --git a/src/skins/vector/css/common.css b/src/skins/vector/css/common.css index 50ffe7bb6..d10fe8047 100644 --- a/src/skins/vector/css/common.css +++ b/src/skins/vector/css/common.css @@ -187,8 +187,7 @@ input[type=text]:focus, textarea:focus, .mx_RoomSettings textarea:focus { padding-right: 1em; } -.mx_ErrorDialogTitle, -.mx_QuestionDialogTitle { +.mx_Dialog_title { min-height: 16px; padding: 12px; border-bottom: 1px solid #a4a4a4; @@ -196,3 +195,14 @@ input[type=text]:focus, textarea:focus, .mx_RoomSettings textarea:focus { font-size: 18px; line-height: 1.4; } + +.mx_TextInputDialog_label { + text-align: left; + padding-bottom: 12px; +} + +.mx_TextInputDialog_input { + color: #747474; + font-weight: 300; + font-size: 15px; +} diff --git a/src/skins/vector/css/vector-web/views/settings/Notifications.css b/src/skins/vector/css/vector-web/views/settings/Notifications.css index 0a50a691e..76cae467e 100644 --- a/src/skins/vector/css/vector-web/views/settings/Notifications.css +++ b/src/skins/vector/css/vector-web/views/settings/Notifications.css @@ -56,14 +56,3 @@ limitations under the License. cursor: pointer; color: #76cfa6; } - -.mx_UserNotifSettings_keywordsLabel { - text-align: left; - padding-bottom: 12px; -} - -.mx_UserNotifSettings_keywordsInput { - color: #747474; - font-weight: 300; - font-size: 15px; -} From ac87830e4e40559198b5eb1f6d2a2f939492083c Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 13 Jan 2016 16:48:22 +0100 Subject: [PATCH 11/25] PushRules settings: Applied new wordings: On, Loud, Off --- .../views/settings/Notifications.js | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 79ce4f46a..3cad6c16b 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -32,7 +32,7 @@ var PushRuleState = { ON: "on", /** The user will receive push notification for this rule with sound and highlight if this is legitimate */ - STRONG: "strong", + LOUD: "loud", /** The push rule is disabled */ OFF: "off" }; @@ -101,9 +101,9 @@ module.exports = React.createClass({ } break; - case PushRuleState.STRONG: + case PushRuleState.LOUD: if (rule.actions.length !== 3) { - actions = this._actionsFor(PushRuleState.STRONG); + actions = this._actionsFor(PushRuleState.LOUD); } if (this.state.vectorContentRules.state === PushRuleState.OFF) { @@ -141,7 +141,7 @@ module.exports = React.createClass({ var rule = this.getRule(vectorRuleId); // For now, we support only enabled/disabled for hs default rules - // Translate ON, STRONG, OFF to one of the 2. + // Translate ON, LOUD, OFF to one of the 2. if (rule && rule.state !== newPushRuleState) { this.setState({ @@ -268,7 +268,7 @@ module.exports = React.createClass({ if (pushRuleState === PushRuleState.ON) { return ['notify']; } - else if (pushRuleState === PushRuleState.STRONG) { + else if (pushRuleState === PushRuleState.LOUD) { return ['notify', {'set_tweak': 'sound', 'value': 'default'}, {'set_tweak': 'highlight', 'value': 'true'} @@ -295,7 +295,7 @@ module.exports = React.createClass({ // HS default rules var defaultRules = {master: [], vector: {}, additional: [], fallthrough: [], suppression: []}; // Content/keyword rules - var contentRules = {on: [], on_but_disabled:[], strong: [], strong_but_disabled: [], other: []}; + var contentRules = {on: [], on_but_disabled:[], loud: [], loud_but_disabled: [], other: []}; for (var kind in rulesets.global) { for (var i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { @@ -317,7 +317,7 @@ module.exports = React.createClass({ } } else if (kind === 'content') { - // Count tweaks to determine if it is a ON or STRONG rule + // Count tweaks to determine if it is a ON or LOUD rule var tweaks = 0; for (var j in r.actions) { var action = r.actions[j]; @@ -338,10 +338,10 @@ module.exports = React.createClass({ break; case 2: if (r.enabled) { - contentRules.strong.push(r); + contentRules.loud.push(r); } else { - contentRules.strong_but_disabled.push(r); + contentRules.loud_but_disabled.push(r); } break; default: @@ -355,7 +355,7 @@ module.exports = React.createClass({ // Decide which content/keyword rules to display in Vector UI. // Vector displays a single global rule for a list of keywords // whereas Matrix has a push rule per keyword. - // Vector can set the unique rule in ON, STRONG or OFF state. + // Vector can set the unique rule in ON, LOUD or OFF state. // Matrix has enabled/disabled plus a combination of (highlight, sound) tweaks. // The code below determines which set of user's content push rules can be @@ -363,19 +363,19 @@ module.exports = React.createClass({ // Push rules that does not fir, ie defined by another Matrix client, ends // in self.state.externalContentRules. // There is priority in the determination of which set will be the displayed one. - // The set with rules that have STRONG tweaks is the first choice. Then, the ones + // The set with rules that have LOUD tweaks is the first choice. Then, the ones // with ON tweaks (no tweaks). - if (contentRules.strong.length) { + if (contentRules.loud.length) { self.state.vectorContentRules = { - state: PushRuleState.STRONG, - rules: contentRules.strong + state: PushRuleState.LOUD, + rules: contentRules.loud } - self.state.externalContentRules = [].concat(contentRules.strong_but_disabled, contentRules.on, contentRules.on_but_disabled, contentRules.other); + self.state.externalContentRules = [].concat(contentRules.loud_but_disabled, contentRules.on, contentRules.on_but_disabled, contentRules.other); } - else if (contentRules.strong_but_disabled.length) { + else if (contentRules.loud_but_disabled.length) { self.state.vectorContentRules = { state: PushRuleState.OFF, - rules: contentRules.strong_but_disabled + rules: contentRules.loud_but_disabled } self.state.externalContentRules = [].concat(contentRules.on, contentRules.on_but_disabled, contentRules.other); } @@ -404,7 +404,7 @@ module.exports = React.createClass({ // Messages containing user's display name // (skip contains_user_name which is too geeky) rule = defaultRules.vector['.m.rule.contains_display_name']; - state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; + state = (rule && rule.enabled) ? PushRuleState.LOUD : PushRuleState.OFF; self.state.vectorPushRules.push({ "vectorRuleId": "contains_display_name", "description" : "Messages containing my name", @@ -424,7 +424,7 @@ module.exports = React.createClass({ // Messages just sent to the user rule = defaultRules.vector['.m.rule.room_one_to_one']; - state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; + state = (rule && rule.enabled) ? PushRuleState.LOUD : PushRuleState.OFF; self.state.vectorPushRules.push({ "vectorRuleId": "room_one_to_one", "description" : "Messages just sent to me", @@ -435,7 +435,7 @@ module.exports = React.createClass({ // Invitation for the user rule = defaultRules.vector['.m.rule.invite_for_me']; - state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; + state = (rule && rule.enabled) ? PushRuleState.LOUD : PushRuleState.OFF; self.state.vectorPushRules.push({ "vectorRuleId": "invite_for_me", "description" : "When I'm invited to a room", @@ -452,12 +452,12 @@ module.exports = React.createClass({ "description" : "When people join or leave a room", "rule": rule, "state": state, - "disabled": PushRuleState.STRONG + "disabled": PushRuleState.LOUD }); // Incoming call rule = defaultRules.vector['.m.rule.call']; - state = (rule && rule.enabled) ? PushRuleState.STRONG : PushRuleState.OFF; + state = (rule && rule.enabled) ? PushRuleState.LOUD : PushRuleState.OFF; self.state.vectorPushRules.push({ "vectorRuleId": "call", "description" : "Call invitation", @@ -510,10 +510,10 @@ module.exports = React.createClass({
@@ -586,8 +586,8 @@ module.exports = React.createClass({ - - + + From 0475bcd9de0875e83b62fe2eb2306c3d4ac85163 Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 13 Jan 2016 17:10:26 +0100 Subject: [PATCH 12/25] PushRules settings: BF when changing state of the keywords rule with such a sequence: on -> off -> loud. --- .../views/settings/Notifications.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 3cad6c16b..ce1706100 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -117,9 +117,9 @@ module.exports = React.createClass({ } if (actions) { - // Note that the workaound in _updatePushRuleActions will automatically + // Note that the workaround in _updatePushRuleActions will automatically // enable the rule - deferreds.push(this._updatePushRuleActions(rule, actions)); + deferreds.push(this._updatePushRuleActions(rule, actions, enabled)); } else if (enabled != undefined) { deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled)); @@ -472,7 +472,7 @@ module.exports = React.createClass({ }); }, - _updatePushRuleActions: function(rule, actions) { + _updatePushRuleActions: function(rule, actions, enabled) { // Workaround for SYN-590 : Push rule update fails // Remove the rule and recreate it with the new actions var cli = MatrixClientPeg.get(); @@ -483,7 +483,18 @@ module.exports = React.createClass({ actions: actions, pattern: rule.pattern }).done(function() { - deferred.resolve(); + + // Then, if requested, enabled or disabled the rule + if (undefined != enabled) { + cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled).done(function() { + deferred.resolve(); + }, function(err) { + deferred.reject(err); + }); + } + else { + deferred.resolve(); + } }, function(err) { deferred.reject(err); }); From 629883731e5459392e8a3c28ee2ddf3a083dd9c6 Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 13 Jan 2016 17:56:59 +0100 Subject: [PATCH 13/25] PushRules settings: BF when adding a new keyword with the keywords rule in Off --- .../views/settings/Notifications.js | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index ce1706100..117a2948d 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -238,9 +238,24 @@ module.exports = React.createClass({ for (var i in newKeywords) { var keyword = newKeywords[i]; + var pushRuleStateKind = self.state.vectorContentRules.state; + if (pushRuleStateKind === PushRuleState.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) { + pushRuleStateKind = self._pushRuleStateKind(self.state.vectorContentRules.rules[0]); + } + else { + // ON is default + pushRuleStateKind = PushRuleState.ON; + } + } + if (vectorContentRulesPatterns.indexOf(keyword) < 0) { deferreds.push(cli.addPushRule('global', 'content', keyword, { - actions: self._actionsFor(self.state.vectorContentRules.state), + actions: self._actionsFor(pushRuleStateKind), pattern: keyword })); } @@ -275,6 +290,31 @@ module.exports = React.createClass({ ];; } }, + + // Determine whether a rule is in the PushRuleState.ON category or in PushRuleState.LOUD + // regardless of its enabled state. + _pushRuleStateKind: function(rule) { + var stateKind; + + // Count tweaks to determine if it is a ON or LOUD rule + var tweaks = 0; + for (var j in rule.actions) { + var action = rule.actions[j]; + if (action.set_tweak === 'sound' || + (action.set_tweak === 'highlight' && action.value)) { + tweaks++; + } + } + switch (tweaks) { + case 0: + stateKind = PushRuleState.ON; + break; + case 2: + stateKind = PushRuleState.LOUD; + break; + } + return stateKind; + }, _refreshFromServer: function() { var self = this; @@ -317,18 +357,8 @@ module.exports = React.createClass({ } } else if (kind === 'content') { - // Count tweaks to determine if it is a ON or LOUD rule - var tweaks = 0; - for (var j in r.actions) { - var action = r.actions[j]; - if (action.set_tweak === 'sound' || - (action.set_tweak === 'highlight' && action.value)) { - tweaks++; - } - } - - switch (tweaks) { - case 0: + switch (self._pushRuleStateKind(r)) { + case PushRuleState.ON: if (r.enabled) { contentRules.on.push(r); } @@ -336,7 +366,7 @@ module.exports = React.createClass({ contentRules.on_but_disabled.push(r); } break; - case 2: + case PushRuleState.LOUD: if (r.enabled) { contentRules.loud.push(r); } From 0577edb055e464942e32ca97290e8f56ef1b410b Mon Sep 17 00:00:00 2001 From: manuroe Date: Thu, 14 Jan 2016 11:03:51 +0100 Subject: [PATCH 14/25] PushRules settings: Added master push rule --- .../views/settings/Notifications.js | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 117a2948d..ce4efc694 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -49,6 +49,7 @@ module.exports = React.createClass({ getInitialState: function() { return { phase: this.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 state: PushRuleState.ON, @@ -66,6 +67,17 @@ module.exports = React.createClass({ }, onEnableNotificationsChange: function(event) { + var self = this; + this.setState({ + phase: this.phases.LOADING + }); + + MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !event.target.checked).done(function() { + self._refreshFromServer(); + }); + }, + + onEnableDesktopNotificationsChange: function(event) { UserSettingsStore.setEnableNotifications(event.target.checked); }, @@ -427,7 +439,12 @@ module.exports = React.createClass({ self.state.externalContentRules = contentRules.other; } - // Build the rules displayed by Vector UI + // Get the master rule if any defined by the hs + if (defaultRules.master.length > 0) { + self.state.masterPushRule = defaultRules.master[0]; + } + + // Build the rules displayed in Vector UI matrix table self.state.vectorPushRules = []; var rule, state; @@ -590,6 +607,38 @@ module.exports = React.createClass({ ); } + if (this.state.masterPushRule) { + var masterPushRuleDiv = ( +
+
+ +
+
+ +
+
+ ); + } + + // When enabled, the master rule inhibits all existing rules + if (this.state.masterPushRule.enabled) { + return ( +
+ {masterPushRuleDiv} + +
+ All notifications are currently disabled for all devices. +
+
+ ); + } + // Build the list of keywords rules that have been defined outside Vector UI var externalKeyWords = []; for (var i in this.state.externalContentRules) { @@ -603,18 +652,21 @@ module.exports = React.createClass({ return (
+ + {masterPushRuleDiv} +
- + onChange={ this.onEnableDesktopNotificationsChange } />
-
From 378f4bb85c41716dfbdebdba6a10fcfa5000152c Mon Sep 17 00:00:00 2001 From: manuroe Date: Thu, 14 Jan 2016 11:15:59 +0100 Subject: [PATCH 15/25] PushRules settings: Display keywords in alphabetical order --- src/components/views/settings/Notifications.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index ce4efc694..679da47d2 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -59,9 +59,6 @@ module.exports = React.createClass({ }; }, - keywordsDialogDiv: "", - newKeywords: undefined, - componentWillMount: function() { this._refreshFromServer(); }, @@ -170,7 +167,6 @@ module.exports = React.createClass({ onKeywordsClicked: function(event) { var self = this; - this.newKeywords = undefined; // Compute the keywords list to display var keywords = []; @@ -179,6 +175,10 @@ module.exports = React.createClass({ keywords.push(rule.pattern); } 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 { From 7412fc7f9778f2f2058c0ce4f98f60742d25dbc6 Mon Sep 17 00:00:00 2001 From: manuroe Date: Fri, 15 Jan 2016 10:51:42 +0100 Subject: [PATCH 16/25] PushRules settings: changed wordings --- .../views/settings/Notifications.js | 131 +++++++++--------- 1 file changed, 65 insertions(+), 66 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 679da47d2..e5d0adf62 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -27,7 +27,7 @@ var Modal = require('matrix-react-sdk/lib/Modal'); * @readonly * @enum {string} */ -var PushRuleState = { +var PushRuleVectorState = { /** The user will receive push notification for this rule */ ON: "on", /** The user will receive push notification for this rule with sound and @@ -52,7 +52,7 @@ module.exports = React.createClass({ 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 - state: PushRuleState.ON, + vectorState: PushRuleVectorState.ON, rules: [] }, externalContentRules: [] // Keyword push rules that have been defined outside Vector UI @@ -82,10 +82,10 @@ module.exports = React.createClass({ var self = this; var cli = MatrixClientPeg.get(); var vectorRuleId = event.target.className.split("-")[0]; - var newPushRuleState = event.target.className.split("-")[1]; + var newPushRuleVectorState = event.target.className.split("-")[1]; if ("keywords" === vectorRuleId - && this.state.vectorContentRules.state !== newPushRuleState + && this.state.vectorContentRules.vectorState !== newPushRuleVectorState && this.state.vectorContentRules.rules.length) { this.setState({ @@ -99,28 +99,28 @@ module.exports = React.createClass({ var enabled; var actions; - switch (newPushRuleState) { - case PushRuleState.ON: + switch (newPushRuleVectorState) { + case PushRuleVectorState.ON: if (rule.actions.length !== 1) { - actions = this._actionsFor(PushRuleState.ON); + actions = this._actionsFor(PushRuleVectorState.ON); } - if (this.state.vectorContentRules.state === PushRuleState.OFF) { + if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { enabled = true; } break; - case PushRuleState.LOUD: + case PushRuleVectorState.LOUD: if (rule.actions.length !== 3) { - actions = this._actionsFor(PushRuleState.LOUD); + actions = this._actionsFor(PushRuleVectorState.LOUD); } - if (this.state.vectorContentRules.state === PushRuleState.OFF) { + if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { enabled = true; } break; - case PushRuleState.OFF: + case PushRuleVectorState.OFF: enabled = false; break; } @@ -151,15 +151,14 @@ module.exports = React.createClass({ // For now, we support only enabled/disabled for hs default rules // Translate ON, LOUD, OFF to one of the 2. - if (rule && rule.state !== newPushRuleState) { + if (rule && rule.vectorState !== newPushRuleVectorState) { this.setState({ phase: this.phases.LOADING }); - cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, (newPushRuleState !== PushRuleState.OFF)).done(function() { - - self._refreshFromServer(); + cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, (newPushRuleVectorState !== PushRuleVectorState.OFF)).done(function() { + self._refreshFromServer(); }); } } @@ -250,24 +249,24 @@ module.exports = React.createClass({ for (var i in newKeywords) { var keyword = newKeywords[i]; - var pushRuleStateKind = self.state.vectorContentRules.state; - if (pushRuleStateKind === PushRuleState.OFF) { + var 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) { - pushRuleStateKind = self._pushRuleStateKind(self.state.vectorContentRules.rules[0]); + pushRuleVectorStateKind = self._pushRuleVectorStateKind(self.state.vectorContentRules.rules[0]); } else { // ON is default - pushRuleStateKind = PushRuleState.ON; + pushRuleVectorStateKind = PushRuleVectorState.ON; } } if (vectorContentRulesPatterns.indexOf(keyword) < 0) { deferreds.push(cli.addPushRule('global', 'content', keyword, { - actions: self._actionsFor(pushRuleStateKind), + actions: self._actionsFor(pushRuleVectorStateKind), pattern: keyword })); } @@ -291,11 +290,11 @@ module.exports = React.createClass({ } }, - _actionsFor: function(pushRuleState) { - if (pushRuleState === PushRuleState.ON) { + _actionsFor: function(pushRuleVectorState) { + if (pushRuleVectorState === PushRuleVectorState.ON) { return ['notify']; } - else if (pushRuleState === PushRuleState.LOUD) { + else if (pushRuleVectorState === PushRuleVectorState.LOUD) { return ['notify', {'set_tweak': 'sound', 'value': 'default'}, {'set_tweak': 'highlight', 'value': 'true'} @@ -303,9 +302,9 @@ module.exports = React.createClass({ } }, - // Determine whether a rule is in the PushRuleState.ON category or in PushRuleState.LOUD + // Determine whether a rule is in the PushRuleVectorState.ON category or in PushRuleVectorState.LOUD // regardless of its enabled state. - _pushRuleStateKind: function(rule) { + _pushRuleVectorStateKind: function(rule) { var stateKind; // Count tweaks to determine if it is a ON or LOUD rule @@ -319,10 +318,10 @@ module.exports = React.createClass({ } switch (tweaks) { case 0: - stateKind = PushRuleState.ON; + stateKind = PushRuleVectorState.ON; break; case 2: - stateKind = PushRuleState.LOUD; + stateKind = PushRuleVectorState.LOUD; break; } return stateKind; @@ -369,8 +368,8 @@ module.exports = React.createClass({ } } else if (kind === 'content') { - switch (self._pushRuleStateKind(r)) { - case PushRuleState.ON: + switch (self._pushRuleVectorStateKind(r)) { + case PushRuleVectorState.ON: if (r.enabled) { contentRules.on.push(r); } @@ -378,7 +377,7 @@ module.exports = React.createClass({ contentRules.on_but_disabled.push(r); } break; - case PushRuleState.LOUD: + case PushRuleVectorState.LOUD: if (r.enabled) { contentRules.loud.push(r); } @@ -394,7 +393,7 @@ module.exports = React.createClass({ } } - // Decide which content/keyword rules to display in Vector UI. + // Decide which content rules to display in Vector UI. // Vector displays a single global rule for a list of keywords // whereas Matrix has a push rule per keyword. // Vector can set the unique rule in ON, LOUD or OFF state. @@ -402,35 +401,35 @@ module.exports = React.createClass({ // The code below determines which set of user's content push rules can be // displayed by the vector UI. - // Push rules that does not fir, ie defined by another Matrix client, ends + // Push rules that does not fit, ie defined by another Matrix client, ends // in self.state.externalContentRules. // There is priority in the determination of which set will be the displayed one. // The set with rules that have LOUD tweaks is the first choice. Then, the ones // with ON tweaks (no tweaks). if (contentRules.loud.length) { self.state.vectorContentRules = { - state: PushRuleState.LOUD, + vectorState: PushRuleVectorState.LOUD, rules: contentRules.loud } self.state.externalContentRules = [].concat(contentRules.loud_but_disabled, contentRules.on, contentRules.on_but_disabled, contentRules.other); } else if (contentRules.loud_but_disabled.length) { self.state.vectorContentRules = { - state: PushRuleState.OFF, + vectorState: PushRuleVectorState.OFF, rules: contentRules.loud_but_disabled } self.state.externalContentRules = [].concat(contentRules.on, contentRules.on_but_disabled, contentRules.other); } else if (contentRules.on.length) { self.state.vectorContentRules = { - state: PushRuleState.ON, + vectorState: PushRuleVectorState.ON, rules: contentRules.on } self.state.externalContentRules = [].concat(contentRules.on_but_disabled, contentRules.other); } else if (contentRules.on_but_disabled.length) { self.state.vectorContentRules = { - state: PushRuleState.OFF, + vectorState: PushRuleVectorState.OFF, rules: contentRules.on_but_disabled } self.state.externalContentRules = contentRules.other; @@ -446,18 +445,18 @@ module.exports = React.createClass({ // Build the rules displayed in Vector UI matrix table self.state.vectorPushRules = []; - var rule, state; + var rule, vectorState; // Messages containing user's display name // (skip contains_user_name which is too geeky) rule = defaultRules.vector['.m.rule.contains_display_name']; - state = (rule && rule.enabled) ? PushRuleState.LOUD : PushRuleState.OFF; + vectorState = (rule && rule.enabled) ? PushRuleVectorState.LOUD : PushRuleVectorState.OFF; self.state.vectorPushRules.push({ "vectorRuleId": "contains_display_name", "description" : "Messages containing my name", "rule": rule, - "state": state, - "disabled": PushRuleState.ON + "vectorState": vectorState, + "disabled": PushRuleVectorState.ON }); // Messages containing keywords @@ -466,51 +465,51 @@ module.exports = React.createClass({ self.state.vectorPushRules.push({ "vectorRuleId": "keywords", "description" : (Messages containing keywords), - "state": self.state.vectorContentRules.state + "vectorState": self.state.vectorContentRules.vectorState }); // Messages just sent to the user rule = defaultRules.vector['.m.rule.room_one_to_one']; - state = (rule && rule.enabled) ? PushRuleState.LOUD : PushRuleState.OFF; + vectorState = (rule && rule.enabled) ? PushRuleVectorState.LOUD : PushRuleVectorState.OFF; self.state.vectorPushRules.push({ "vectorRuleId": "room_one_to_one", "description" : "Messages just sent to me", "rule": rule, - "state": state, - "disabled": PushRuleState.ON + "vectorState": vectorState, + "disabled": PushRuleVectorState.ON }); // Invitation for the user rule = defaultRules.vector['.m.rule.invite_for_me']; - state = (rule && rule.enabled) ? PushRuleState.LOUD : PushRuleState.OFF; + vectorState = (rule && rule.enabled) ? PushRuleVectorState.LOUD : PushRuleVectorState.OFF; self.state.vectorPushRules.push({ "vectorRuleId": "invite_for_me", "description" : "When I'm invited to a room", "rule": rule, - "state": state, - "disabled": PushRuleState.ON + "vectorState": vectorState, + "disabled": PushRuleVectorState.ON }); // When people join or leave a room rule = defaultRules.vector['.m.rule.member_event']; - state = (rule && rule.enabled) ? PushRuleState.ON : PushRuleState.OFF; + vectorState = (rule && rule.enabled) ? PushRuleVectorState.ON : PushRuleVectorState.OFF; self.state.vectorPushRules.push({ "vectorRuleId": "member_event", "description" : "When people join or leave a room", "rule": rule, - "state": state, - "disabled": PushRuleState.LOUD + "vectorState": vectorState, + "disabled": PushRuleVectorState.LOUD }); // Incoming call rule = defaultRules.vector['.m.rule.call']; - state = (rule && rule.enabled) ? PushRuleState.LOUD : PushRuleState.OFF; + vectorState = (rule && rule.enabled) ? PushRuleVectorState.LOUD : PushRuleVectorState.OFF; self.state.vectorPushRules.push({ "vectorRuleId": "call", "description" : "Call invitation", "rule": rule, - "state": state, - "disabled": PushRuleState.ON + "vectorState": vectorState, + "disabled": PushRuleVectorState.ON }); self.setState({ @@ -552,7 +551,7 @@ module.exports = React.createClass({ return deferred.promise; }, - renderNotifRulesTableRow: function(title, className, pushRuleState, disabled) { + renderNotifRulesTableRow: function(title, className, pushRuleVectorState, disabled) { return (
@@ -590,7 +589,7 @@ module.exports = React.createClass({ var rows = []; for (var i in this.state.vectorPushRules) { var rule = this.state.vectorPushRules[i]; - rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.state, rule.disabled)); + rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState, rule.disabled)); } return rows; }, @@ -599,7 +598,7 @@ module.exports = React.createClass({ var self = this; if (this.state.phase === this.phases.LOADING) { - var Loader = sdk.getComponent("elements.Spinner"); + var Loader = sdk.getComponent("elements.Spinner"); return (
From c3469b5b51958d750fd40d20501bacee18def917 Mon Sep 17 00:00:00 2001 From: manuroe Date: Fri, 15 Jan 2016 11:29:03 +0100 Subject: [PATCH 17/25] PushRules settings: coding: separate UI and data management --- .../views/settings/Notifications.js | 313 ++++++++++-------- 1 file changed, 166 insertions(+), 147 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index e5d0adf62..d57c5481a 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -79,87 +79,16 @@ module.exports = React.createClass({ }, onNotifStateButtonClicked: function(event) { - var self = this; - var cli = MatrixClientPeg.get(); var vectorRuleId = event.target.className.split("-")[0]; var newPushRuleVectorState = event.target.className.split("-")[1]; - if ("keywords" === vectorRuleId - && this.state.vectorContentRules.vectorState !== newPushRuleVectorState - && this.state.vectorContentRules.rules.length) { - - this.setState({ - phase: this.phases.LOADING - }); - - // Update all rules in self.state.vectorContentRules - var deferreds = []; - for (var i in this.state.vectorContentRules.rules) { - var rule = this.state.vectorContentRules.rules[i]; - - var enabled; - var actions; - switch (newPushRuleVectorState) { - case PushRuleVectorState.ON: - if (rule.actions.length !== 1) { - actions = this._actionsFor(PushRuleVectorState.ON); - } - - if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { - enabled = true; - } - break; - - case PushRuleVectorState.LOUD: - if (rule.actions.length !== 3) { - actions = this._actionsFor(PushRuleVectorState.LOUD); - } - - if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { - enabled = true; - } - break; - - case PushRuleVectorState.OFF: - enabled = false; - break; - } - - 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)); - } - } - - q.all(deferreds).done(function(resps) { - self._refreshFromServer(); - }, function(error) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Can't update user notification settings", - description: error.toString(), - onFinished: self._refreshFromServer - }); - }); + if ("keywords" === vectorRuleId) { + this._changeKeywordsPushRuleVectorState(newPushRuleVectorState) } else { var rule = this.getRule(vectorRuleId); - - // For now, we support only enabled/disabled for hs default rules - // Translate ON, LOUD, OFF to one of the 2. - if (rule && rule.vectorState !== newPushRuleVectorState) { - - this.setState({ - phase: this.phases.LOADING - }); - - cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, (newPushRuleVectorState !== PushRuleVectorState.OFF)).done(function() { - self._refreshFromServer(); - }); + if (rule) { + this._changePushRuleVectorState(rule, newPushRuleVectorState); } } }, @@ -192,9 +121,6 @@ module.exports = React.createClass({ onFinished: function onFinished(should_leave, newValue) { if (should_leave && newValue !== keywords) { - var cli = MatrixClientPeg.get(); - var removeDeferreds = []; - var newKeywords = newValue.split(','); for (var i in newKeywords) { newKeywords[i] = newKeywords[i].trim(); @@ -207,75 +133,8 @@ module.exports = React.createClass({ } return array; },[]); - - self.setState({ - phase: self.phases.LOADING - }); - - // Remove per-word push rules of keywords that are no more in the list - var vectorContentRulesPatterns = []; - for (var i in self.state.vectorContentRules.rules) { - var rule = self.state.vectorContentRules.rules[i]; - - vectorContentRulesPatterns.push(rule.pattern); - - if (newKeywords.indexOf(rule.pattern) < 0) { - removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); - } - } - - // If the keyword is part of `externalContentRules`, remove the rule - // before recreating it in the right Vector path - for (var i in self.state.externalContentRules) { - var rule = self.state.externalContentRules[i]; - - if (newKeywords.indexOf(rule.pattern) >= 0) { - removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); - } - } - - var onError = function(error) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Can't update keywords", - description: error.toString(), - onFinished: self._refreshFromServer - }); - } - - // Then, add the new ones - q.all(removeDeferreds).done(function(resps) { - var deferreds = []; - for (var i in newKeywords) { - var keyword = newKeywords[i]; - - var 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 = self._pushRuleVectorStateKind(self.state.vectorContentRules.rules[0]); - } - else { - // ON is default - pushRuleVectorStateKind = PushRuleVectorState.ON; - } - } - - if (vectorContentRulesPatterns.indexOf(keyword) < 0) { - deferreds.push(cli.addPushRule('global', 'content', keyword, { - actions: self._actionsFor(pushRuleVectorStateKind), - pattern: keyword - })); - } - } - - q.all(deferreds).done(function(resps) { - self._refreshFromServer(); - }, onError); - }, onError); + + self._updateKeywords(newKeywords); } } }); @@ -326,6 +185,166 @@ module.exports = React.createClass({ } return stateKind; }, + + _changePushRuleVectorState: function(rule, newPushRuleVectorState) { + // For now, we support only enabled/disabled for hs default rules + // Translate ON, LOUD, OFF to one of the 2. + if (rule && rule.vectorState !== newPushRuleVectorState) { + + this.setState({ + phase: this.phases.LOADING + }); + + var self = this; + MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, (newPushRuleVectorState !== PushRuleVectorState.OFF)).done(function() { + self._refreshFromServer(); + }); + } + }, + + _changeKeywordsPushRuleVectorState: function(newPushRuleVectorState) { + // Is there really a change? + if (this.state.vectorContentRules.vectorState === newPushRuleVectorState + || this.state.vectorContentRules.rules.length === 0) { + return; + } + + var self = this; + var cli = MatrixClientPeg.get(); + + this.setState({ + phase: this.phases.LOADING + }); + + // Update all rules in self.state.vectorContentRules + var deferreds = []; + for (var i in this.state.vectorContentRules.rules) { + var rule = this.state.vectorContentRules.rules[i]; + + var enabled; + var actions; + switch (newPushRuleVectorState) { + case PushRuleVectorState.ON: + if (rule.actions.length !== 1) { + actions = this._actionsFor(PushRuleVectorState.ON); + } + + if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { + enabled = true; + } + break; + + case PushRuleVectorState.LOUD: + if (rule.actions.length !== 3) { + actions = this._actionsFor(PushRuleVectorState.LOUD); + } + + if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { + enabled = true; + } + break; + + case PushRuleVectorState.OFF: + enabled = false; + break; + } + + 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)); + } + } + + q.all(deferreds).done(function(resps) { + self._refreshFromServer(); + }, function(error) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't update user notification settings", + description: error.toString(), + onFinished: self._refreshFromServer + }); + }); + }, + + _updateKeywords: function(newKeywords) { + this.setState({ + phase: this.phases.LOADING + }); + + var self = this; + var cli = MatrixClientPeg.get(); + var removeDeferreds = []; + + // Remove per-word push rules of keywords that are no more in the list + var vectorContentRulesPatterns = []; + for (var i in self.state.vectorContentRules.rules) { + var rule = self.state.vectorContentRules.rules[i]; + + vectorContentRulesPatterns.push(rule.pattern); + + if (newKeywords.indexOf(rule.pattern) < 0) { + removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); + } + } + + // If the keyword is part of `externalContentRules`, remove the rule + // before recreating it in the right Vector path + for (var i in self.state.externalContentRules) { + var rule = self.state.externalContentRules[i]; + + if (newKeywords.indexOf(rule.pattern) >= 0) { + removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); + } + } + + var onError = function(error) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't update keywords", + description: error.toString(), + onFinished: self._refreshFromServer + }); + } + + // Then, add the new ones + q.all(removeDeferreds).done(function(resps) { + var deferreds = []; + for (var i in newKeywords) { + var keyword = newKeywords[i]; + + var 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 = self._pushRuleVectorStateKind(self.state.vectorContentRules.rules[0]); + } + else { + // ON is default + pushRuleVectorStateKind = PushRuleVectorState.ON; + } + } + + if (vectorContentRulesPatterns.indexOf(keyword) < 0) { + deferreds.push(cli.addPushRule('global', 'content', keyword, { + actions: self._actionsFor(pushRuleVectorStateKind), + pattern: keyword + })); + } + } + + q.all(deferreds).done(function(resps) { + self._refreshFromServer(); + }, onError); + }, onError); + }, _refreshFromServer: function() { var self = this; From 2dd2acd4e026c9d1578e3c217708a93761a7d585 Mon Sep 17 00:00:00 2001 From: manuroe Date: Fri, 15 Jan 2016 14:28:50 +0100 Subject: [PATCH 18/25] PushRules settings: BF adding a keyword when the keywords rule is OFF --- .../views/settings/Notifications.js | 77 +++++++++++++------ 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index d57c5481a..faca5c495 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -83,12 +83,12 @@ module.exports = React.createClass({ var newPushRuleVectorState = event.target.className.split("-")[1]; if ("keywords" === vectorRuleId) { - this._changeKeywordsPushRuleVectorState(newPushRuleVectorState) + this._setKeywordsPushRuleVectorState(newPushRuleVectorState) } else { var rule = this.getRule(vectorRuleId); if (rule) { - this._changePushRuleVectorState(rule, newPushRuleVectorState); + this._setPushRuleVectorState(rule, newPushRuleVectorState); } } }, @@ -134,7 +134,7 @@ module.exports = React.createClass({ return array; },[]); - self._updateKeywords(newKeywords); + self._setKeywords(newKeywords); } } }); @@ -186,7 +186,7 @@ module.exports = React.createClass({ return stateKind; }, - _changePushRuleVectorState: function(rule, newPushRuleVectorState) { + _setPushRuleVectorState: function(rule, newPushRuleVectorState) { // For now, we support only enabled/disabled for hs default rules // Translate ON, LOUD, OFF to one of the 2. if (rule && rule.vectorState !== newPushRuleVectorState) { @@ -202,7 +202,7 @@ module.exports = React.createClass({ } }, - _changeKeywordsPushRuleVectorState: function(newPushRuleVectorState) { + _setKeywordsPushRuleVectorState: function(newPushRuleVectorState) { // Is there really a change? if (this.state.vectorContentRules.vectorState === newPushRuleVectorState || this.state.vectorContentRules.rules.length === 0) { @@ -221,8 +221,7 @@ module.exports = React.createClass({ for (var i in this.state.vectorContentRules.rules) { var rule = this.state.vectorContentRules.rules[i]; - var enabled; - var actions; + var enabled, actions; switch (newPushRuleVectorState) { case PushRuleVectorState.ON: if (rule.actions.length !== 1) { @@ -271,7 +270,7 @@ module.exports = React.createClass({ }); }, - _updateKeywords: function(newKeywords) { + _setKeywords: function(newKeywords) { this.setState({ phase: this.phases.LOADING }); @@ -314,30 +313,40 @@ module.exports = React.createClass({ // Then, add the new ones q.all(removeDeferreds).done(function(resps) { var deferreds = []; + + var 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 = self._pushRuleVectorStateKind(self.state.vectorContentRules.rules[0]); + } + else { + // ON is default + pushRuleVectorStateKind = PushRuleVectorState.ON; + } + } + for (var i in newKeywords) { var keyword = newKeywords[i]; - var 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 = self._pushRuleVectorStateKind(self.state.vectorContentRules.rules[0]); + if (vectorContentRulesPatterns.indexOf(keyword) < 0) { + if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) { + deferreds.push(cli.addPushRule + ('global', 'content', keyword, { + actions: self._actionsFor(pushRuleVectorStateKind), + pattern: keyword + })); } else { - // ON is default - pushRuleVectorStateKind = PushRuleVectorState.ON; + deferreds.push(self._addDisabledPushRule('global', 'content', keyword, { + actions: self._actionsFor(pushRuleVectorStateKind), + pattern: keyword + })); } } - - if (vectorContentRulesPatterns.indexOf(keyword) < 0) { - deferreds.push(cli.addPushRule('global', 'content', keyword, { - actions: self._actionsFor(pushRuleVectorStateKind), - pattern: keyword - })); - } } q.all(deferreds).done(function(resps) { @@ -346,6 +355,24 @@ module.exports = React.createClass({ }, onError); }, + // Create a push rule but disabled + _addDisabledPushRule: function(scope, kind, ruleId, body) { + var cli = MatrixClientPeg.get(); + var deferred = q.defer(); + + cli.addPushRule(scope, kind, ruleId, body).done(function() { + cli.setPushRuleEnabled(scope, kind, ruleId, false).done(function() { + deferred.resolve(); + }, function(err) { + deferred.reject(err); + }); + }, function(err) { + deferred.reject(err); + }); + + return deferred.promise; + }, + _refreshFromServer: function() { var self = this; MatrixClientPeg.get().getPushRules().done(function(rulesets) { From cb8b052dc0b29daedb2cf3a6f540c83808c082b2 Mon Sep 17 00:00:00 2001 From: manuroe Date: Fri, 15 Jan 2016 16:45:27 +0100 Subject: [PATCH 19/25] PushRules settings: Show unmanaged rules into an "advanced section" --- .../views/settings/Notifications.js | 77 ++++++++++++++----- .../views/settings/Notifications.css | 4 + 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index faca5c495..a551ba10c 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -55,6 +55,7 @@ module.exports = React.createClass({ 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 }; }, @@ -378,19 +379,24 @@ module.exports = React.createClass({ MatrixClientPeg.get().getPushRules().done(function(rulesets) { MatrixClientPeg.get().pushRules = rulesets; - // Get homeserver default rules expected by Vector + // Get homeserver default rules and triage them by categories var rule_categories = { + // The master rule (all notifications disabling) '.m.rule.master': 'master', + // The rules displayed by Vector UI + // XXX: .m.rule.contains_user_name is not managed (not a fancy rule for Vector?) '.m.rule.contains_display_name': 'vector', '.m.rule.room_one_to_one': 'vector', '.m.rule.invite_for_me': 'vector', '.m.rule.member_event': 'vector', - '.m.rule.call': 'vector', + '.m.rule.call': 'vector' + + // Others go to others }; // HS default rules - var defaultRules = {master: [], vector: {}, additional: [], fallthrough: [], suppression: []}; + var defaultRules = {master: [], vector: {}, others: []}; // Content/keyword rules var contentRules = {on: [], on_but_disabled:[], loud: [], loud_but_disabled: [], other: []}; @@ -401,16 +407,15 @@ module.exports = React.createClass({ r.kind = kind; if (r.rule_id[0] === '.') { if (cat) { - if (cat === 'vector') - { + if (cat === 'vector') { defaultRules.vector[r.rule_id] = r; } - else - { + else { defaultRules[cat].push(r); } - } else { - defaultRules.additional.push(r); + } + else { + defaultRules['others'].push(r); } } else if (kind === 'content') { @@ -558,6 +563,25 @@ module.exports = React.createClass({ "disabled": PushRuleVectorState.ON }); + // Build the rules not managed by Vector UI + var otherRulesDescriptions = { + '.m.rule.suppress_notices': "Suppress notifications from bots", + '.m.rule.message': "Notify for all other messages/rooms", + '.m.rule.fallback': "Notify me for anything else" + }; + + self.state.externalPushRules = []; + for (var i in defaultRules.others) { + var rule = defaultRules.others[i]; + var 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); + } + } + self.setState({ phase: self.phases.DISPLAY }); @@ -672,6 +696,7 @@ module.exports = React.createClass({ } // When enabled, the master rule inhibits all existing rules + // So do not show all notification settings if (this.state.masterPushRule.enabled) { return (
@@ -684,17 +709,37 @@ module.exports = React.createClass({ ); } - // Build the list of keywords rules that have been defined outside Vector UI + // Build external push rules + var externalRules = []; + for (var i in this.state.externalPushRules) { + var rule = this.state.externalPushRules[i]; + externalRules.push(
  • { rule.description }
  • ); + } + + // Show keywords not displayed by the vector UI as a single external push rule var externalKeyWords = []; for (var i in this.state.externalContentRules) { var rule = this.state.externalContentRules[i]; externalKeyWords.push(rule.pattern); } - if (externalKeyWords.length) { externalKeyWords = externalKeyWords.join(", "); + externalRules.push(
  • Notifications on the following keywords follow rules which can’t be displayed here: { externalKeyWords }
  • ); } - + + var advancedSettings; + if (externalRules.length) { + advancedSettings = ( +
    +

    Advanced notifications settings

    + There are advanced rules which are not shown here. You might have configured them in another client than Vector. You cannot tune them in Vector but they still apply. +
      + { externalRules } +
    +
    + ); + } + return (
    @@ -737,12 +782,8 @@ module.exports = React.createClass({
    -
    NormalStrongOnLoud Off
    @@ -560,26 +559,26 @@ module.exports = React.createClass({ - - -
    -
    - -
    -

    - Warning: Push rules on the following keywords has been defined:
    - { externalKeyWords } + { advancedSettings } +
    diff --git a/src/skins/vector/css/vector-web/views/settings/Notifications.css b/src/skins/vector/css/vector-web/views/settings/Notifications.css index 76cae467e..f0446e43b 100644 --- a/src/skins/vector/css/vector-web/views/settings/Notifications.css +++ b/src/skins/vector/css/vector-web/views/settings/Notifications.css @@ -33,6 +33,10 @@ limitations under the License. display: table-cell; } +.mx_UserNotifSettings_pushRulesTableWrapper { + padding-bottom: 21px; +} + .mx_UserNotifSettings_pushRulesTable { width: 100%; table-layout: fixed; From d7ffe70d4447de27c8ce68ab881683402d4b3eae Mon Sep 17 00:00:00 2001 From: manuroe Date: Fri, 15 Jan 2016 17:28:57 +0100 Subject: [PATCH 20/25] PushRules settings: Applied Amandine's review comments --- src/components/views/settings/Notifications.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index a551ba10c..6cbff4711 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -732,7 +732,8 @@ module.exports = React.createClass({ advancedSettings = (

    Advanced notifications settings

    - There are advanced rules which are not shown here. You might have configured them in another client than Vector. You cannot tune them in Vector but they still apply. + There are advanced notifications which are not shown here.
    + You might have configured them in another client than Vector. You cannot tune them in Vector but they still apply.
      { externalRules }
    From 830160f074a4312f3bcf5a8a9f7e0297a01e221a Mon Sep 17 00:00:00 2001 From: manuroe Date: Mon, 18 Jan 2016 16:20:33 +0100 Subject: [PATCH 21/25] PushRules settings: Enabled all radio buttons of the table. Each rule is described in the code so that if the server does not have it in its default rules or if the user wants to use actions different from the hs one, the code will create a new rule that will override the hs one. --- .../views/settings/Notifications.js | 350 ++++++++++++++---- 1 file changed, 285 insertions(+), 65 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 6cbff4711..e17601f9a 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -37,6 +37,169 @@ var PushRuleVectorState = { OFF: "off" }; +/** + * The descriptions of rules managed by the Vector UI. + * Each rule is described so that if the server does not have it in its default + * rules or if the user wants to use actions ('PushRuleVectorState') that are + * different from the hs one, the code will create a new rule that will override + * the hs one. + */ +var VectorPushRulesDefinitions = { + + // Messages containing user's display name + // (skip contains_user_name which is too geeky) + "im.vector.rule.contains_display_name": { + hsDefaultRuleId: ".m.rule.contains_display_name", + description: "Messages containing my name", + conditions: [{ + "kind": "contains_display_name" + }], + hsDefaultRuleVectorState: PushRuleVectorState.LOUD, + vectorStateToActions: { + on: [ + "notify" + ], + loud: [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak":"highlight" + } + ] + } + }, + + // Messages just sent to the user in a 1:1 room + "im.vector.rule.room_one_to_one": { + hsDefaultRuleId: ".m.rule.room_one_to_one", + description: "Messages just sent to me", + conditions: [{ + "is": "2", + "kind": "room_member_count" + }], + hsDefaultRuleVectorState: PushRuleVectorState.LOUD, + vectorStateToActions: { + on: [ + "notify" + ], + loud: [ + "notify", + { + "set_tweak": "sound", + "value": "default" + } + ] + } + }, + + // Messages just sent to a group chat room + "im.vector.rule.fallback": { + description: "Messages sent to group", + conditions: [], + hsDefaultRuleId: ".m.rule.fallback", + hsDefaultRuleVectorState: PushRuleVectorState.on, + vectorStateToActions: { + on: [ + "notify" + ], + loud: [ + "notify", + { + "set_tweak": "sound", + "value": "default" + } + ] + } + }, + + // Invitation for the user + "im.vector.rule.invite_for_me": { + hsDefaultRuleId: ".m.rule.invite_for_me", + description: "When I'm invited to a room", + conditions: [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.room.member" + }, + { + "key": "content.membership", + "kind": "event_match", + "pattern": "invite" + }, + { + "key": "state_key", + "kind": "event_match", + "pattern": "" // It is updated at runtime the user id + } + ], + hsDefaultRuleVectorState: PushRuleVectorState.LOUD, + vectorStateToActions: { + on: [ + "notify" + ], + loud: [ + "notify", + { + "set_tweak": "sound", + "value": "default" + } + ] + } + }, + + // When people join or leave a room + "im.vector.rule.member_event": { + hsDefaultRuleId: ".m.rule.member_event", + description: "When people join or leave a room", + conditions: [{ + "pattern": "m.room.member", + "kind": "event_match", + "key": "type" + }], + hsDefaultRuleVectorState: PushRuleVectorState.ON, + vectorStateToActions: { + on: [ + "notify" + ], + loud: [ + "notify", + { + "set_tweak": "sound", + "value": "default" + } + ] + } + }, + + // Incoming call + "im.vector.rule.call": { + hsDefaultRuleId: ".m.rule.call", + description: "Call invitation", + conditions: [{ + "pattern": "m.room.member", + "kind": "event_match", + "key": "type" + }], + hsDefaultRuleVectorState: PushRuleVectorState.LOUD, + vectorStateToActions: { + on: [ + "notify" + ], + loud: [ + "notify", + { + "set_tweak": "sound", + "value": "default" + } + ] + } + }, +}; + module.exports = React.createClass({ displayName: 'Notififications', @@ -61,6 +224,9 @@ module.exports = React.createClass({ }, componentWillMount: function() { + // Finalise the vector definitions + VectorPushRulesDefinitions["im.vector.rule.invite_for_me"].conditions[2].pattern = MatrixClientPeg.get().credentials.userId; + this._refreshFromServer(); }, @@ -188,8 +354,6 @@ module.exports = React.createClass({ }, _setPushRuleVectorState: function(rule, newPushRuleVectorState) { - // For now, we support only enabled/disabled for hs default rules - // Translate ON, LOUD, OFF to one of the 2. if (rule && rule.vectorState !== newPushRuleVectorState) { this.setState({ @@ -197,8 +361,52 @@ module.exports = React.createClass({ }); var self = this; - MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, (newPushRuleVectorState !== PushRuleVectorState.OFF)).done(function() { + var cli = MatrixClientPeg.get(); + var deferreds = []; + var ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId]; + + if (rule.rule) { + if (newPushRuleVectorState === PushRuleVectorState.OFF) { + // Remove the vector rule if any + if (!rule.isHSDefaultRule) { + deferreds.push(cli.deletePushRule('global', rule.rule.kind, rule.rule.rule_id)) + } + + // And disable the hs default rule + deferreds.push(cli.setPushRuleEnabled('global', 'underride', ruleDefinition.hsDefaultRuleId, false)); + } + else { + if (rule.isHSDefaultRule) { + // If the new state corresponds to the hs default rule actions, enable it + // Else create a new rule that will override it + if (newPushRuleVectorState === ruleDefinition.hsDefaultRuleVectorState) { + deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true)); + } + else { + deferreds.push(this._addOverridingVectorPushRule(rule.vectorRuleId, newPushRuleVectorState)); + } + } + else { + // Change the actions of the overriding Vector rule + deferreds.push(this._updatePushRuleActions(rule.rule, ruleDefinition.vectorStateToActions[newPushRuleVectorState])); + } + } + } + else { + // This is a Vector rule which does not exist yet server side + // Create it + deferreds.push(this._addOverridingVectorPushRule(rule.vectorRuleId, newPushRuleVectorState)); + } + + q.all(deferreds).done(function() { self._refreshFromServer(); + }, function(error) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't change settings", + description: error.toString(), + onFinished: self._refreshFromServer + }); }); } }, @@ -374,6 +582,20 @@ module.exports = React.createClass({ return deferred.promise; }, + // Add a push rule server side according to the 'VectorPushRulesDefinitions' spec + _addOverridingVectorPushRule: function(vectorRuleId, vectorState) { + var self = this; + + // Create the rule as predefined + var ruleDefinition = VectorPushRulesDefinitions[vectorRuleId]; + var body = { + conditions: ruleDefinition.conditions, + actions: ruleDefinition.vectorStateToActions[vectorState] + } + + return MatrixClientPeg.get().addPushRule('global', "override", vectorRuleId, body); + }, + _refreshFromServer: function() { var self = this; MatrixClientPeg.get().getPushRules().done(function(rulesets) { @@ -384,7 +606,7 @@ module.exports = React.createClass({ // The master rule (all notifications disabling) '.m.rule.master': 'master', - // The rules displayed by Vector UI + // The default push rules displayed by Vector UI // XXX: .m.rule.contains_user_name is not managed (not a fancy rule for Vector?) '.m.rule.contains_display_name': 'vector', '.m.rule.room_one_to_one': 'vector', @@ -397,6 +619,8 @@ module.exports = React.createClass({ // HS default rules var defaultRules = {master: [], vector: {}, others: []}; + // Push rules defined py Vector to override hs default rules + var vectorOverridingRules = {}; // Content/keyword rules var contentRules = {on: [], on_but_disabled:[], loud: [], loud_but_disabled: [], other: []}; @@ -408,6 +632,14 @@ module.exports = React.createClass({ if (r.rule_id[0] === '.') { if (cat) { if (cat === 'vector') { + // Remove disabled, useless actions + r.actions = r.actions.reduce(function(array, action){ + if (action.value !== false) { + array.push(action); + } + return array; + },[]); + defaultRules.vector[r.rule_id] = r; } else { @@ -418,6 +650,9 @@ module.exports = React.createClass({ defaultRules['others'].push(r); } } + else if (r.rule_id.startsWith('im.vector')) { + vectorOverridingRules[r.rule_id] = r; + } else if (kind === 'content') { switch (self._pushRuleVectorStateKind(r)) { case PushRuleVectorState.ON: @@ -496,19 +731,50 @@ module.exports = React.createClass({ // Build the rules displayed in Vector UI matrix table self.state.vectorPushRules = []; - var rule, vectorState; - // Messages containing user's display name - // (skip contains_user_name which is too geeky) - rule = defaultRules.vector['.m.rule.contains_display_name']; - vectorState = (rule && rule.enabled) ? PushRuleVectorState.LOUD : PushRuleVectorState.OFF; - self.state.vectorPushRules.push({ - "vectorRuleId": "contains_display_name", - "description" : "Messages containing my name", - "rule": rule, - "vectorState": vectorState, - "disabled": PushRuleVectorState.ON - }); + var vectorRuleIds = [ + 'im.vector.rule.contains_display_name', + 'im.vector.rule.room_one_to_one', + 'im.vector.rule.fallback', + 'im.vector.rule.invite_for_me', + 'im.vector.rule.member_event', + 'im.vector.rule.call' + ]; + for (var i in vectorRuleIds) { + var vectorRuleId = vectorRuleIds[i]; + var ruleDefinition = VectorPushRulesDefinitions[vectorRuleId]; + + var rule = vectorOverridingRules[vectorRuleId]; + var isHSDefaultRule = false; + if (!rule) { + // If the rule is not defined, look at the hs default one + rule = defaultRules.vector[ruleDefinition.hsDefaultRuleId]; + isHSDefaultRule = true; + } + + // Translate the rule actions into vector state + var vectorState = PushRuleVectorState.OFF; + if (rule && rule.enabled) { + if (JSON.stringify(rule.actions) === JSON.stringify(ruleDefinition.vectorStateToActions[PushRuleVectorState.ON])) { + vectorState = PushRuleVectorState.ON; + } + else if (JSON.stringify(rule.actions) === JSON.stringify(ruleDefinition.vectorStateToActions[PushRuleVectorState.LOUD])) { + vectorState = PushRuleVectorState.LOUD; + } + else { + console.error("Cannot translate rule actionsinto Vector rule state"); + } + } + + self.state.vectorPushRules.push({ + "vectorRuleId": vectorRuleId, + "description" : ruleDefinition.description, + "rule": rule, + "vectorState": vectorState, + "isHSDefaultRule": isHSDefaultRule, + "hsDefaultRule": defaultRules.vector[ruleDefinition.hsDefaultRuleId] + }); + } // Messages containing keywords // For Vector UI, this is a single global push rule but translated in Matrix, @@ -518,50 +784,6 @@ module.exports = React.createClass({ "description" : (Messages containing keywords), "vectorState": self.state.vectorContentRules.vectorState }); - - // Messages just sent to the user - rule = defaultRules.vector['.m.rule.room_one_to_one']; - vectorState = (rule && rule.enabled) ? PushRuleVectorState.LOUD : PushRuleVectorState.OFF; - self.state.vectorPushRules.push({ - "vectorRuleId": "room_one_to_one", - "description" : "Messages just sent to me", - "rule": rule, - "vectorState": vectorState, - "disabled": PushRuleVectorState.ON - }); - - // Invitation for the user - rule = defaultRules.vector['.m.rule.invite_for_me']; - vectorState = (rule && rule.enabled) ? PushRuleVectorState.LOUD : PushRuleVectorState.OFF; - self.state.vectorPushRules.push({ - "vectorRuleId": "invite_for_me", - "description" : "When I'm invited to a room", - "rule": rule, - "vectorState": vectorState, - "disabled": PushRuleVectorState.ON - }); - - // When people join or leave a room - rule = defaultRules.vector['.m.rule.member_event']; - vectorState = (rule && rule.enabled) ? PushRuleVectorState.ON : PushRuleVectorState.OFF; - self.state.vectorPushRules.push({ - "vectorRuleId": "member_event", - "description" : "When people join or leave a room", - "rule": rule, - "vectorState": vectorState, - "disabled": PushRuleVectorState.LOUD - }); - - // Incoming call - rule = defaultRules.vector['.m.rule.call']; - vectorState = (rule && rule.enabled) ? PushRuleVectorState.LOUD : PushRuleVectorState.OFF; - self.state.vectorPushRules.push({ - "vectorRuleId": "call", - "description" : "Call invitation", - "rule": rule, - "vectorState": vectorState, - "disabled": PushRuleVectorState.ON - }); // Build the rules not managed by Vector UI var otherRulesDescriptions = { @@ -596,6 +818,7 @@ module.exports = React.createClass({ cli.deletePushRule('global', rule.kind, rule.rule_id).done(function() { cli.addPushRule('global', rule.kind, rule.rule_id, { + conditions: rule.conditions, actions: actions, pattern: rule.pattern }).done(function() { @@ -621,7 +844,7 @@ module.exports = React.createClass({ return deferred.promise; }, - renderNotifRulesTableRow: function(title, className, pushRuleVectorState, disabled) { + renderNotifRulesTableRow: function(title, className, pushRuleVectorState) { return ( @@ -632,7 +855,6 @@ module.exports = React.createClass({ @@ -640,7 +862,6 @@ module.exports = React.createClass({ @@ -648,7 +869,6 @@ module.exports = React.createClass({ @@ -659,7 +879,7 @@ module.exports = React.createClass({ var rows = []; for (var i in this.state.vectorPushRules) { var rule = this.state.vectorPushRules[i]; - rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState, rule.disabled)); + rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState)); } return rows; }, From ae14210763cb98ed3f8f2fa3b6864329b94f8fd9 Mon Sep 17 00:00:00 2001 From: manuroe Date: Mon, 18 Jan 2016 16:31:18 +0100 Subject: [PATCH 22/25] PushRules settings: Put keywords in the right position --- .../views/settings/Notifications.js | 78 ++++++++++--------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index e17601f9a..5fa46cc13 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -249,7 +249,7 @@ module.exports = React.createClass({ var vectorRuleId = event.target.className.split("-")[0]; var newPushRuleVectorState = event.target.className.split("-")[1]; - if ("keywords" === vectorRuleId) { + if ("_keywords" === vectorRuleId) { this._setKeywordsPushRuleVectorState(newPushRuleVectorState) } else { @@ -729,11 +729,12 @@ module.exports = React.createClass({ self.state.masterPushRule = defaultRules.master[0]; } - // Build the rules displayed in Vector UI matrix table + // Build the rules displayed in the Vector UI matrix table self.state.vectorPushRules = []; var vectorRuleIds = [ 'im.vector.rule.contains_display_name', + '_keywords', 'im.vector.rule.room_one_to_one', 'im.vector.rule.fallback', 'im.vector.rule.invite_for_me', @@ -744,46 +745,49 @@ module.exports = React.createClass({ var vectorRuleId = vectorRuleIds[i]; var ruleDefinition = VectorPushRulesDefinitions[vectorRuleId]; - var rule = vectorOverridingRules[vectorRuleId]; - var isHSDefaultRule = false; - if (!rule) { - // If the rule is not defined, look at the hs default one - rule = defaultRules.vector[ruleDefinition.hsDefaultRuleId]; - isHSDefaultRule = true; + 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" : (Messages containing keywords), + "vectorState": self.state.vectorContentRules.vectorState + }); } + else { + var rule = vectorOverridingRules[vectorRuleId]; + var isHSDefaultRule = false; + if (!rule) { + // If the rule is not defined, look at the hs default one + rule = defaultRules.vector[ruleDefinition.hsDefaultRuleId]; + isHSDefaultRule = true; + } - // Translate the rule actions into vector state - var vectorState = PushRuleVectorState.OFF; - if (rule && rule.enabled) { - if (JSON.stringify(rule.actions) === JSON.stringify(ruleDefinition.vectorStateToActions[PushRuleVectorState.ON])) { - vectorState = PushRuleVectorState.ON; - } - else if (JSON.stringify(rule.actions) === JSON.stringify(ruleDefinition.vectorStateToActions[PushRuleVectorState.LOUD])) { - vectorState = PushRuleVectorState.LOUD; - } - else { - console.error("Cannot translate rule actionsinto Vector rule state"); + // Translate the rule actions into vector state + var vectorState = PushRuleVectorState.OFF; + if (rule && rule.enabled) { + if (JSON.stringify(rule.actions) === JSON.stringify(ruleDefinition.vectorStateToActions[PushRuleVectorState.ON])) { + vectorState = PushRuleVectorState.ON; + } + else if (JSON.stringify(rule.actions) === JSON.stringify(ruleDefinition.vectorStateToActions[PushRuleVectorState.LOUD])) { + vectorState = PushRuleVectorState.LOUD; + } + else { + console.error("Cannot translate rule actionsinto Vector rule state"); + } } + + self.state.vectorPushRules.push({ + "vectorRuleId": vectorRuleId, + "description" : ruleDefinition.description, + "rule": rule, + "vectorState": vectorState, + "isHSDefaultRule": isHSDefaultRule, + "hsDefaultRule": defaultRules.vector[ruleDefinition.hsDefaultRuleId] + }); } - - self.state.vectorPushRules.push({ - "vectorRuleId": vectorRuleId, - "description" : ruleDefinition.description, - "rule": rule, - "vectorState": vectorState, - "isHSDefaultRule": isHSDefaultRule, - "hsDefaultRule": defaultRules.vector[ruleDefinition.hsDefaultRuleId] - }); } - - // Messages containing keywords - // 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" : (Messages containing keywords), - "vectorState": self.state.vectorContentRules.vectorState - }); // Build the rules not managed by Vector UI var otherRulesDescriptions = { From bdcf6839429efc23a996a62612b982e8448db32b Mon Sep 17 00:00:00 2001 From: manuroe Date: Mon, 18 Jan 2016 16:41:48 +0100 Subject: [PATCH 23/25] PushRules settings: Create a dedicated rule for "Messages sent to group". The default fallback rule cannot be used because it matches with too much events. --- src/components/views/settings/Notifications.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 5fa46cc13..175462239 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -96,10 +96,13 @@ var VectorPushRulesDefinitions = { }, // Messages just sent to a group chat room - "im.vector.rule.fallback": { + "im.vector.rule.room_group": { description: "Messages sent to group", - conditions: [], - hsDefaultRuleId: ".m.rule.fallback", + conditions: [{ + "is": ">2", + "kind": "room_member_count" + }], + hsDefaultRuleId: undefined, // Matrix does not define a default hs push rule for group hsDefaultRuleVectorState: PushRuleVectorState.on, vectorStateToActions: { on: [ @@ -736,7 +739,7 @@ module.exports = React.createClass({ 'im.vector.rule.contains_display_name', '_keywords', 'im.vector.rule.room_one_to_one', - 'im.vector.rule.fallback', + 'im.vector.rule.room_group', 'im.vector.rule.invite_for_me', 'im.vector.rule.member_event', 'im.vector.rule.call' From 7c0fffa79bce45662bda198cc2bdd0484284dc66 Mon Sep 17 00:00:00 2001 From: manuroe Date: Mon, 18 Jan 2016 18:07:33 +0100 Subject: [PATCH 24/25] PushRules settings: Applied easy review remarks --- src/components/views/settings/Notifications.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 175462239..1e99096ad 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2016 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -195,7 +195,7 @@ var VectorPushRulesDefinitions = { loud: [ "notify", { - "set_tweak": "sound", + "set_tweak": "ring", "value": "default" } ] From 6182c983abb98f6ce7fba7b2b72045da12daa250 Mon Sep 17 00:00:00 2001 From: manuroe Date: Mon, 18 Jan 2016 18:24:53 +0100 Subject: [PATCH 25/25] PushRules settings: Applied review remarks (2/2) --- src/components/views/settings/Notifications.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 1e99096ad..068b0ddf7 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -331,9 +331,9 @@ module.exports = React.createClass({ } }, - // Determine whether a rule is in the PushRuleVectorState.ON category or in PushRuleVectorState.LOUD - // regardless of its enabled state. - _pushRuleVectorStateKind: function(rule) { + // Determine whether a content rule is in the PushRuleVectorState.ON category or in PushRuleVectorState.LOUD + // regardless of its enabled state. Returns undefined if it does not match these categories. + _contentRuleVectorStateKind: function(rule) { var stateKind; // Count tweaks to determine if it is a ON or LOUD rule @@ -533,7 +533,7 @@ module.exports = React.createClass({ // when creating the new rule. // Thus, this new rule will join the 'vectorContentRules' set. if (self.state.vectorContentRules.rules.length) { - pushRuleVectorStateKind = self._pushRuleVectorStateKind(self.state.vectorContentRules.rules[0]); + pushRuleVectorStateKind = self._contentRuleVectorStateKind(self.state.vectorContentRules.rules[0]); } else { // ON is default @@ -657,7 +657,7 @@ module.exports = React.createClass({ vectorOverridingRules[r.rule_id] = r; } else if (kind === 'content') { - switch (self._pushRuleVectorStateKind(r)) { + switch (self._contentRuleVectorStateKind(r)) { case PushRuleVectorState.ON: if (r.enabled) { contentRules.on.push(r);