Selaa lähdekoodia

Store presence info in a separate collection

So that we can cache roster data for longer and presence data for
shorter.
JC Brand 7 vuotta sitten
vanhempi
commit
36fd859a88

+ 3 - 0
CHANGES.md

@@ -25,10 +25,13 @@
 - #1039 Multi-option data form elements not shown and saved correctly
 
 ### API changes
+
 - `_converse.api.vcard.get` now also accepts a `Backbone.Model` instance and
   has an additional `force` parameter to force fetching the vcard even if it
   has already been fetched.
 - New API method `_converse.api.vcard.update`.
+- The `contactStatusChanged` event has been renamed to `contactPresenceChanged`
+  and a event `presenceChanged` is now also triggered on the contact.
 
 ## UI changes
 

+ 2 - 1
dev.html

@@ -26,6 +26,7 @@
             //     'prosody@conference.prosody.im',
             //     'jdev@conference.jabber.org'
             // ],
+            websocket_url: 'ws://chat.example.org:5280/xmpp-websocket',
             view_mode: 'fullscreen',
             archived_messages_page_size: '500',
             allow_public_bookmarks: true,
@@ -33,7 +34,7 @@
                 'discuss@conference.conversejs.org'
             ],
             // bosh_service_url: 'http://chat.example.org:5280/http-bind/',
-            bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
+            // bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
             message_archiving: 'always',
             debug: true
         });

+ 5 - 4
docs/source/events.rst

@@ -179,12 +179,13 @@ The user has removed a contact.
 ``_converse.api.listen.on('contactRemoved', function (data) { ... });``
 
 
-contactStatusChanged
-~~~~~~~~~~~~~~~~~~~~
+contactPresenceChanged
+~~~~~~~~~~~~~~~~~~~~~~
 
-When a chat buddy's chat status has changed.
+When a chat buddy's presence status has changed.
+The presence status is either `online`, `offline`, `dnd`, `away` or `xa`.
 
-``_converse.api.listen.on('contactStatusChanged', function (buddy) { ... });``
+``_converse.api.listen.on('contactPresenceChanged', function (presence) { ... });``
 
 contactStatusMessageChanged
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 1 - 1
spec/chatroom.js

@@ -1845,7 +1845,7 @@
                     expect(view.model.get('open')).toBe(false);
                     expect(view.model.get('membersonly')).toBe(true);
                     done();
-                });
+                }).catch(_.partial(console.error, _));
             }));
 
             it("indicates when a room is no longer anonymous",

+ 1 - 1
spec/notification.js

@@ -134,7 +134,7 @@
                         spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
                         spyOn(_converse, 'showChatStateNotification');
                         var jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'busy'); // This will emit 'contactStatusChanged'
+                        _converse.roster.get(jid).presence.set('show', 'busy'); // This will emit 'contactStatusChanged'
                         expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled();
                         expect(_converse.showChatStateNotification).toHaveBeenCalled();
                     }));

+ 70 - 70
spec/presence.js

@@ -144,10 +144,10 @@
             '    <delay xmlns="urn:xmpp:delay" stamp="2017-02-15T20:26:05Z" from="'+contact_jid+'/priority-1-resource"/>'+
             '</presence>');
             _converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
