Improve upon the previous implementation.

If the resource with the highest priority goes offline or becomes unavailable,
then the chat status of the contact must fall back to that of the resource with
the next highest priority.

In your example from #785, if the resource with priority 1 goes offline or
becomes unavailable, then in your implementation the chat status would stay at
online, although it must actually go to xa.

The solution is to update the resources attribute on the contact to not just be
an array or strings, but instead to be a map of resources to priorities and
statuses and to use that data structure.
This commit is contained in:
JC Brand 2017-02-20 22:27:34 +01:00
parent 15d2640c43
commit 789654d54e
3 changed files with 79 additions and 67 deletions

View File

@ -16,7 +16,6 @@
* Templates are no longer stored as attributes on the `_converse` object. * Templates are no longer stored as attributes on the `_converse` object.
If you need a particular template, use `require` to load it. If you need a particular template, use `require` to load it.
- Add Presence Priority Handling [w3host]
- The chat room `description` is now shown in the heading, not the `subject`. - The chat room `description` is now shown in the heading, not the `subject`.
[jcbrand] [jcbrand]
- Chat room features are shown in the sidebar. [jcbrand] - Chat room features are shown in the sidebar. [jcbrand]
@ -53,6 +52,8 @@
- #366 Show the chat room occupant's JID in the tooltip (if you're allowed to see it). [jcbrand] - #366 Show the chat room occupant's JID in the tooltip (if you're allowed to see it). [jcbrand]
- #694 The `notification_option` wasn't being used consistently. [jcbrand] - #694 The `notification_option` wasn't being used consistently. [jcbrand]
- #770 Allow setting contact attrs on chats.open [Ape] - #770 Allow setting contact attrs on chats.open [Ape]
- #785 Add presence priority handling [w3host, jcbrand]
## 2.0.6 (2017-02-13) ## 2.0.6 (2017-02-13)
- Escape user-generated input to prevent JS-injection attacks. (Thanks to SamWhited) [jcbrand] - Escape user-generated input to prevent JS-injection attacks. (Thanks to SamWhited) [jcbrand]

View File

@ -1,3 +1,4 @@
/*jshint sub:true*/
(function (root, factory) { (function (root, factory) {
define([ define([
"jquery", "jquery",
@ -17,6 +18,7 @@
test_utils.openControlBox(); test_utils.openControlBox();
test_utils.createContacts(_converse, 'current'); // Create some contacts so that we can test positioning test_utils.createContacts(_converse, 'current'); // Create some contacts so that we can test positioning
var contact_jid = mock.cur_names[8].replace(/ /g,'.').toLowerCase() + '@localhost'; var contact_jid = mock.cur_names[8].replace(/ /g,'.').toLowerCase() + '@localhost';
var contact = _converse.roster.get(contact_jid);
var stanza = $( var stanza = $(
'<presence xmlns="jabber:client"'+ '<presence xmlns="jabber:client"'+
' to="dummy@localhost/converse.js-21770972"'+ ' to="dummy@localhost/converse.js-21770972"'+
@ -30,7 +32,10 @@
' <delay xmlns="urn:xmpp:delay" stamp="2017-02-15T20:26:05Z" from="jabbim.hu"/>'+ ' <delay xmlns="urn:xmpp:delay" stamp="2017-02-15T20:26:05Z" from="jabbim.hu"/>'+
'</presence>'); '</presence>');
_converse.connection._dataRecv(test_utils.createRequest(stanza[0])); _converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('online'); expect(contact.get('chat_status')).toBe('online');
expect(_.keys(contact.get('resources')).length).toBe(1);
expect(contact.get('resources')['c71f218b-0797-4732-8a88-b42cb1d8557a']['priority']).toBe(1);
expect(contact.get('resources')['c71f218b-0797-4732-8a88-b42cb1d8557a']['status']).toBe('online');
stanza = $( stanza = $(
'<presence xmlns="jabber:client"'+ '<presence xmlns="jabber:client"'+
@ -46,6 +51,33 @@
'</presence>'); '</presence>');
_converse.connection._dataRecv(test_utils.createRequest(stanza[0])); _converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('online'); expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('online');
expect(_.keys(contact.get('resources')).length).toBe(2);
expect(contact.get('resources')['c71f218b-0797-4732-8a88-b42cb1d8557a']['priority']).toBe(1);
expect(contact.get('resources')['c71f218b-0797-4732-8a88-b42cb1d8557a']['status']).toBe('online');
expect(contact.get('resources')['androidkhydmcKW']['priority']).toBe(0);
expect(contact.get('resources')['androidkhydmcKW']['status']).toBe('xa');
stanza = $(
'<presence xmlns="jabber:client"'+
' to="dummy@localhost/converse.js-21770972"'+
' type="unavailable"'+
' from="'+contact_jid+'/c71f218b-0797-4732-8a88-b42cb1d8557a">'+
'</presence>');
_converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('xa');
expect(_.keys(contact.get('resources')).length).toBe(1);
expect(contact.get('resources')['androidkhydmcKW']['priority']).toBe(0);
expect(contact.get('resources')['androidkhydmcKW']['status']).toBe('xa');
stanza = $(
'<presence xmlns="jabber:client"'+
' to="dummy@localhost/converse.js-21770972"'+
' type="unavailable"'+
' from="'+contact_jid+'/androidkhydmcKW">'+
'</presence>');
_converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('offline');
expect(_.keys(contact.get('resources')).length).toBe(0);
})); }));
}); });
})); }));

