Update the actions of default rules instead of overriding.

The Matrix CS API, and synapse now supports setting the actions for default
rules. Doing that makes managing the rules much simpler from a vector
persepctive since the ON/LOUD/OFF toggle buttons can be implemented by
setting the actions and enabling/disabling the default rules rather than
overidding them.

Overriding the default rules was difficult because it was not possible
to intermingle the evaluation of user-specified rules with the default
rules. So even though you could add a rule with the same conditions as a
default rule, it would evaluate before *all* the other default rules.

Also creating new rules under a im.vector namespace creates challenges
if we want vector to cooperate with other matrix clients that want to
provide a similar set of toggle switches for the push rules.
This commit is contained in:
Mark Haines 2016-02-26 20:51:16 +00:00
parent c3b819b4da
commit 731d94eea4
1 changed files with 99 additions and 290 deletions

View File

@ -37,6 +37,40 @@ var PushRuleVectorState = {
OFF: "off" OFF: "off"
}; };
var ACTION_NOTIFY = [ "notify" ];
var ACTION_NOTIFY_DEFAULT_SOUND = [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
];
var ACTION_NOTIFY_RING_SOUND = [
"notify",
{
"set_tweak": "sound",
"value": "ring"
}
];
var ACTION_HIGHLIGHT_DEFAULT_SOUND = [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak":"highlight"
}
];
var ACTION_DONT_NOTIFY = [ "dont_notify" ];
var ACTION_DISABLED = null;
/** /**
* The descriptions of rules managed by the Vector UI. * 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 * Each rule is described so that if the server does not have it in its default
@ -48,225 +82,71 @@ var VectorPushRulesDefinitions = {
// Messages containing user's display name // Messages containing user's display name
// (skip contains_user_name which is too geeky) // (skip contains_user_name which is too geeky)
"im.vector.rule.contains_display_name": { ".m.rule.contains_display_name": {
kind: "underride", kind: "underride",
hsDefaultRuleId: ".m.rule.contains_display_name",
description: "Messages containing my name", description: "Messages containing my name",
conditions: [{ vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
"kind": "contains_display_name" on: ACTION_NOTIFY,
}], loud: ACTION_HIGHLIGHT_DEFAULT,
vectorStateToActions: { // The actions for each vector state off: ACTION_DISABLED
on: [ }
"notify"
],
loud: [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak":"highlight"
}
]
},
vectorStateToHsDefaultRuleEnabled: { // If it exists, the hs default push rule enabled expected value for each vector state
on: undefined, // ON (and its actions) does not corresponds to the default hs push rule, so NA
loud: true, // LOUD corresponds to the default rule when its enabled value is true
off: false // OFF corresponds to the default rule when its enabled value is false
},
}, },
// Messages just sent to the user in a 1:1 room // Messages just sent to the user in a 1:1 room
"im.vector.rule.room_one_to_one": { ".m.rule.room_one_to_one": {
kind: "underride", kind: "underride",
hsDefaultRuleId: ".m.rule.room_one_to_one",
description: "Messages in one-to-one chats", description: "Messages in one-to-one chats",
conditions: [{
"is": "2",
"kind": "room_member_count"
}],
vectorStateToActions: { vectorStateToActions: {
on: [ on: ACTION_NOTIFY,
"notify" loud: ACTION_NOTIFY_DEFAULT_SOUND,
], off: ACTION_DONT_NOTIFY
loud: [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
],
off: [
"dont_notify"
]
},
vectorStateToHsDefaultRuleEnabled: {
on: undefined,
loud: true,
off: undefined
} }
}, },
// Messages just sent to a group chat room // Messages just sent to a group chat room
// 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined
// By opposition, all other room messages are from group chat rooms. // By opposition, all other room messages are from group chat rooms.
"im.vector.rule.room_message": { ".m.rule.room_message": {
kind: "underride", kind: "underride",
description: "Messages in group chats", description: "Messages in group chats",
conditions: [{
"pattern": "m.room.message",
"kind": "event_match",
"key": "type"
}],
hsDefaultRuleId: ".m.rule.message",
vectorStateToActions: { vectorStateToActions: {
on: [ on: ACTION_NOTIFY,
"notify" loud: ACTION_NOTIFY_DEFAULT_SOUND,
], off: ACTION_DONT_NOTIFY
loud: [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
],
off: [
"dont_notify"
]
},
vectorStateToHsDefaultRuleEnabled: {
on: true,
loud: undefined,
off: undefined
} }
}, },
// Invitation for the user // Invitation for the user
"im.vector.rule.invite_for_me": { ".m.rule.invite_for_me": {
kind: "underride", kind: "underride",
hsDefaultRuleId: ".m.rule.invite_for_me",
description: "When I'm invited to a room", 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
}
],
vectorStateToActions: { vectorStateToActions: {
on: [ on: ACTION_NOTIFY,
"notify" loud: ACTION_NOTIFY_DEFAULT_SOUND,
], off: ACTION_DISABLED
loud: [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
]
},
vectorStateToHsDefaultRuleEnabled: {
on: undefined,
loud: true,
off: false
} }
}, },
// 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"
}],
vectorStateToActions: {
on: [
"notify"
],
loud: [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
]
},
vectorStateToHsDefaultRuleEnabled: {
on: true,
loud: undefined,
off: false
}
},*/
// Incoming call // Incoming call
"im.vector.rule.call": { ".m.rule.call": {
kind: "underride", kind: "underride",
hsDefaultRuleId: ".m.rule.call",
description: "Call invitation", description: "Call invitation",
conditions: [{
"pattern": "m.room.member",
"kind": "event_match",
"key": "type"
}],
vectorStateToActions: { vectorStateToActions: {
on: [ on: ACTION_NOTIFY,
"notify" loud: ACTION_NOTIFY_RING_SOUND,
], off: ACTION_DISABLED
loud: [
"notify",
{
"set_tweak": "sound",
"value": "ring"
}
],
},
vectorStateToHsDefaultRuleEnabled: {
on: undefined,
loud: true,
off: false
} }
}, },
// Notifications from bots // Notifications from bots
"im.vector.rule.notices": { ".m.rule.suppress_notices": {
kind: "override", kind: "override",
hsDefaultRuleId: ".m.rule.suppress_notices",
description: "Messages sent by bot", description: "Messages sent by bot",
conditions: [{
"kind": "event_match",
"key": "content.msgtype",
"pattern": "m.notice"
}],
vectorStateToActions: { vectorStateToActions: {
on: undefined, // ON for vector UI means that the .m.rule.suppress_notices rule is disabled. // .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI
loud: [ on: ACTION_DISABLED,
"notify", loud: ACTION_NOTIFY_DEFAULT_SOUND,
{ off: ACTION_DONT_NOTIFY,
"set_tweak": "sound",
"value": "ring"
}
],
off: [
"dont_notify"
]
},
vectorStateToHsDefaultRuleEnabled: {
on: false, // .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI
loud: undefined,
off: true
} }
} }
}; };
@ -295,9 +175,6 @@ module.exports = React.createClass({
}, },
componentWillMount: function() { componentWillMount: function() {
// Finalise the vector definitions
VectorPushRulesDefinitions["im.vector.rule.invite_for_me"].conditions[2].pattern = MatrixClientPeg.get().credentials.userId;
this._refreshFromServer(); this._refreshFromServer();
}, },
@ -389,13 +266,10 @@ module.exports = React.createClass({
_actionsFor: function(pushRuleVectorState) { _actionsFor: function(pushRuleVectorState) {
if (pushRuleVectorState === PushRuleVectorState.ON) { if (pushRuleVectorState === PushRuleVectorState.ON) {
return ['notify']; return ACTIONS_NOTIFY;
} }
else if (pushRuleVectorState === PushRuleVectorState.LOUD) { else if (pushRuleVectorState === PushRuleVectorState.LOUD) {
return ['notify', return ACTIONS_HIGHLIGHT_DEFAULT_SOUND;
{'set_tweak': 'sound', 'value': 'default'},
{'set_tweak': 'highlight', 'value': 'true'}
];;
} }
}, },
@ -437,35 +311,17 @@ module.exports = React.createClass({
var ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId]; var ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId];
if (rule.rule) { if (rule.rule) {
if (undefined !== ruleDefinition.vectorStateToHsDefaultRuleEnabled[newPushRuleVectorState] && rule.hsDefaultRule) { var actions = ruleDefinition.vectorStateToActions(newPushRuleVectorState);
// The new state corresponds to the default hs rule
// Enable or disable it according to the rule definition
deferreds.push(cli.setPushRuleEnabled('global', rule.hsDefaultRule.kind, ruleDefinition.hsDefaultRuleId,
ruleDefinition.vectorStateToHsDefaultRuleEnabled[newPushRuleVectorState]));
// Remove the vector rule if any if (actions === ACTIONS_DISABLED) {
if (!rule.isHSDefaultRule) { // The new state corresponds to disabling the rule.
deferreds.push(cli.deletePushRule('global', rule.rule.kind, rule.rule.rule_id)) deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false));
}
} }
else { else {
// The new state (and its implied actions) does not correspond to a default hs rule // The new state corresponds to enabling the rule and setting specific actions
// or the HS does not expose this default rule. deferreds.push(this._updatePushRuleActions(rule.rule, actions, true));
if (rule.isHSDefaultRule) {
// Create a new rule that will override the default one
deferreds.push(this._addOverridingVectorPushRule(rule.vectorRuleId, newPushRuleVectorState));
}
else {
// Change the actions of the existing 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() { q.all(deferreds).done(function() {
self._refreshFromServer(); self._refreshFromServer();
@ -690,8 +546,6 @@ module.exports = React.createClass({
// HS default rules // HS default rules
var defaultRules = {master: [], vector: {}, others: []}; var defaultRules = {master: [], vector: {}, others: []};
// Push rules defined py Vector to override hs default rules
var vectorOverridingRules = {};
// Content/keyword rules // Content/keyword rules
var contentRules = {on: [], on_but_disabled:[], loud: [], loud_but_disabled: [], other: []}; var contentRules = {on: [], on_but_disabled:[], loud: [], loud_but_disabled: [], other: []};
@ -701,29 +555,16 @@ module.exports = React.createClass({
var cat = rule_categories[r.rule_id]; var cat = rule_categories[r.rule_id];
r.kind = kind; r.kind = kind;
if (r.rule_id[0] === '.') { if (r.rule_id[0] === '.') {
if (cat) { if (cat === 'vector') {
if (cat === 'vector') { defaultRules.vector[r.rule_id] = r;
// Remove disabled, useless actions }
r.actions = r.actions.reduce(function(array, action){ else if (cat === 'master') {
if (action.value !== false) { defaultRules.master.push(r);
array.push(action);
}
return array;
},[]);
defaultRules.vector[r.rule_id] = r;
}
else {
defaultRules[cat].push(r);
}
} }
else { else {
defaultRules['others'].push(r); defaultRules['others'].push(r);
} }
} }
else if (r.rule_id.startsWith('im.vector')) {
vectorOverridingRules[r.rule_id] = r;
}
else if (kind === 'content') { else if (kind === 'content') {
switch (self._contentRuleVectorStateKind(r)) { switch (self._contentRuleVectorStateKind(r)) {
case PushRuleVectorState.ON: case PushRuleVectorState.ON:
@ -804,14 +645,14 @@ module.exports = React.createClass({
self.state.vectorPushRules = []; self.state.vectorPushRules = [];
var vectorRuleIds = [ var vectorRuleIds = [
'im.vector.rule.contains_display_name', '.m.rule.contains_display_name',
'_keywords', '_keywords',
'im.vector.rule.room_one_to_one', '.m.rule.room_one_to_one',
'im.vector.rule.room_message', '.m.rule.room_message',
'im.vector.rule.invite_for_me', '.m.rule.invite_for_me',
//'im.vector.rule.member_event', //'im.vector.rule.member_event',
'im.vector.rule.call', '.m.rule.call',
'im.vector.rule.notices' '.m.rule.notices'
]; ];
for (var i in vectorRuleIds) { for (var i in vectorRuleIds) {
var vectorRuleId = vectorRuleIds[i]; var vectorRuleId = vectorRuleIds[i];
@ -828,13 +669,7 @@ module.exports = React.createClass({
}); });
} }
else { else {
var rule = vectorOverridingRules[vectorRuleId]; var rule = defaultRules.vector[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 and its enabled value into vector state // Translate the rule actions and its enabled value into vector state
var vectorState; var vectorState;
@ -843,9 +678,9 @@ module.exports = React.createClass({
var state = PushRuleVectorState[stateKey]; var state = PushRuleVectorState[stateKey];
var vectorStateToActions = ruleDefinition.vectorStateToActions[state]; var vectorStateToActions = ruleDefinition.vectorStateToActions[state];
if (!vectorStateToActions) { if (vectorStateToActions === ACTIONS_DISABLED) {
// No defined actions means that this vector state expects a disabled default hs rule // No defined actions means that this vector state expects a disabled default hs rule
if (isHSDefaultRule && rule.enabled === ruleDefinition.vectorStateToHsDefaultRuleEnabled[state]) { if (rule.enabled === false) {
vectorState = state; vectorState = state;
break; break;
} }
@ -853,14 +688,8 @@ module.exports = React.createClass({
else { else {
// The actions must match to the ones expected by vector state // The actions must match to the ones expected by vector state
if (JSON.stringify(rule.actions) === JSON.stringify(vectorStateToActions)) { if (JSON.stringify(rule.actions) === JSON.stringify(vectorStateToActions)) {
if (isHSDefaultRule) { // And the rule must be enabled.
// In the case of a default hs push rule, the enabled value must also match if (rule.enabled === true) {
if (rule.enabled === ruleDefinition.vectorStateToHsDefaultRuleEnabled[state]) {
vectorState = state;
break;
}
}
else {
vectorState = state; vectorState = state;
break; break;
} }
@ -882,8 +711,6 @@ module.exports = React.createClass({
"description" : ruleDefinition.description, "description" : ruleDefinition.description,
"rule": rule, "rule": rule,
"vectorState": vectorState, "vectorState": vectorState,
"isHSDefaultRule": isHSDefaultRule,
"hsDefaultRule": defaultRules.vector[ruleDefinition.hsDefaultRuleId]
}); });
} }
} }
@ -913,37 +740,19 @@ module.exports = React.createClass({
}, },
_updatePushRuleActions: function(rule, actions, enabled) { _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(); var cli = MatrixClientPeg.get();
var deferred = q.defer(); var deferred = q.defer();
cli.deletePushRule('global', rule.kind, rule.rule_id).done(function() { return cli.setPushRuleActions(
cli.addPushRule('global', rule.kind, rule.rule_id, { 'global', rule.kind, rule.rule_id, actions
conditions: rule.conditions, ).then( function() {
actions: actions, // Then, if requested, enabled or disabled the rule
pattern: rule.pattern if (undefined != enabled) {
}).done(function() { return cli.setPushRuleEnabled(
'global', rule.kind, rule.rule_id, enabled
// 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);
});
}, function(err) {
deferred.reject(err);
}); });
return deferred.promise;
}, },
renderNotifRulesTableRow: function(title, className, pushRuleVectorState) { renderNotifRulesTableRow: function(title, className, pushRuleVectorState) {