Merge pull request #1037 from vector-im/markjh/change_push_actions

Update the actions of default rules instead of overriding.
This commit is contained in:
Mark Haines 2016-03-15 18:10:04 +00:00
commit eb01cb9cba

View file

@ -37,240 +37,186 @@ var PushRuleVectorState = {
OFF: "off" OFF: "off"
}; };
// Encodes a dictionary of {
// "notify": true/false,
// "sound": string or undefined,
// "highlight: true/false,
// }
// to a list of push actions.
function encodeActions(action) {
var notify = action.notify;
var sound = action.sound;
var highlight = action.highlight;
if (notify) {
var actions = ["notify"];
if (sound) {
actions.push({"set_tweak": "sound", "value": sound});
}
if (highlight) {
actions.push({"set_tweak": "highlight"});
} else {
actions.push({"set_tweak": "highlight", "value": false});
}
return actions;
} else {
return ["dont_notify"];
}
}
// Decode a list of actions to a dictionary of {
// "notify": true/false,
// "sound": string or undefined,
// "highlight: true/false,
// }
// If the actions couldn't be decoded then returns null.
function decodeActions(actions) {
var notify = false;
var sound = null;
var highlight = false;
for (var i = 0; i < actions.length; ++i) {
var action = actions[i];
if (action === "notify") {
notify = true;
} else if (action === "dont_notify") {
notify = false;
} else if (typeof action === 'object') {
if (action.set_tweak === "sound") {
sound = action.value
} else if (action.set_tweak === "highlight") {
highlight = action.value;
} else {
// We don't understand this kind of tweak, so give up.
return null;
}
} else {
// We don't understand this kind of action, so give up.
return null;
}
}
if (highlight === undefined) {
// If a highlight tweak is missing a value then it defaults to true.
highlight = true;
}
var result = {notify: notify, highlight: highlight};
if (sound !== null) {
result.sound = sound;
}
return result;
}
var ACTION_NOTIFY = encodeActions({notify: true});
var ACTION_NOTIFY_DEFAULT_SOUND = encodeActions({notify: true, sound: "default"});
var ACTION_NOTIFY_RING_SOUND = encodeActions({notify: true, sound: "ring"});
var ACTION_HIGHLIGHT_DEFAULT_SOUND = encodeActions({notify: true, sound: "default", highlight: true});
var ACTION_DONT_NOTIFY = encodeActions({notify: false});
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
* 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 = { 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_SOUND,
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.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
} }
} }
}; };
/**
* Rules that Vector used to set in order to override the actions of default rules.
* These are used to port peoples existing overrides to match the current API.
* These can be removed and forgotten once everyone has moved to the new client.
*/
var LEGACY_RULES = {
"im.vector.rule.contains_display_name": ".m.rule.contains_display_name",
"im.vector.rule.room_one_to_one": ".m.rule.room_one_to_one",
"im.vector.rule.room_message": ".m.rule.message",
"im.vector.rule.invite_for_me": ".m.rule.invite_for_me",
"im.vector.rule.call": ".m.rule.call",
"im.vector.rule.notices": ".m.rule.suppress_notices"
};
function portLegacyActions(actions) {
var decoded = decodeActions(actions);
if (decoded !== null) {
return encodeActions(decoded);
} else {
// We don't recognise one of the actions here, so we don't try to
// canonicalise them.
return actions;
}
}
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'Notififications', displayName: 'Notififications',
@ -295,9 +241,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 +332,10 @@ module.exports = React.createClass({
_actionsFor: function(pushRuleVectorState) { _actionsFor: function(pushRuleVectorState) {
if (pushRuleVectorState === PushRuleVectorState.ON) { if (pushRuleVectorState === PushRuleVectorState.ON) {
return ['notify']; return ACTION_NOTIFY;
} }
else if (pushRuleVectorState === PushRuleVectorState.LOUD) { else if (pushRuleVectorState === PushRuleVectorState.LOUD) {
return ['notify', return ACTION_HIGHLIGHT_DEFAULT_SOUND;
{'set_tweak': 'sound', 'value': 'default'},
{'set_tweak': 'highlight', 'value': 'true'}
];;
} }
}, },
@ -437,36 +377,18 @@ 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 === ACTION_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();
}, function(error) { }, function(error) {
@ -651,23 +573,45 @@ module.exports = React.createClass({
return deferred.promise; return deferred.promise;
}, },
// Add a push rule server side according to the 'VectorPushRulesDefinitions' spec // Check if any legacy im.vector rules need to be ported to the new API
_addOverridingVectorPushRule: function(vectorRuleId, vectorState) { // for overriding the actions of default rules.
_portRulesToNewAPI: function(rulesets) {
var self = this; var self = this;
var needsUpdate = [];
var cli = MatrixClientPeg.get();
// Create the rule as predefined for (var kind in rulesets.global) {
var ruleDefinition = VectorPushRulesDefinitions[vectorRuleId]; var ruleset = rulesets.global[kind];
var body = { for (var i = 0; i < ruleset.length; ++i) {
conditions: ruleDefinition.conditions, var rule = ruleset[i];
actions: ruleDefinition.vectorStateToActions[vectorState] if (rule.rule_id in LEGACY_RULES) {
console.log("Porting legacy rule", rule);
needsUpdate.push( function(kind, rule) {
return cli.setPushRuleActions(
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions)
).then( function() {
return cli.deletePushRule('global', kind, rule.rule_id);
})
}(kind, rule));
}
}
}
if (needsUpdate.length > 0) {
// If some of the rules need to be ported then wait for the porting
// to happen and then fetch the rules again.
return q.allSettled(needsUpdate).then( function() {
return cli.getPushRules();
});
} else {
// Otherwise return the rules that we already have.
return rulesets;
} }
return MatrixClientPeg.get().addPushRule('global', ruleDefinition.kind, vectorRuleId, body);
}, },
_refreshFromServer: function() { _refreshFromServer: function() {
var self = this; var self = this;
MatrixClientPeg.get().getPushRules().done(function(rulesets) { MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).done(function(rulesets) {
MatrixClientPeg.get().pushRules = rulesets; MatrixClientPeg.get().pushRules = rulesets;
// Get homeserver default rules and triage them by categories // Get homeserver default rules and triage them by categories
@ -690,8 +634,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: []};
@ -700,30 +642,18 @@ module.exports = React.createClass({
var r = rulesets.global[kind][i]; var r = rulesets.global[kind][i];
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 (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; if (r.rule_id[0] === '.') {
} if (cat === 'vector') {
else { defaultRules.vector[r.rule_id] = r;
defaultRules[cat].push(r); }
} else if (cat === 'master') {
defaultRules.master.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 +734,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.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.suppress_notices'
]; ];
for (var i in vectorRuleIds) { for (var i in vectorRuleIds) {
var vectorRuleId = vectorRuleIds[i]; var vectorRuleId = vectorRuleIds[i];
@ -828,13 +758,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 +767,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 === ACTION_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 +777,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;
} }
@ -870,7 +788,7 @@ module.exports = React.createClass({
if (!vectorState) { if (!vectorState) {
console.error("Cannot translate rule actions into Vector rule state. Rule: " + rule); console.error("Cannot translate rule actions into Vector rule state. Rule: " + rule);
vectorState = PushRuleVectorState.OFF; vectorState = PushRuleVectorState.OFF;
} }
} }
else { else {
@ -882,9 +800,7 @@ 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,39 +829,21 @@ 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() {
cli.addPushRule('global', rule.kind, rule.rule_id, {
conditions: rule.conditions,
actions: actions,
pattern: rule.pattern
}).done(function() {
// Then, if requested, enabled or disabled the rule return cli.setPushRuleActions(
if (undefined != enabled) { 'global', rule.kind, rule.rule_id, actions
cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled).done(function() { ).then( function() {
deferred.resolve(); // Then, if requested, enabled or disabled the rule
}, function(err) { if (undefined != enabled) {
deferred.reject(err); return cli.setPushRuleEnabled(
}); 'global', rule.kind, rule.rule_id, enabled
} );
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) {
return ( return (
<tr key = {className}> <tr key = {className}>
@ -1019,7 +917,7 @@ module.exports = React.createClass({
// When enabled, the master rule inhibits all existing rules // When enabled, the master rule inhibits all existing rules
// So do not show all notification settings // So do not show all notification settings
if (this.state.masterPushRule.enabled) { if (this.state.masterPushRule && this.state.masterPushRule.enabled) {
return ( return (
<div> <div>
{masterPushRuleDiv} {masterPushRuleDiv}