Sfoglia il codice sorgente

Refactor views to use Backbone.OrderedListView

JC Brand 7 anni fa
parent
commit
ac643ae63f
8 ha cambiato i file con 174 aggiunte e 214 eliminazioni
  1. 30 28
      spec/chatbox.js
  2. 75 70
      spec/controlbox.js
  3. 22 12
      spec/protocol.js
  4. 1 0
      src/config.js
  5. 1 2
      src/converse-core.js
  6. 10 54
      src/converse-muc.js
  7. 34 48
      src/converse-rosterview.js
  8. 1 0
      tests/mock.js

+ 30 - 28
spec/chatbox.js

@@ -73,21 +73,25 @@
                 spyOn(_converse.chatboxviews, 'trimChats');
                 expect($("#conversejs .chatbox").length).toBe(1); // Controlbox is open
 
-                var online_contacts = _converse.rosterview.$el.find('.roster-group .current-xmpp-contact a.open-chat');
-                expect(online_contacts.length).toBe(15);
-                for (i=0; i<online_contacts.length; i++) {
-                    $el = $(online_contacts[i]);
-                    jid = $el.text().trim().replace(/ /g,'.').toLowerCase() + '@localhost';
-                    $el.click();
-                    chatboxview = _converse.chatboxviews.get(jid);
-                    expect(_converse.chatboxes.length).toEqual(i+2);
-                    expect(_converse.chatboxviews.trimChats).toHaveBeenCalled();
-                    // Check that new chat boxes are created to the left of the
-                    // controlbox (but to the right of all existing chat boxes)
-                    expect($("#conversejs .chatbox").length).toBe(i+2);
-                    expect($("#conversejs .chatbox")[1].id).toBe(chatboxview.model.get('box_id'));
-                }
-                done();
+                test_utils.waitUntil(function () {
+                    return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
+                    var online_contacts = _converse.rosterview.$el.find('.roster-group .current-xmpp-contact a.open-chat');
+                    expect(online_contacts.length).toBe(15);
+                    for (i=0; i<online_contacts.length; i++) {
+                        $el = $(online_contacts[i]);
+                        jid = $el.text().trim().replace(/ /g,'.').toLowerCase() + '@localhost';
+                        $el.click();
+                        chatboxview = _converse.chatboxviews.get(jid);
+                        expect(_converse.chatboxes.length).toEqual(i+2);
+                        expect(_converse.chatboxviews.trimChats).toHaveBeenCalled();
+                        // Check that new chat boxes are created to the left of the
+                        // controlbox (but to the right of all existing chat boxes)
+                        expect($("#conversejs .chatbox").length).toBe(i+2);
+                        expect($("#conversejs .chatbox")[1].id).toBe(chatboxview.model.get('box_id'));
+                    }
+                    done();
+                });
             }));
 
             it("can be trimmed to conserve space",
@@ -109,10 +113,10 @@
                 spyOn(trimmed_chatboxes, 'removeChat').and.callThrough();
                 expect($("#conversejs .chatbox").length).toBe(1); // Controlbox is open
 
-                _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced.
+                _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attached.
                 test_utils.waitUntil(function () {
-                    return _converse.rosterview.$el.find('.roster-group').length;
-                }, 300).then(function () {
+                    return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     // Test that they can be maximized again
                     var online_contacts = _converse.rosterview.$el.find('.roster-group .current-xmpp-contact a.open-chat');
                     expect(online_contacts.length).toBe(15);
@@ -836,10 +840,9 @@
                                 }).c('body').t('Message: '+i).up()
                                 .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
                         }
-                        test_utils.waitUntil(function () {
-                                return chatboxview.$content.scrollTop();
-                            }, 1000)
-                        .then(function () {
+                        return test_utils.waitUntil(function () {
+                            return chatboxview.$content.scrollTop();
+                        }, 1000).then(function () {
                             return test_utils.waitUntil(function () {
                                 return !chatboxview.model.get('auto_scrolled');
                             }, 500);
@@ -872,7 +875,7 @@
                             chatboxview.$content.scrollTop(chatboxview.$content[0].scrollHeight);
                             return test_utils.waitUntil(function () {
                                 return !chatboxview.$('.new-msgs-indicator').is(':visible');
-                            }, 500);
+                            }, 700);
                         }).then(done);
                     }));
 