-            expect(contact.get('chat_status')).toBe('online');
-            expect(_.keys(contact.get('resources')).length).toBe(1);
-            expect(contact.get('resources')['priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['priority-1-resource']['status']).toBe('online');
+            expect(contact.presence.get('show')).toBe('online');
+            expect(_.keys(contact.presence.get('resources')).length).toBe(1);
+            expect(contact.presence.get('resources')['priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['priority-1-resource']['show']).toBe('online');
 
             stanza = $(
             '<presence xmlns="jabber:client"'+
@@ -161,12 +161,12 @@
             '    <delay xmlns="urn:xmpp:delay" stamp="2017-02-15T17:02:24Z" from="'+contact_jid+'/priority-0-resource"/>'+
             '</presence>');
             _converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
-            expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('online');
-            expect(_.keys(contact.get('resources')).length).toBe(2);
-            expect(contact.get('resources')['priority-0-resource']['priority']).toBe(0);
-            expect(contact.get('resources')['priority-0-resource']['status']).toBe('xa');
-            expect(contact.get('resources')['priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['priority-1-resource']['status']).toBe('online');
+            expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('online');
+            expect(_.keys(contact.presence.get('resources')).length).toBe(2);
+            expect(contact.presence.get('resources')['priority-0-resource']['priority']).toBe(0);
+            expect(contact.presence.get('resources')['priority-0-resource']['show']).toBe('xa');
+            expect(contact.presence.get('resources')['priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['priority-1-resource']['show']).toBe('online');
 
             stanza = $(
             '<presence xmlns="jabber:client"'+
@@ -176,14 +176,14 @@
             '    <show>dnd</show>'+
             '</presence>');
             _converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
-            expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('dnd');
-            expect(_.keys(contact.get('resources')).length).toBe(3);
-            expect(contact.get('resources')['priority-0-resource']['priority']).toBe(0);
-            expect(contact.get('resources')['priority-0-resource']['status']).toBe('xa');
-            expect(contact.get('resources')['priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['priority-1-resource']['status']).toBe('online');
-            expect(contact.get('resources')['priority-2-resource']['priority']).toBe(2);
-            expect(contact.get('resources')['priority-2-resource']['status']).toBe('dnd');
+            expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('dnd');
+            expect(_.keys(contact.presence.get('resources')).length).toBe(3);
+            expect(contact.presence.get('resources')['priority-0-resource']['priority']).toBe(0);
+            expect(contact.presence.get('resources')['priority-0-resource']['show']).toBe('xa');
+            expect(contact.presence.get('resources')['priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['priority-1-resource']['show']).toBe('online');
+            expect(contact.presence.get('resources')['priority-2-resource']['priority']).toBe(2);
+            expect(contact.presence.get('resources')['priority-2-resource']['show']).toBe('dnd');
 
             stanza = $(
             '<presence xmlns="jabber:client"'+
@@ -193,16 +193,16 @@
             '    <show>away</show>'+
             '</presence>');
             _converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
-            expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('away');
-            expect(_.keys(contact.get('resources')).length).toBe(4);
-            expect(contact.get('resources')['priority-0-resource']['priority']).toBe(0);
-            expect(contact.get('resources')['priority-0-resource']['status']).toBe('xa');
-            expect(contact.get('resources')['priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['priority-1-resource']['status']).toBe('online');
-            expect(contact.get('resources')['priority-2-resource']['priority']).toBe(2);
-            expect(contact.get('resources')['priority-2-resource']['status']).toBe('dnd');
-            expect(contact.get('resources')['priority-3-resource']['priority']).toBe(3);
-            expect(contact.get('resources')['priority-3-resource']['status']).toBe('away');
+            expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('away');
+            expect(_.keys(contact.presence.get('resources')).length).toBe(4);
+            expect(contact.presence.get('resources')['priority-0-resource']['priority']).toBe(0);
+            expect(contact.presence.get('resources')['priority-0-resource']['show']).toBe('xa');
+            expect(contact.presence.get('resources')['priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['priority-1-resource']['show']).toBe('online');
+            expect(contact.presence.get('resources')['priority-2-resource']['priority']).toBe(2);
+            expect(contact.presence.get('resources')['priority-2-resource']['show']).toBe('dnd');
+            expect(contact.presence.get('resources')['priority-3-resource']['priority']).toBe(3);
+            expect(contact.presence.get('resources')['priority-3-resource']['show']).toBe('away');
 
             stanza = $(
             '<presence xmlns="jabber:client"'+
@@ -213,18 +213,18 @@
             '    <delay xmlns="urn:xmpp:delay" stamp="2017-02-15T15:02:24Z" from="'+contact_jid+'/older-priority-1-resource"/>'+
             '</presence>');
             _converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
-            expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('away');
-            expect(_.keys(contact.get('resources')).length).toBe(5);
-            expect(contact.get('resources')['older-priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['older-priority-1-resource']['status']).toBe('dnd');
-            expect(contact.get('resources')['priority-0-resource']['priority']).toBe(0);
-            expect(contact.get('resources')['priority-0-resource']['status']).toBe('xa');
-            expect(contact.get('resources')['priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['priority-1-resource']['status']).toBe('online');
-            expect(contact.get('resources')['priority-2-resource']['priority']).toBe(2);
-            expect(contact.get('resources')['priority-2-resource']['status']).toBe('dnd');
-            expect(contact.get('resources')['priority-3-resource']['priority']).toBe(3);
-            expect(contact.get('resources')['priority-3-resource']['status']).toBe('away');
+            expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('away');
+            expect(_.keys(contact.presence.get('resources')).length).toBe(5);
+            expect(contact.presence.get('resources')['older-priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['older-priority-1-resource']['show']).toBe('dnd');
+            expect(contact.presence.get('resources')['priority-0-resource']['priority']).toBe(0);
+            expect(contact.presence.get('resources')['priority-0-resource']['show']).toBe('xa');
+            expect(contact.presence.get('resources')['priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['priority-1-resource']['show']).toBe('online');
+            expect(contact.presence.get('resources')['priority-2-resource']['priority']).toBe(2);
+            expect(contact.presence.get('resources')['priority-2-resource']['show']).toBe('dnd');
+            expect(contact.presence.get('resources')['priority-3-resource']['priority']).toBe(3);
+            expect(contact.presence.get('resources')['priority-3-resource']['show']).toBe('away');
 
             stanza = $(
             '<presence xmlns="jabber:client"'+
@@ -233,16 +233,16 @@
             '          from="'+contact_jid+'/priority-3-resource">'+
             '</presence>');
             _converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
-            expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('dnd');
-            expect(_.keys(contact.get('resources')).length).toBe(4);
-            expect(contact.get('resources')['priority-0-resource']['priority']).toBe(0);
-            expect(contact.get('resources')['priority-0-resource']['status']).toBe('xa');
-            expect(contact.get('resources')['priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['priority-1-resource']['status']).toBe('online');
-            expect(contact.get('resources')['priority-2-resource']['priority']).toBe(2);
-            expect(contact.get('resources')['priority-2-resource']['status']).toBe('dnd');
-            expect(contact.get('resources')['older-priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['older-priority-1-resource']['status']).toBe('dnd');
+            expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('dnd');
+            expect(_.keys(contact.presence.get('resources')).length).toBe(4);
+            expect(contact.presence.get('resources')['priority-0-resource']['priority']).toBe(0);
+            expect(contact.presence.get('resources')['priority-0-resource']['show']).toBe('xa');
+            expect(contact.presence.get('resources')['priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['priority-1-resource']['show']).toBe('online');
+            expect(contact.presence.get('resources')['priority-2-resource']['priority']).toBe(2);
+            expect(contact.presence.get('resources')['priority-2-resource']['show']).toBe('dnd');
+            expect(contact.presence.get('resources')['older-priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['older-priority-1-resource']['show']).toBe('dnd');
 
             stanza = $(
             '<presence xmlns="jabber:client"'+
@@ -251,14 +251,14 @@
             '          from="'+contact_jid+'/priority-2-resource">'+
             '</presence>');
             _converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
-            expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('online');
-            expect(_.keys(contact.get('resources')).length).toBe(3);
-            expect(contact.get('resources')['priority-0-resource']['priority']).toBe(0);
-            expect(contact.get('resources')['priority-0-resource']['status']).toBe('xa');
-            expect(contact.get('resources')['priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['priority-1-resource']['status']).toBe('online');
-            expect(contact.get('resources')['older-priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['older-priority-1-resource']['status']).toBe('dnd');
+            expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('online');
+            expect(_.keys(contact.presence.get('resources')).length).toBe(3);
+            expect(contact.presence.get('resources')['priority-0-resource']['priority']).toBe(0);
+            expect(contact.presence.get('resources')['priority-0-resource']['show']).toBe('xa');
+            expect(contact.presence.get('resources')['priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['priority-1-resource']['show']).toBe('online');
+            expect(contact.presence.get('resources')['older-priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['older-priority-1-resource']['show']).toBe('dnd');
 
             stanza = $(
             '<presence xmlns="jabber:client"'+
@@ -267,12 +267,12 @@
             '          from="'+contact_jid+'/priority-1-resource">'+
             '</presence>');
             _converse.connection._dataRecv(test_utils.createRequest(stanza[0]));
-            expect(_converse.roster.get(contact_jid).get('chat_status')).toBe('dnd');
-            expect(_.keys(contact.get('resources')).length).toBe(2);
-            expect(contact.get('resources')['priority-0-resource']['priority']).toBe(0);
-            expect(contact.get('resources')['priority-0-resource']['status']).toBe('xa');
-            expect(contact.get('resources')['older-priority-1-resource']['priority']).toBe(1);
-            expect(contact.get('resources')['older-priority-1-resource']['status']).toBe('dnd');
+            expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('dnd');
+            expect(_.keys(contact.presence.get('resources')).length).toBe(2);
+            expect(contact.presence.get('resources')['priority-0-resource']['priority']).toBe(0);
+            expect(contact.presence.get('resources')['priority-0-resource']['show']).toBe('xa');
+            expect(contact.presence.get('resources')['older-priority-1-resource']['priority']).toBe(1);
+            expect(contact.presence.get('resources')['older-priority-1-resource']['show']).toBe('dnd');
 
             stanza = $(
             '<presence xmlns="jabber:client"'+
@@ -281,10 +281,10 @@
             '          from="'+contact_jid+'/older-priority-1-resource">'+
             '</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')['priority-0-resource']['priority']).toBe(0);
-            expect(contact.get('resources')['priority-0-resource']['status']).toBe('xa');
+            expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('xa');
+            expect(_.keys(contact.presence.get('resources')).length).toBe(1);
+            expect(contact.presence.get('resources')['priority-0-resource']['priority']).toBe(0);
+            expect(contact.presence.get('resources')['priority-0-resource']['show']).toBe('xa');
 
             stanza = $(
             '<presence xmlns="jabber:client"'+
@@ -293,8 +293,8 @@
             '          from="'+contact_jid+'/priority-0-resource">'+
             '</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);
+            expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('offline');
+            expect(_.keys(contact.presence.get('resources')).length).toBe(0);
             done();
         }));
     });

+ 2 - 2
spec/protocol.js

@@ -306,7 +306,7 @@
                     expect($contacts.hasClass('both')).toBeFalsy();
                     expect($contacts.hasClass('current-xmpp-contact')).toBeTruthy();
                     expect($contacts.text().trim()).toBe('Contact');
-                    expect(contact.get('chat_status')).toBe('offline');
+                    expect(contact.presence.get('show')).toBe('offline');
 
                     /*  <presence
                      *      from='contact@example.org/resource'
@@ -315,7 +315,7 @@
                     stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'});
                     _converse.connection._dataRecv(test_utils.createRequest(stanza));
                     // Now the contact should also be online.
-                    expect(contact.get('chat_status')).toBe('online');
+                    expect(contact.presence.get('show')).toBe('online');
 
                     /* Section 8.3.  Creating a Mutual Subscription
                      *

+ 22 - 31
spec/roster.js

@@ -2,6 +2,7 @@
     define(["jquery", "jasmine", "mock", "converse-core", "test-utils"], factory);
 } (this, function ($, jasmine, mock, converse, test_utils) {
     var _ = converse.env._;
+    var Strophe = converse.env.Strophe;
     var $pres = converse.env.$pres;
     var $msg = converse.env.$msg;
     var $iq = converse.env.$iq;
@@ -94,16 +95,12 @@
                 var $roster = $(_converse.rosterview.roster_el);
                 _converse.rosterview.filter_view.delegateEvents();
 
-                var promise = test_utils.waitUntil(function () {
-                    return $roster.find('li:visible').length === 15;
-                }, 600).then(function (contacts) {
+                var promise = test_utils.waitUntil(() => $roster.find('li:visible').length === 15, 600)
+                .then(function (contacts) {
                     expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
                     $filter[0].value = "candice";
                     u.triggerEvent($filter[0], "keydown", "KeyboardEvent");
-
-                    return test_utils.waitUntil(function () {
-                        return $roster.find('li:visible').length === 1;
-                    }, 600);
+                    return test_utils.waitUntil(() => $roster.find('li:visible').length === 1, 600);
                 }).then(function (contacts) {
                     // Only one roster contact is now visible
                     expect($roster.find('li:visible').length).toBe(1);
@@ -149,7 +146,7 @@
                     expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
                     _converse.roster_groups = false;
                     done();
-                });
+                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
             }));
 
             it("will also filter out contacts added afterwards",
@@ -290,26 +287,21 @@
 
                 test_utils.createGroupedContacts(_converse);
                 var jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@localhost';
-                _converse.roster.get(jid).set('chat_status', 'online');
+                _converse.roster.get(jid).presence.set('show', 'online');
                 jid = mock.cur_names[4].replace(/ /g,'.').toLowerCase() + '@localhost';
-                _converse.roster.get(jid).set('chat_status', 'dnd');
+                _converse.roster.get(jid).presence.set('show', 'dnd');
                 test_utils.openControlBox();
 
                 var button = _converse.rosterview.el.querySelector('span[data-type="state"]');
                 button.click();
 
                 var $roster = $(_converse.rosterview.roster_el);
-                test_utils.waitUntil(function () {
-                    return $roster.find('li:visible').length === 15;
-                }, 500).then(function () {
+                test_utils.waitUntil(() => $roster.find('li:visible').length === 15, 500).then(function () {
                     var filter = _converse.rosterview.el.querySelector('.state-type');
                     expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
                     filter.value = "online";
                     u.triggerEvent(filter, 'change');
-
-                    return test_utils.waitUntil(function () {
-                        return $roster.find('li:visible').length === 1;
-                    }, 500)
+                    return test_utils.waitUntil(() => $roster.find('li:visible').length === 1, 500);
                 }).then(function () {
                     expect($roster.find('li:visible').eq(0).text().trim()).toBe('Rinse Sommer');
                     expect($roster.find('ul.roster-group-contacts:visible').length).toBe(1);
@@ -323,7 +315,7 @@
                 }).then(function () {
                     expect($roster.find('ul.roster-group-contacts:visible').length).toBe(1);
                     done();
-                });
+                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
             }));
         });
 
@@ -851,16 +843,15 @@
                     function (done, _converse) {
 
                 _addContacts(_converse);
-                test_utils.waitUntil(function () {
-                        return $(_converse.rosterview.el).find('.roster-group li').length;
-                }, 700).then(function () {
+                test_utils.waitUntil(() => $(_converse.rosterview.el).find('.roster-group li').length, 700)
+                .then(function () {
                     var jid, t;
                     spyOn(_converse, 'emit');
                     spyOn(_converse.rosterview, 'update').and.callThrough();
                     var $roster = $(_converse.rosterview.el);
                     for (var i=0; i<mock.cur_names.length; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'online');
+                        _converse.roster.get(jid).presence.set('show', 'online');
                         expect(_converse.rosterview.update).toHaveBeenCalled();
                         // Check that they are sorted alphabetically
                         t = _.reduce($roster.find('.roster-group').find('.current-xmpp-contact.online a.open-chat'), function (result, value) {
@@ -887,7 +878,7 @@
                     var $roster = $(_converse.rosterview.el);
                     for (var i=0; i<mock.cur_names.length; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'dnd');
+                        _converse.roster.get(jid).presence.set('show', 'dnd');
                         expect(_converse.rosterview.update).toHaveBeenCalled();
                         // Check that they are sorted alphabetically
                         t = _.reduce($roster.find('.roster-group .current-xmpp-contact.dnd a.open-chat'),
@@ -915,7 +906,7 @@
                     var $roster = $(_converse.rosterview.el);
                     for (var i=0; i<mock.cur_names.length; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'away');
+                        _converse.roster.get(jid).presence.set('show', 'away');
                         expect(_converse.rosterview.update).toHaveBeenCalled();
                         // Check that they are sorted alphabetically
                         t = _.reduce($roster.find('.roster-group .current-xmpp-contact.away a.open-chat'),
@@ -943,7 +934,7 @@
                     var $roster = $(_converse.rosterview.el);
                     for (var i=0; i<mock.cur_names.length; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'xa');
+                        _converse.roster.get(jid).presence.set('show', 'xa');
                         expect(_converse.rosterview.update).toHaveBeenCalled();
                         // Check that they are sorted alphabetically
                         t = _.reduce($roster.find('.roster-group .current-xmpp-contact.xa a.open-chat'),
@@ -972,7 +963,7 @@
                     var $roster = $(_converse.rosterview.el);
                     for (var i=0; i<mock.cur_names.length; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'unavailable');
+                        _converse.roster.get(jid).presence.set('show', 'unavailable');
                         expect(_converse.rosterview.update).toHaveBeenCalled();
                         // Check that they are sorted alphabetically
                         t = _.reduce($roster.find('.roster-group .current-xmpp-contact.unavailable a.open-chat'),
@@ -997,23 +988,23 @@
                     var i, jid;
                     for (i=0; i<3; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'online');
+                        _converse.roster.get(jid).presence.set('show', 'online');
                     }
                     for (i=3; i<6; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'dnd');
+                        _converse.roster.get(jid).presence.set('show', 'dnd');
                     }
                     for (i=6; i<9; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'away');
+                        _converse.roster.get(jid).presence.set('show', 'away');
                     }
                     for (i=9; i<12; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'xa');
+                        _converse.roster.get(jid).presence.set('show', 'xa');
                     }
                     for (i=12; i<15; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        _converse.roster.get(jid).set('chat_status', 'unavailable');
+                        _converse.roster.get(jid).presence.set('show', 'unavailable');
                     }
                     return test_utils.waitUntil(function () {
                         return $(_converse.rosterview.el).find('li.online').length

+ 2 - 2
spec/spoilers.js

@@ -94,7 +94,7 @@
                 var spoiler_hint_el = view.el.querySelector('.spoiler-hint');
                 expect(spoiler_hint_el.textContent).toBe('');
                 done();
-            });
+            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
         }));
 
         it("can be sent without a hint",
@@ -169,7 +169,7 @@
                 spoiler_toggle.click();
                 expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
                 done();
-            });
+            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
         }));
 
         it("can be sent with a hint",

+ 3 - 5
src/converse-chatboxes.js

@@ -230,7 +230,7 @@
             });
 
 
-            _converse.ChatBox = Backbone.Model.extend({
+            _converse.ChatBox = _converse.ModelWithVCardAndPresence.extend({
                 defaults: {
                     'bookmarked': false,
                     'chat_state': undefined,
@@ -241,10 +241,8 @@
                 },
 
                 initialize () {
-                    this.vcard = _converse.vcards.findWhere({'jid': this.get('jid')});
-                    if (_.isNil(this.vcard)) {
-                        this.vcard = _converse.vcards.create({'jid': this.get('jid')});
-                    }
+                    _converse.ModelWithVCardAndPresence.prototype.initialize.apply(this, arguments);
+
                     _converse.api.waitUntil('rosterContactsFetched').then(() => {
                         this.addRelatedContact(_converse.roster.findWhere({'jid': this.get('jid')}));
                     });

+ 12 - 13
src/converse-chatview.js

@@ -344,8 +344,8 @@
 
                     this.model.on('show', this.show, this);
                     this.model.on('destroy', this.remove, this);
-                    // TODO check for changed fullname as well
-                    this.model.on('change:chat_status', this.onChatStatusChanged, this);
+
+                    this.model.presence.on('change:show', this.onPresenceChanged, this);
                     this.model.on('showHelpMessages', this.showHelpMessages, this);
                     this.render();
 
@@ -446,14 +446,14 @@
                         return;
                     }
                     const contact_jid = this.model.get('jid');
-                    const resources = this.model.get('resources');
+                    const resources = this.model.presence.get('resources');
                     if (_.isEmpty(resources)) {
                         return;
                     }
                     Promise.all(_.map(_.keys(resources), (resource) =>
                         _converse.api.disco.supports(Strophe.NS.SPOILER, `${contact_jid}/${resource}`)
                     )).then((results) => {
-                        if (results.length) {
+                        if (_.filter(results, 'length').length) {
                             const html = tpl_spoiler_button(this.model.toJSON());
                             if (_converse.visible_toolbar_buttons.emoji) {
                                 this.el.querySelector('.toggle-smiley').insertAdjacentHTML('afterEnd', html);
@@ -999,20 +999,19 @@
                     }
                 },
 
-                onChatStatusChanged (item) {
-                    const chat_status = item.get('chat_status');
-                    let fullname = item.get('fullname');
-                    let text;
+                onPresenceChanged (item) {
+                    const show = item.get('show'),
+                          fullname = this.model.getDisplayName();
 
-                    fullname = _.isEmpty(fullname)? item.get('jid'): fullname;
+                    let text;
                     if (u.isVisible(this.el)) {
-                        if (chat_status === 'offline') {
+                        if (show === 'offline') {
                             text = fullname+' '+__('has gone offline');
-                        } else if (chat_status === 'away') {
+                        } else if (show === 'away') {
                             text = fullname+' '+__('has gone away');
-                        } else if ((chat_status === 'dnd')) {
+                        } else if ((show === 'dnd')) {
                             text = fullname+' '+__('is busy');
-                        } else if (chat_status === 'online') {
+                        } else if (show === 'online') {
                             text = fullname+' '+__('is online');
                         }
                         if (text) {

+ 0 - 8
src/converse-core.js

@@ -751,13 +751,6 @@
         };
 
 
-        this.unregisterPresenceHandler = function () {
-            if (!_.isUndefined(_converse.presence_ref)) {
-                _converse.connection.deleteHandler(_converse.presence_ref);
-                delete _converse.presence_ref;
-            }
-        };
-
         this.sendInitialPresence = function () {
             if (_converse.send_initial_presence) {
                 _converse.xmppstatus.sendPresence();
@@ -1094,7 +1087,6 @@
              * connection.
              */
             _converse.emit('beforeTearDown');
-            _converse.unregisterPresenceHandler();
             if (!_.isUndefined(_converse.session)) {
                 _converse.session.destroy();
             }

+ 4 - 4
src/converse-disco.js

@@ -448,8 +448,8 @@
                         if (_.isNil(entity_jid)) {
                             throw new TypeError('disco.supports: You need to provide an entity JID');
                         }
-                        return _converse.api.waitUntil('discoInitialized').then(() => {
-                            return new Promise((resolve, reject) => {
+                        return new Promise((resolve, reject) => {
+                            return _converse.api.waitUntil('discoInitialized').then(() => {
                                 _converse.api.disco.entities.get(entity_jid, true).then((entity) => {
                                     entity.waitUntilFeaturesDiscovered.then(() => {
                                         const promises = _.concat(
@@ -460,8 +460,8 @@
                                             resolve(f.filter(f.isObject, result));
                                         }).catch(reject);
                                     }).catch(reject);
-                                })
-                            });
+                                }).catch(reject);
+                            }).catch(reject);
                         }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                     },
 

+ 2 - 2
src/converse-notification.js

@@ -225,7 +225,7 @@
             };
 
             _converse.handleChatStateNotification = function (contact) {
-                /* Event handler for on('contactStatusChanged').
+                /* Event handler for on('contactPresenceChanged').
                  * Will show an HTML5 notification to indicate that the chat
                  * status has changed.
                  */
@@ -274,7 +274,7 @@
                 // registered, because other plugins might override some of our
                 // handlers.
                 _converse.on('contactRequest',  _converse.handleContactRequestNotification);
-                _converse.on('contactStatusChanged',  _converse.handleChatStateNotification);
+                _converse.on('contactPresenceChanged',  _converse.handleChatStateNotification);
                 _converse.on('message',  _converse.handleMessageNotification);
                 _converse.on('feedback', _converse.handleFeedback);
                 _converse.on('connected', _converse.requestPermission);

+ 141 - 97
src/converse-roster.js

@@ -18,9 +18,6 @@
         overrides: {
             _tearDown () {
                 this.__super__._tearDown.apply(this, arguments);
-                if (this.roster) {
-                    this.roster.off().reset(); // Removes roster contacts
-                }
             }
         },
 
@@ -55,10 +52,10 @@
                 * roster and the roster groups.
                 */
                 _converse.roster = new _converse.RosterContacts();
-                _converse.roster.browserStorage = new Backbone.BrowserStorage.session(
+                _converse.roster.browserStorage = new Backbone.BrowserStorage[_converse.storage](
                     b64_sha1(`converse.contacts-${_converse.bare_jid}`));
                 _converse.rostergroups = new _converse.RosterGroups();
-                _converse.rostergroups.browserStorage = new Backbone.BrowserStorage.session(
+                _converse.rostergroups.browserStorage = new Backbone.BrowserStorage[_converse.storage](
                     b64_sha1(`converse.roster.groups${_converse.bare_jid}`));
                 _converse.emit('rosterInitialized');
             };
@@ -98,11 +95,115 @@
             };
 
 
-            _converse.RosterContact = Backbone.Model.extend({
+            _converse.Presence = Backbone.Model.extend({
+                defaults: {
+                    'show': 'offline',
+                    'resources': {}
+                },
+
+                getHighestPriorityResource () {
+                    /* Return the resource with the highest priority.
+                     *
+                     * If multiple resources have the same priority, take the
+                     * latest one.
+                     */
+                    const resources = this.get('resources');
+                    if (_.isObject(resources) && _.size(resources)) {
+                        const val = _.flow(
+                                _.values,
+                                _.partial(_.sortBy, _, ['priority', 'timestamp']),
+                                _.reverse
+                            )(resources)[0];
+                        if (!_.isUndefined(val)) {
+                            return val;
+                        }
+                    }
+                },
+
+                addResource (presence) {
+                    /* Adds a new resource and it's associated attributes as taken
+                     * from the passed in presence stanza.
+                     *
+                     * Also updates the presence if the resource has higher priority (and is newer).
+                     */
+                    const jid = presence.getAttribute('from'),
+                        show = _.propertyOf(presence.querySelector('show'))('textContent') || 'online',
+                        resource = Strophe.getResourceFromJid(jid),
+                        delay = presence.querySelector(
+                            `delay[xmlns="${Strophe.NS.DELAY}"]`
+                        ),
+                        timestamp = _.isNull(delay) ? moment().format() : moment(delay.getAttribute('stamp')).format();
+
+                    let priority = _.propertyOf(presence.querySelector('priority'))('textContent') || 0;
+                    priority = _.isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10);
+
+                    const resources = _.isObject(this.get('resources')) ? this.get('resources') : {};
+                    resources[resource] = {
+                        'name': resource,
+                        'priority': priority,
+                        'show': show,
+                        'timestamp': timestamp
+                    };
+                    const changed = {'resources': resources};
+                    const hpr = this.getHighestPriorityResource();
+                    if (priority == hpr.priority && timestamp == hpr.timestamp) {
+                        // Only set the "global" presence if this is the newest resource
+                        // with the highest priority
+                        changed.show = show;
+                    }
+                    this.save(changed);
+                    return resources;
+                },
+
+
+                removeResource (resource) {
+                    /* Remove the passed in resource from the resources map.
+                     *
+                     * Also redetermines the presence given that there's one less
+                     * resource.
+                     */
+                     let resources = this.get('resources');
+                     if (!_.isObject(resources)) {
+                         resources = {};
+                     } else {
+                         delete resources[resource];
+                     }
+                     this.save({
+                         'resources': resources,
+                         'show': _.propertyOf(
+                             this.getHighestPriorityResource())('show') || 'offline'
+                     });
+                },
+
+            });
+
+
+            _converse.Presences = Backbone.Collection.extend({
+                model: _converse.Presence,
+            });
+
+
+            _converse.ModelWithVCardAndPresence = Backbone.Model.extend({
+                initialize () {
+                    this.setVCard();
+                    this.setPresence();
+                },
+
+                setVCard () {
+                    const jid = this.get('jid');
+                    this.vcard = _converse.vcards.findWhere({'jid': jid}) || _converse.vcards.create({'jid': jid});
+                },
+
+                setPresence () {
+                    const jid = this.get('jid');
+                    this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid});
+                }
+            });
+
 
+            _converse.RosterContact = _converse.ModelWithVCardAndPresence.extend({
                 defaults: {
                     'chat_state': undefined,
-                    'chat_status': 'offline',
                     'image': _converse.DEFAULT_IMAGE,
                     'image_type': _converse.DEFAULT_IMAGE_TYPE,
                     'num_unread': 0,
@@ -110,6 +211,8 @@
                 },
 
                 initialize (attributes) {
+                    _converse.ModelWithVCardAndPresence.prototype.initialize.apply(this, arguments);
+
                     const { jid } = attributes,
                         bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(),
                         resource = Strophe.getResourceFromJid(jid);
@@ -119,18 +222,11 @@
                         'groups': [],
                         'id': bare_jid,
                         'jid': bare_jid,
-                        'resources': {},
                         'user_id': Strophe.getNodeFromJid(jid)
                     }, attributes));
 
-                    this.vcard = _converse.vcards.findWhere({'jid': bare_jid});
-                    if (_.isNil(this.vcard)) {
-                        this.vcard = _converse.vcards.create({'jid': bare_jid});
-                    }
-
-                    this.on('change:chat_status', function (item) {
-                        _converse.emit('contactStatusChanged', item.attributes);
-                    });
+                    this.presence.on('change:show', () => _converse.emit('contactPresenceChanged', this));
+                    this.presence.on('change:show', () => this.trigger('presenceChanged'));
                 },
 
                 getDisplayName () {
@@ -209,80 +305,6 @@
                     return this;
                 },
 
-                addResource (presence) {
-                    /* Adds a new resource and it's associated attributes as taken
-                    * from the passed in presence stanza.
-                    *
-                    * Also updates the contact's chat_status if the presence has
-                    * higher priority (and is newer).
-                    */
-                    const jid = presence.getAttribute('from'),
-                        chat_status = _.propertyOf(presence.querySelector('show'))('textContent') || 'online',
-                        resource = Strophe.getResourceFromJid(jid),
-                        delay = presence.querySelector(
-                            `delay[xmlns="${Strophe.NS.DELAY}"]`
-                        ),
-                        timestamp = _.isNull(delay) ? moment().format() : moment(delay.getAttribute('stamp')).format();
-
-                    let priority = _.propertyOf(presence.querySelector('priority'))('textContent') || 0;
-                    priority = _.isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10);
-
-                    const resources = _.isObject(this.get('resources')) ? this.get('resources') : {};
-                    resources[resource] = {
-                        'name': resource,
-                        'priority': priority,
-                        'status': chat_status,
-                        'timestamp': timestamp
-                    };
-                    const changed = {'resources': resources};
-                    const hpr = this.getHighestPriorityResource();
-                    if (priority == hpr.priority && timestamp == hpr.timestamp) {
-                        // Only set the chat status if this is the newest resource
-                        // with the highest priority
-                        changed.chat_status = chat_status;
-                    }
-                    this.save(changed);
-                    return resources;
-                },
-
-                removeResource (resource) {
-                    /* Remove the passed in resource from the contact's resources map.
-                    *
-                    * Also recomputes the chat_status given that there's one less
-                    * resource.
-                    */
-                    let resources = this.get('resources');
-                    if (!_.isObject(resources)) {
-                        resources = {};
-                    } else {
-                        delete resources[resource];
-                    }
-                    this.save({
-                        'resources': resources,
-                        'chat_status': _.propertyOf(
-                            this.getHighestPriorityResource())('status') || 'offline'
-                    });
-                },
-
-                getHighestPriorityResource () {
-                    /* Return the resource with the highest priority.
-                    *
-                    * If multiple resources have the same priority, take the
-                    * newest one.
-                    */
-                    const resources = this.get('resources');
-                    if (_.isObject(resources) && _.size(resources)) {
-                        const val = _.flow(
-                                _.values,
-                                _.partial(_.sortBy, _, ['priority', 'timestamp']),
-                                _.reverse
-                            )(resources)[0];
-                        if (!_.isUndefined(val)) {
-                            return val;
-                        }
-                    }
-                },
-
                 removeFromRoster (callback, errback) {
                     /* Instruct the XMPP server to remove this contact from our roster
                     * Parameters:
@@ -301,8 +323,8 @@
                 model: _converse.RosterContact,
 
                 comparator (contact1, contact2) {
-                    const status1 = contact1.get('chat_status') || 'offline';
-                    const status2 = contact2.get('chat_status') || 'offline';
+                    const status1 = contact1.presence.get('show') || 'offline';
+                    const status2 = contact2.presence.get('show') || 'offline';
                     if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) {
                         const name1 = (contact1.getDisplayName()).toLowerCase();
                         const name2 = (contact2.getDisplayName()).toLowerCase();
@@ -485,7 +507,7 @@
                     if (_converse.show_only_online_users) {
                         ignored = _.union(ignored, ['dnd', 'xa', 'away']);
                     }
-                    return _.sum(this.models.filter((model) => !_.includes(ignored, model.get('chat_status'))));
+                    return _.sum(this.models.filter((model) => !_.includes(ignored, model.presence.get('show'))));
                 },
 
                 onRosterPush (iq) {
@@ -633,7 +655,6 @@
                     const jid = presence.getAttribute('from'),
                         bare_jid = Strophe.getBareJidFromJid(jid),
                         resource = Strophe.getResourceFromJid(jid),
-                        chat_status = _.propertyOf(presence.querySelector('show'))('textContent') || 'online',
                         status_message = _.propertyOf(presence.querySelector('status'))('textContent'),
                         contact = this.get(bare_jid);
 
@@ -645,7 +666,8 @@
                             // Another resource has changed its status and
                             // synchronize_availability option set to update,
                             // we'll update ours as well.
-                            _converse.xmppstatus.save({'status': chat_status});
+                            const show = _.propertyOf(presence.querySelector('show'))('textContent') || 'online';
+                            _converse.xmppstatus.save({'status': show});
                             if (status_message) {
                                 _converse.xmppstatus.save({'status_message': status_message});
                             }
@@ -683,10 +705,10 @@
                     } else if (presence_type === 'subscribe') {
                         this.handleIncomingSubscription(presence);
                     } else if (presence_type === 'unavailable' && contact) {
-                        contact.removeResource(resource);
+                        contact.presence.removeResource(resource);
                     } else if (contact) {
                         // presence_type is undefined
-                        contact.addResource(presence);
+                        contact.presence.addResource(presence);
                     }
                 }
             });
@@ -725,15 +747,37 @@
                 }
             });
 
+            _converse.unregisterPresenceHandler = function () {
+                if (!_.isUndefined(_converse.presence_ref)) {
+                    _converse.connection.deleteHandler(_converse.presence_ref);
+                    delete _converse.presence_ref;
+                }
+            };
+
 
             /********** Event Handlers *************/
 
+            _converse.api.listen.on('beforeTearDown', _converse.unregisterPresenceHandler());
+
+            _converse.api.listen.on('afterTearDown', () => {
+                if (_converse.presence) {
+                    _converse.presences.off().reset(); // Remove presences
+                }
+            });
+
             _converse.api.listen.on('clearSession', () => {
                 if (!_.isUndefined(this.roster)) {
                     this.roster.browserStorage._clear();
                 }
             });
 
+            _converse.api.listen.on('connectionInitialized', () => {
+                _converse.presences = new _converse.Presences();
+                _converse.presences.browserStorage = 
+                    new Backbone.BrowserStorage.session(b64_sha1(`converse.presences-${_converse.bare_jid}`));
+                _converse.presences.fetch();
+            });
+
             _converse.api.listen.on('statusInitialized', (reconnecting) => {
                 if (reconnecting) {
                     // No need to recreate the roster, otherwise we lose our

+ 23 - 18
src/converse-rosterview.js

@@ -371,6 +371,8 @@
                     this.model.on("destroy", this.remove, this);
                     this.model.on("open", this.openChat, this);
                     this.model.on("remove", this.remove, this);
+                    
+                    this.model.presence.on("change:show", this.render, this);
                     this.model.vcard.on('change:fullname', this.render, this);
                 },
 
@@ -382,7 +384,7 @@
                     }
                     const item = this.model,
                         ask = item.get('ask'),
-                        chat_status = item.get('chat_status'),
+                        show = item.presence.get('show'),
                         requesting  = item.get('requesting'),
                         subscription = item.get('subscription');
 
@@ -398,8 +400,8 @@
                                 that.el.classList.remove(cls);
                             }
                         });
-                    this.el.classList.add(chat_status);
-                    this.el.setAttribute('data-status', chat_status);
+                    this.el.classList.add(show);
+                    this.el.setAttribute('data-status', show);
 
                     if ((ask === 'subscribe') || (subscription === 'from')) {
                         /* ask === 'subscribe'
@@ -444,21 +446,21 @@
 
                 renderRosterItem (item) {
                     let status_icon = 'fa-times-circle';
-                    const chat_status = item.get('chat_status') || 'offline';
-                    if (chat_status === 'online') {
+                    const show = item.presence.get('show') || 'offline';
+                    if (show === 'online') {
                         status_icon = 'fa-circle';
-                    } else if (chat_status === 'away') {
+                    } else if (show === 'away') {
                         status_icon = 'fa-dot-circle-o';
-                    } else if (chat_status === 'xa') {
+                    } else if (show === 'xa') {
                         status_icon = 'fa-circle-o';
-                    } else if (chat_status === 'dnd') {
+                    } else if (show === 'dnd') {
                         status_icon = 'fa-minus-circle';
                     }
                     const display_name = item.getDisplayName();
                     this.el.innerHTML = tpl_roster_item(
                         _.extend(item.toJSON(), {
                             'display_name': display_name,
-                            'desc_status': STATUSES[chat_status],
+                            'desc_status': STATUSES[show],
                             'status_icon': status_icon,
                             'desc_chat': __('Click to chat with %1$s (JID: %2$s)', display_name, item.get('jid')),
                             'desc_remove': __('Click to remove %1$s as a contact', display_name),
@@ -476,7 +478,7 @@
                      * It doesn't check for the more specific case of whether
                      * the group it's in is collapsed.
                      */
-                    const chatStatus = this.model.get('chat_status');
+                    const chatStatus = this.model.presence.get('show');
                     if ((_converse.show_only_online_users && chatStatus !== 'online') ||
                         (_converse.hide_offline_users && chatStatus === 'offline')) {
                         // If pending or requesting, show
@@ -544,7 +546,7 @@
                 ItemView: _converse.RosterContactView,
                 listItems: 'model.contacts',
                 listSelector: '.roster-group-contacts',
-                sortEvent: 'change:chat_status',
+                sortEvent: 'presenceChanged',
 
                 initialize () {
                     Backbone.OrderedListView.prototype.initialize.apply(this, arguments);
@@ -629,13 +631,13 @@
                             // show requesting contacts, even though they don't
                             // have the state in question.
                             matches = this.model.contacts.filter(
-                                (contact) => u.contains.not('chat_status', q)(contact) && !contact.get('requesting')
+                                (contact) => !_.includes(contact.presence.get('show'), q) && !contact.get('requesting')
                             );
                         } else if (q === 'unread_messages') {
                             matches = this.model.contacts.filter({'num_unread': 0});
                         } else {
                             matches = this.model.contacts.filter(
-                                u.contains.not('chat_status', q)
+                                (contact) => !_.includes(contact.presence.get('show'), q)
                             );
                         }
                     } else  {
@@ -745,6 +747,10 @@
                     _converse.roster.on('change', this.onContactChange, this);
                     _converse.roster.on("destroy", this.update, this);
                     _converse.roster.on("remove", this.update, this);
+                    _converse.presences.on('change:show', () => {
+                        this.update();
+                        this.updateFilter();
+                    });
 
                     this.model.on("reset", this.reset, this);
 
@@ -848,12 +854,14 @@
                 },
 
                 onContactAdded (contact) {
-                    this.addRosterContact(contact).update();
+                    this.addRosterContact(contact)
+                    this.update();
                     this.updateFilter();
                 },
 
                 onContactChange (contact) {
-                    this.updateChatBox(contact).update();
+                    this.updateChatBox(contact)
+                    this.update();
                     if (_.has(contact.changed, 'subscription')) {
                         if (contact.changed.subscription === 'from') {
                             this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
@@ -876,9 +884,6 @@
                     if (!chatbox) {
                         return this;
                     }
-                    if (_.has(contact.changed, 'chat_status')) {
-                        changes.chat_status = contact.get('chat_status');
-                    }
                     if (_.has(contact.changed, 'status')) {
                         changes.status = contact.get('status');
                     }

+ 1 - 1
src/converse-vcard.js

@@ -124,7 +124,7 @@
             /* Event handlers */
             _converse.initVCardCollection = function () {
                 _converse.vcards = new _converse.VCards();
-                _converse.vcards.browserStorage = new Backbone.BrowserStorage.local(b64_sha1(`converse.vcards`));
+                _converse.vcards.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1(`converse.vcards`));
                 _converse.vcards.fetch();
             }
             _converse.api.listen.on('connectionInitialized', _converse.initVCardCollection);

+ 1 - 1
src/templates/chatroom_head.html

@@ -1,4 +1,4 @@
-<div class="col col-9">
+<div class="col col-8">
     <div class="chat-title" title="{{{o.jid}}}">
         {[ if (o.name && o.name !== o.Strophe.getNodeFromJid(o.jid)) { ]}
             {{{ o.name }}}

+ 1 - 1
tests/utils.js

@@ -44,7 +44,7 @@
                 stanza.c('item', {'jid': item}).up();
             });
             _converse.connection._dataRecv(utils.createRequest(stanza));
-        });
+        }).catch(_.partial(console.error, _));
     }
 
     utils.createRequest = function (iq) {