View File

@ -775,7 +775,7 @@
'fullname': bare_jid, 'fullname': bare_jid,
'chat_status': 'offline', 'chat_status': 'offline',
'user_id': Strophe.getNodeFromJid(jid), 'user_id': Strophe.getNodeFromJid(jid),
'resources': resource ? [resource] : [], 'resources': resource ? {'resource':0} : {},
'groups': [], 'groups': [],
'image_type': DEFAULT_IMAGE_TYPE, 'image_type': DEFAULT_IMAGE_TYPE,
'image': DEFAULT_IMAGE, 'image': DEFAULT_IMAGE,
@ -855,23 +855,44 @@
return this; return this;
}, },
addResource: function (resource, priority, chat_status) {
var resources = this.get('resources');
if (!_.isObject(resources)) { resources = {}; }
resources[resource] = {
'priority': _.isNaN(Number(priority)) ? 0 : Number(priority),
'status': chat_status
};
this.set({'resources': resources});
return resources;
},
removeResource: function (resource) { removeResource: function (resource) {
var resources = this.get('resources'), idx; /* Remove the passed in resource from the contact's resources
if (resource) { * map.
idx = _.indexOf(resources, resource); * Return the amount of resources left over.
if (idx !== -1) { */
resources.splice(idx, 1); var resources = this.get('resources');
this.save({'resources': resources}); if (!_.isObject(resources)) {
resources = {};
} else {
delete resources[resource];
}
this.save({'resources': resources});
return _.size(resources);
},
getHighestPriorityStatus: function () {
/* Return the chat status assigned to the resource with the
* highest priority.
*/
var resources = this.get('resources');
if (_.isObject(resources) && _.size(resources)) {
var val = _(resources).values().sortBy('priority').get(0);
if (!_.isUndefined(val)) {
return val.status;
} }
} }
else { return 'offline';
// if there is no resource (resource is null), it probably
// means that the user is now completely offline. To make sure
// that there isn't any "ghost" resources left, we empty the array
this.save({'resources': []});
return 0;
}
return resources.length;
}, },
removeFromRoster: function (callback) { removeFromRoster: function (callback) {
@ -1023,45 +1044,6 @@
return deferred.promise(); return deferred.promise();
}, },
addResource: function (bare_jid, resource) {
var item = this.get(bare_jid),
resources;
if (item) {
resources = item.get('resources');
if (resources) {
if (!_.includes(resources, resource)) {
resources.push(resource);
item.set({'resources': resources});
}
} else {
item.set({'resources': [resource]});
}
}
},
setPriority: function (bare_jid, resource, priority) {
var item = this.get(bare_jid),
stored_priority;
if (item) {
stored_priority = item.get('priority');
if (stored_priority) {
if (priority >= stored_priority) {
item.set({'priority': priority});
item.set({'priority_updated_by': resource});
return true;
} else if (resource === item.get('priority_updated_by')) {
item.set({'priority': priority});
return true;
}
} else {
item.set({'priority': priority});
item.set({'priority_updated_by': resource});
return true;
}
}
return false;
},
subscribeBack: function (bare_jid) { subscribeBack: function (bare_jid) {
var contact = this.get(bare_jid); var contact = this.get(bare_jid);
if (contact instanceof _converse.RosterContact) { if (contact instanceof _converse.RosterContact) {
@ -1267,17 +1249,14 @@
} else if (presence_type === 'subscribe') { } else if (presence_type === 'subscribe') {
this.handleIncomingSubscription(presence); this.handleIncomingSubscription(presence);
} else if (presence_type === 'unavailable' && contact) { } else if (presence_type === 'unavailable' && contact) {
// update priority to default level contact.removeResource(resource);
this.setPriority(bare_jid, resource, 0); contact.save({'chat_status': contact.getHighestPriorityStatus()});
// Only set the user to offline if there aren't any
// other resources still available.
if (contact.removeResource(resource) === 0) {
contact.save({'chat_status': "offline"});
}
} else if (contact) { // presence_type is undefined } else if (contact) { // presence_type is undefined
this.addResource(bare_jid, resource); var resources = contact.addResource(resource, priority, chat_status);
if (this.setPriority(bare_jid, resource, priority)) { if (priority >= _(resources).values().map('priority').max()) {
contact.save({'chat_status': chat_status}); // Only save if it's the resource with the highest
// priority
contact.save({'chat_status': chat_status});
} }
} }
} }