@@ -1541,7 +1544,7 @@
                             view.model.maximize();
                             return test_utils.waitUntil(function () {
                                 return view.model.get('chat_state') === 'active';
-                            }, 500);
+                            }, 700);
                         }).then(function () {
                             expect(_converse.connection.send).toHaveBeenCalled();
                             var calls = _.filter(_converse.connection.send.calls.all(), function (call) {
@@ -1693,9 +1696,8 @@
                         test_utils.openControlBox();
                         test_utils.openContactsPanel(_converse);
                         test_utils.waitUntil(function () {
-                                return _converse.rosterview.$el.find('.roster-group').length;
-                            }, 300)
-                        .then(function () {
+                                return _converse.rosterview.$el.find('.roster-group li').length;
+                        }, 700).then(function () {
                             _converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test
 
                             contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
@@ -1715,7 +1717,7 @@
                             return test_utils.waitUntil(function () {
                                 return view.model.get('chat_state') === 'paused';
                             }, 500);
-                    }).then(function () {
+                        }).then(function () {
                             expect(_converse.connection.send).toHaveBeenCalled();
                             var calls = _.filter(_converse.connection.send.calls.all(), function (call) {
                                 return call.args[0] instanceof Strophe.Builder;

+ 75 - 70
spec/controlbox.js

@@ -182,14 +182,14 @@
 
                 var promise = test_utils.waitUntil(function () {
                     return $roster.find('li:visible').length === 15;
-                }, 500).then(function (contacts) {
+                }, 600).then(function (contacts) {
                     expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
                     $filter.val("candice");
                     $filter.trigger('keydown');
 
                     return test_utils.waitUntil(function () {
                         return $roster.find('li:visible').length === 1;
-                    }, 500);
+                    }, 600);
                 }).then(function (contacts) {
                     // Only one roster contact is now visible
                     expect($roster.find('li:visible').length).toBe(1);
@@ -203,7 +203,7 @@
                     $filter.trigger('keydown');
                     return test_utils.waitUntil(function () {
                         return $roster.find('li:visible').length === 5;
-                    }, 500)
+                    }, 600)
                 }).then(function (contacts) {
                     // Five roster contact is now visible
                     expect($roster.find('li:visible').length).toBe(5);
@@ -220,7 +220,7 @@
                     $filter.trigger('keydown');
                     return test_utils.waitUntil(function () {
                         return $roster.find('li:visible').length === 0;
-                    }, 500)
+                    }, 600)
                 }).then(function () {
                     expect($roster.find('ul.roster-group-contacts:visible a.group-toggle').length).toBe(0);
                     $filter = _converse.rosterview.$('.roster-filter');
@@ -228,7 +228,7 @@
                     $filter.trigger('keydown');
                     return test_utils.waitUntil(function () {
                         return $roster.find('li:visible').length === 15;
-                    }, 500)
+                    }, 600)
                 }).then(function () {
                     expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5);
                     _converse.roster_groups = false;
@@ -253,14 +253,14 @@
                 $type.val('groups');
                 test_utils.waitUntil(function () {
                     return $roster.find('li:visible').length === 15;
-                }, 500).then(function () {
+                }, 600).then(function () {
                     expect($roster.find('div.roster-group:visible a.group-toggle').length).toBe(5);
 
                     $filter.val("colleagues");
                     $filter.trigger('keydown');
                     return test_utils.waitUntil(function () {
                         return $roster.find('div.roster-group:not(.collapsed) a.group-toggle').length === 1;
-                    }, 500);
+                    }, 600);
                 }).then(function () {
                     expect(_.trim($roster.find('div.roster-group:not(.collapsed) a').eq(0).text())).toBe('colleagues');
                     expect($roster.find('div.roster-group:not(.collapsed) li:visible').length).toBe(3);
@@ -281,7 +281,7 @@
                     $filter.trigger('keydown');
                     return test_utils.waitUntil(function () {
                         return $roster.find('div.roster-group.collapsed a.group-toggle').length === 0;
-                    }, 500);
+                    }, 600);
                 }).then(function () {
                     expect($roster.find('div.roster-group:not(collapsed)').length).toBe(5);
                     expect($roster.find('div.roster-group:not(collapsed) li').length).toBe(15);
@@ -418,8 +418,8 @@
                     });
                 }
                 test_utils.waitUntil(function () {
-                    return _converse.rosterview.$el.find('li:visible').length;
-                }, 500).then(function () {
+                    return _converse.rosterview.$el.find('li:visible').length === 30;
+                }, 600).then(function () {
                     // Check that usernames appear alphabetically per group
                     _.each(groups, function (name) {
                         var $contacts = _converse.rosterview.$('.roster-group[data-group="'+name+'"] li');
@@ -528,7 +528,7 @@
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
                     return _converse.rosterview.$el.find('li').length;
-                }, 500).then(function () {
+                }, 700).then(function () {
                     expect(_converse.rosterview.$el.is(':visible')).toEqual(true);
                     expect(_converse.rosterview.update).toHaveBeenCalled();
                     expect(_converse.rosterview.$el.find('li:visible').length).toBe(3);
@@ -575,13 +575,12 @@
                 });
                 test_utils.waitUntil(function () {
                         return _converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')").length;
-                    }, 500)
-                .then(function () {
+                }, 700).then(function () {
                     _converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')")
                         .parent().siblings('.remove-xmpp-contact').click();
                     return test_utils.waitUntil(function () {
                         return _converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')").length === 0
-                    }, 500)
+                    }, 700)
                 }).then(function () {
                     expect(window.confirm).toHaveBeenCalled();
                     expect(_converse.connection.sendIQ).toHaveBeenCalled();
@@ -609,9 +608,9 @@
                     if (typeof callback === "function") { return callback(); }
                 });
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.get('Pending contacts').$el.is(':visible');
-                    }, 500)
-                .then(function () {
+                        var $pending_contacts = _converse.rosterview.get('Pending contacts').$el;
+                        return $pending_contacts.is(':visible') && $pending_contacts.find('li:visible').length;
+                }, 700).then(function () {
                     _converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')")
                         .parent().siblings('.remove-xmpp-contact').click();
                     expect(window.confirm).toHaveBeenCalled();
@@ -644,6 +643,7 @@
                     function (done, _converse) {
 
                 var i, t;
+                test_utils.openControlBox();
                 spyOn(_converse, 'emit');
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 for (i=0; i<mock.pend_names.length; i++) {
@@ -655,18 +655,25 @@
                     });
                     expect(_converse.rosterview.update).toHaveBeenCalled();
                 }
-                // Check that they are sorted alphabetically
-                t = _.reduce(_converse.rosterview.get('Pending contacts').$el.find('.pending-xmpp-contact span'), function (result, value) {
-                    return result + _.trim(value.textContent);
-                }, '');
-                expect(t).toEqual(mock.pend_names.slice(0,i+1).sort().join(''));
-                done();
+                return test_utils.waitUntil(function () {
+                    return _converse.rosterview.get('Pending contacts').$el.find('li:visible').length;
+                }, 700).then(function () {
+                    // Check that they are sorted alphabetically
+                    t = _.reduce(_converse.rosterview.get('Pending contacts').$el.find('.pending-xmpp-contact span'),
+                        function (result, value) {
+                            return result + _.trim(value.textContent);
+                        }, '');
+                    expect(t).toEqual(mock.pend_names.slice(0,i+1).sort().join(''));
+                    done();
+                });
             }));
         });
 
         describe("Existing Contacts", function () {
             var _addContacts = function (_converse) {
-                test_utils.createContacts(_converse, 'current').openControlBox().openContactsPanel(_converse);
+                test_utils.createContacts(_converse, 'current')
+                    .openControlBox()
+                    .openContactsPanel(_converse);
             };
 
             it("can be collapsed under their own header", 
@@ -717,6 +724,7 @@
                     null, ['rosterGroupsFetched'], {},
                     function (done, _converse) {
 
+                test_utils.openControlBox();
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 for (var i=0; i<mock.cur_names.length; i++) {
                     _converse.roster.create({
@@ -729,11 +737,13 @@
                 }
                 test_utils.waitUntil(function () {
                     return _converse.rosterview.$el.find('li').length;
-                }).then(function () {
+                }, 600).then(function () {
                     // Check that they are sorted alphabetically
-                    var t = _.reduce(_converse.rosterview.$('.roster-group').find('.current-xmpp-contact.offline a.open-chat'), function (result, value) {
-                        return result + _.trim(value.textContent);
-                    }, '');
+                    var t = _.reduce(_converse.rosterview.$('.roster-group')
+                             .find('.current-xmpp-contact.offline a.open-chat'),
+                        function (result, value) {
+                            return result + _.trim(value.textContent);
+                        }, '');
                     expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
                     done();
                 });
@@ -781,9 +791,8 @@
                     fullname: name
                 });
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
-                    }, 500)
-                .then(function () {
+                    return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     spyOn(window, 'confirm').and.returnValue(true);
                     spyOn(contact, 'removeFromRoster');
                     spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
@@ -808,9 +817,8 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
-                    }, 500)
-                .then(function () {
+                        return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     var jid, t;
                     spyOn(_converse, 'emit');
                     spyOn(_converse.rosterview, 'update').and.callThrough();
@@ -836,9 +844,8 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
-                    }, 500)
-                .then(function () {
+                    return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     var jid, t;
                     spyOn(_converse, 'emit');
                     spyOn(_converse.rosterview, 'update').and.callThrough();
@@ -865,9 +872,8 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
-                    }, 500)
-                .then(function () {
+                    return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     var jid, t;
                     spyOn(_converse, 'emit');
                     spyOn(_converse.rosterview, 'update').and.callThrough();
@@ -894,9 +900,8 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
-                    }, 500)
-                .then(function () {
+                        return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     var jid, t;
                     spyOn(_converse, 'emit');
                     spyOn(_converse.rosterview, 'update').and.callThrough();
@@ -923,7 +928,7 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
+                        return _converse.rosterview.$el.find('.roster-group li').length;
                     }, 500)
                 .then(function () {
                     var jid, t;
@@ -952,8 +957,8 @@
 
                 _addContacts(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
-                }, 500).then(function () {
+                    return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     var i, jid;
                     for (i=0; i<3; i++) {
                         jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
@@ -1071,13 +1076,17 @@
                         fullname: mock.req_names[i]
                     });
                 }
-                expect(_converse.rosterview.update).toHaveBeenCalled();
-                // Check that they are sorted alphabetically
-                children = _converse.rosterview.get('Contact requests').$el.find('.requesting-xmpp-contact span');
-                names = [];
-                children.each(addName);
-                expect(names.join('')).toEqual(mock.req_names.slice(0,mock.req_names.length+1).sort().join(''));
-                done();
+                test_utils.waitUntil(function () {
+                    return _converse.rosterview.get('Contact requests').$el.find('li').length;
+                }, 700).then(function () {
+                    expect(_converse.rosterview.update).toHaveBeenCalled();
+                    // Check that they are sorted alphabetically
+                    children = _converse.rosterview.get('Contact requests').$el.find('.requesting-xmpp-contact span');
+                    names = [];
+                    children.each(addName);
+                    expect(names.join('')).toEqual(mock.req_names.slice(0,mock.req_names.length+1).sort().join(''));
+                    done();
+                });
             }));
 
             it("do not have a header if there aren't any", 
@@ -1096,9 +1105,8 @@
                     fullname: name
                 });
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
-                    }, 500)
-                .then(function () {
+                        return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     expect(_converse.rosterview.get('Contact requests').$el.is(':visible')).toEqual(true);
                     _converse.rosterview.$el.find(".req-contact-name:contains('"+name+"')")
                         .parent().siblings('.request-actions')
@@ -1116,8 +1124,8 @@
 
                 test_utils.createContacts(_converse, 'requesting').openControlBox();
                 test_utils.waitUntil(function () {
-                    return _converse.rosterview.$el.find('.roster-group').length;
-                }, 500).then(function () {
+                    return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     checkHeaderToggling.apply(
                         _converse,
                         [_converse.rosterview.get('Contact requests').$el]
@@ -1132,9 +1140,8 @@
 
                 test_utils.createContacts(_converse, 'requesting').openControlBox();
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
-                    }, 500)
-                .then(function () {
+                    return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     // TODO: Testing can be more thorough here, the user is
                     // actually not accepted/authorized because of
                     // mock_connection.
@@ -1161,9 +1168,8 @@
 
                 test_utils.createContacts(_converse, 'requesting').openControlBox();
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
-                    }, 500)
-                .then(function () {
+                    return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced.
                     var name = mock.req_names.sort()[1];
                     var jid =  name.replace(/ /g,'.').toLowerCase() + '@localhost';
@@ -1193,7 +1199,7 @@
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
                 test_utils.waitUntil(function () {
                     return $('a:contains("Contact requests")').length;
-                }).then(function () {
+                }, 700).then(function () {
                     expect(_converse.roster.pluck('jid').length).toBe(1);
                     expect(_.includes(_converse.roster.pluck('jid'), 'data@enterprise')).toBeTruthy();
                     // Taken from the spec
@@ -1265,9 +1271,8 @@
                 test_utils.createContacts(_converse, 'all').openControlBox();
                 test_utils.openContactsPanel(_converse);
                 test_utils.waitUntil(function () {
-                        return _converse.rosterview.$el.find('.roster-group').length;
-                    }, 500)
-                .then(function () {
+                    return _converse.rosterview.$el.find('.roster-group li').length;
+                }, 700).then(function () {
                     var jid, name, i;
                     for (i=0; i<mock.cur_names.length; i++) {
                         name = mock.cur_names[i];
@@ -1324,8 +1329,8 @@
                 fullname: mock.pend_names[0]
             });
             test_utils.waitUntil(function () {
-                return _converse.rosterview.$el.find('.roster-group').length;
-            }, 500).then(function () {
+                return _converse.rosterview.$el.find('.roster-group li').length;
+            }, 700).then(function () {
                 // Checking that only one entry is created because both JID is same (Case sensitive check)
                 expect(_converse.rosterview.$el.find('li:visible').length).toBe(1);
                 expect(_converse.rosterview.update).toHaveBeenCalled();

+ 22 - 12
spec/protocol.js

@@ -228,11 +228,11 @@
                     expect(_converse.roster.updateContact).toHaveBeenCalled();
                     // Check that the user is now properly shown as a pending
                     // contact in the roster.
-            
-                    var $header = $('a:contains("Pending contacts")');
                     return test_utils.waitUntil(function () {
-                        return $('a:contains("Pending contacts")').length && $header.is(":visible");
-                    }, 300);
+                        var $header = $('a:contains("Pending contacts")');
+                        var $contacts = $header.parent().find('li');
+                        return $contacts.length;
+                    }, 600);
                 }).then(function () {
                     var $header = $('a:contains("Pending contacts")');
                     var $contacts = $header.parent().find('li');
@@ -297,10 +297,16 @@
 
                     // The contact should now be visible as an existing
                     // contact (but still offline).
-                    $header = $('a:contains("My contacts")');
+                    return test_utils.waitUntil(function () {
+                        var $header = $('a:contains("My contacts")');
+                        var $contacts = $header.parent().find('li');
+                        return $contacts.length;
+                    }, 600);
+                }).then(function () {
+                    var $header = $('a:contains("My contacts")');
                     expect($header.length).toBe(1);
                     expect($header.is(":visible")).toBeTruthy();
-                    $contacts = $header.parent().find('li');
+                    var $contacts = $header.parent().find('li');
                     expect($contacts.length).toBe(1);
                     // Check that it has the right classes and text
                     expect($contacts.hasClass('to')).toBeTruthy();
@@ -482,10 +488,12 @@
                     sent_IQ = iq;
                     IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
+                return test_utils.waitUntil(function () {
+                    var $header = $('a:contains("My contacts")');
+                    var $contacts = $header.parent().find('li');
+                    return $contacts.length;
+                }, 600).then(function () {
 
-                test_utils.waitUntil(function () {
-                    return $('a:contains("My contacts")').length;
-                }).then(function () {
                     var $header = $('a:contains("My contacts")');
                     // remove the first user
                     $($header.parent().find('li .remove-xmpp-contact').get(0)).click();
@@ -547,9 +555,11 @@
                     'xmlns': Strophe.NS.NICK,
                 }).t('Clint Contact');
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                test_utils.waitUntil(function () {
-                    return $('a:contains("Contact requests")').length;
-                }).then(function () {
+                return test_utils.waitUntil(function () {
+                    var $header = $('a:contains("Contact requests")');
+                    var $contacts = $header.parent().find('li');
+                    return $contacts.length;
+                }, 600).then(function () {
                     expect(_converse.emit).toHaveBeenCalledWith('contactRequest', jasmine.any(Object));
                     var $header = $('a:contains("Contact requests")');
                     expect($header.length).toBe(1);

+ 1 - 0
src/config.js

@@ -22,6 +22,7 @@ require.config({
         "backbone.browserStorage":  "node_modules/backbone.browserStorage/backbone.browserStorage",
         "backbone.noconflict":      "src/backbone.noconflict",
         "backbone.overview":        "node_modules/backbone.overview/backbone.overview",
+        "backbone.orderedlistview": "node_modules/backbone.overview/backbone.orderedlistview",
         "backbone.vdomview":        "node_modules/backbone.vdomview/backbone.vdomview",
         "emojione":                 "node_modules/emojione/lib/js/emojione",
         "es6-promise":              "node_modules/es6-promise/dist/es6-promise.auto",

+ 1 - 2
src/converse-core.js

@@ -16,8 +16,7 @@
             "strophe",
             "pluggable",
             "backbone.noconflict",
-            "backbone.browserStorage",
-            "backbone.overview",
+            "backbone.browserStorage"
     ], factory);
 }(this, function (sizzle, Promise, _, polyfill, i18n, utils, moment, Strophe, pluggable, Backbone) {
 

+ 10 - 54
src/converse-muc.js

@@ -37,6 +37,8 @@
             "awesomplete",
             "converse-chatview",
             "converse-disco",
+            "backbone.overview",
+            "backbone.orderedlistview",
             "backbone.vdomview"
     ], factory);
 }(this, function (
@@ -2213,16 +2215,17 @@
                 },
             });
 
-            _converse.ChatRoomOccupantsView = Backbone.Overview.extend({
+            _converse.ChatRoomOccupantsView = Backbone.OrderedListView.extend({
                 tagName: 'div',
                 className: 'occupants',
+                listItems: 'model',
+                sortEvent: 'change:role',
+                listSelector: '.occupant-list',
+
+                ItemView: _converse.ChatRoomOccupantView,
 
                 initialize () {
-                    this.model.on("add", this.onOccupantAdded, this);
-                    this.model.on("change:role", (occupant) => {
-                        this.model.sort();
-                        this.positionOccupant(occupant);
-                    });
+                    Backbone.OrderedListView.prototype.initialize.apply(this, arguments);
 
                     this.chatroomview = this.model.chatroomview;
                     this.chatroomview.model.on('change:open', this.renderInviteWidget, this);
@@ -2247,9 +2250,7 @@
                     this.model.fetch({
                         'add': true,
                         'silent': true,
-                        'success': () => {
-                            this.model.each(this.onOccupantAdded.bind(this));
-                        }
+                        'success': this.sortAndPositionAllItems.bind(this)
                     });
                 },
 
@@ -2353,57 +2354,12 @@
                     this.debouncedRenderRoomFeatures();
                 },
 
-
                 setOccupantsHeight () {
                     const el = this.el.querySelector('.chatroom-features');
                     this.el.querySelector('.occupant-list').style.cssText =
                         `height: calc(100% - ${el.offsetHeight}px - 5em);`;
                 },
 
-                positionOccupant (occupant) {
-                    /* Positions an occupant correctly in the list of
-                     * occupants.
-                     *
-                     * IMPORTANT: there's an important implicit assumption being
-                     * made here. And that is that initially this method gets called
-                     * for each occupant in the right positional order.
-                     *
-                     * In other words, it gets called for the 0th, then the
-                     * 1st, then the 2nd, 3rd and so on.
-                     *
-                     * That's why we call it in the "success" handler after
-                     * fetching the occupants, so that we know we have ALL of
-                     * them and that they're sorted.
-                     */
-                    const view = this.get(occupant.get('id'));
-                    view.render();
-                    const list = this.el.querySelector('.occupant-list');
-                    const index = this.model.indexOf(view.model);
-                    if (index === 0) {
-                        list.insertAdjacentElement('afterbegin', view.el);
-                    } else if (index === (this.model.length-1)) {
-                        list.insertAdjacentElement('beforeend', view.el);
-                    } else {
-                        const neighbour_el = list.querySelector('li:nth-child('+index+')');
-                        neighbour_el.insertAdjacentElement('afterend', view.el);
-                    }
-                    return view;
-                },
-
-                onOccupantAdded (item) {
-                    let view = this.get(item.get('id'));
-                    if (!view) {
-                        view = this.add(
-                            item.get('id'),
-                            new _converse.ChatRoomOccupantView({model: item})
-                        );
-                    } else {
-                        view.model = item;
-                        view.initialize();
-                    }
-                    this.positionOccupant(item);
-                },
-
                 parsePresence (pres) {
                     const id = Strophe.getResourceFromJid(pres.getAttribute("from"));
                     const data = {

+ 34 - 48
src/converse-rosterview.js

@@ -285,8 +285,12 @@
                     this.model.on("reset", this.reset, this);
                     _converse.on('rosterGroupsFetched', this.positionFetchedGroups, this);
                     _converse.on('rosterContactsFetched', () => {
-                        _converse.roster.each(this.onContactAdded.bind(this));
+                        _converse.roster.each((contact) => {
+                            this.addRosterContact(contact, {'silent': true});
+                        });
                         this.update();
+                        this.updateFilter();
+                        this.trigger('rosterContactsFetchedAndProcessed');
                     });
                     this.createRosterFilter();
                 },
@@ -474,11 +478,11 @@
                     return this.model.create({name, id: b64_sha1(name)});
                 },
 
-                addContactToGroup (contact, name) {
-                    this.getGroup(name).contacts.add(contact);
+                addContactToGroup (contact, name, options) {
+                    this.getGroup(name).contacts.add(contact, options);
                 },
 
-                addExistingContact (contact) {
+                addExistingContact (contact, options) {
                     let groups;
                     if (_converse.roster_groups) {
                         groups = contact.get('groups');
@@ -488,17 +492,17 @@
                     } else {
                         groups = [HEADER_CURRENT_CONTACTS];
                     }
-                    _.each(groups, _.bind(this.addContactToGroup, this, contact));
+                    _.each(groups, _.bind(this.addContactToGroup, this, contact, _, options));
                 },
 
-                addRosterContact (contact) {
+                addRosterContact (contact, options) {
                     if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') {
-                        this.addExistingContact(contact);
+                        this.addExistingContact(contact, options);
                     } else {
                         if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
-                            this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
+                            this.addContactToGroup(contact, HEADER_PENDING_CONTACTS, options);
                         } else if (contact.get('requesting') === true) {
-                            this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
+                            this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS, options);
                         }
                     }
                     return this;
@@ -670,22 +674,34 @@
             });
 
 
-            _converse.RosterGroupView = Backbone.Overview.extend({
+            _converse.RosterGroupView = Backbone.OrderedListView.extend({
                 tagName: 'div',
                 className: 'roster-group',
                 events: {
                     "click a.group-toggle": "toggle"
                 },
+                listItems: 'model.contacts',
+                sortEvent: 'change:chat_status',
+                listSelector: '.roster-group-contacts',
+
+                ItemView: _converse.RosterContactView,
 
                 initialize () {
-                    this.sortEventually = _.debounce(this.sortAndPositionAll, 500);
-                    this.model.contacts.on("add", this.onContactAdded, this);
+                    Backbone.OrderedListView.prototype.initialize.apply(this, arguments);
                     this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this);
                     this.model.contacts.on("change:requesting", this.onContactRequestChange, this);
-                    this.model.contacts.on("change:chat_status", this.sortEventually, this);
                     this.model.contacts.on("destroy", this.onRemove, this);
                     this.model.contacts.on("remove", this.onRemove, this);
                     _converse.roster.on('change:groups', this.onContactGroupChange, this);
+
+                    // This event gets triggered once *all* contacts (i.e. not
+                    // just this group's) have been fetched from browser
+                    // storage or the XMPP server and once they've been
+                    // assigned to their various groups.
+                    _converse.rosterview.on(
+                        'rosterContactsFetchedAndProcessed',
+                        this.sortAndPositionAllItems.bind(this)
+                    );
                 },
 
                 render () {
@@ -700,49 +716,19 @@
                     return this;
                 },
 
-                createContactView (contact) {
-                    const contact_view = new _converse.RosterContactView({model: contact});
-                    this.add(contact.get('id'), contact_view);
-                    contact_view.render();
-                    return contact_view;
-                },
-
-                onContactAdded (contact) {
-                    const contact_view = this.positionContact(contact);
+                createItemView (contact) {
+                    const contact_view =
+                        Backbone.OrderedListView.prototype.createItemView.apply(this, arguments);
                     if (contact_view.mayBeShown()) {
                         if (this.model.get('state') === _converse.CLOSED) {
                             u.hideElement(contact_view.el);
-                            u.showElement(this.el);
                         } else {
                             u.showElement(contact_view.el);
-                            u.showElement(this.el);
                         }
+                        u.showElement(this.el);
                     }
                 },
 
-                positionContact (contact) {
-                    /* Place the contact's DOM element in the correct alphabetical
-                     * position amongst the other contacts in this group.
-                     */
-                    const view = this.get(contact.get('id')) || this.createContactView(contact);
-                    const list = this.contacts_el;
-                    const index = this.model.contacts.indexOf(contact);
-                    if (index === 0) {
-                        list.insertAdjacentElement('afterbegin', view.el);
-                    } else if (index === (this.model.contacts.length-1)) {
-                        list.insertAdjacentElement('beforeend', view.el);
-                    } else {
-                        const neighbour_el = list.querySelector('li:nth-child('+index+')');
-                        neighbour_el.insertAdjacentElement('afterend', view.el);
-                    }
-                    return view;
-                },
-
-                sortAndPositionAll () {
-                    this.model.contacts.sort();
-                    this.model.contacts.each(this.positionContact.bind(this));
-                },
-
                 show () {
                     u.showElement(this.el);
                     _.each(this.getAll(), (contact_view) => {
@@ -851,7 +837,7 @@
                     if (in_this_group && !in_this_overview) {
                         this.model.contacts.remove(cid);
                     } else if (!in_this_group && in_this_overview) {
-                        this.onContactAdded(contact);
+                        this.items.trigger('add', contact);
                     }
                 },
 

+ 1 - 0
tests/mock.js

@@ -106,6 +106,7 @@
             'bosh_service_url': 'localhost',
             'connection': connection,
             'animate': false,
+            'use_emojione': false,
             'no_trimming': true,
             'auto_login': true,
             'jid': 'dummy@localhost',