2
0
Эх сурвалжийг харах

Merge branch 'master' into plugin-api

JC Brand 10 жил өмнө
parent
commit
36db4c8b27

+ 7 - 0
Gruntfile.js

@@ -99,10 +99,17 @@ module.exports = function(grunt) {
             done();
         };
         exec('./node_modules/requirejs/bin/r.js -o src/build.js && ' +
+             './node_modules/requirejs/bin/r.js -o src/build.js optimize=none out=builds/converse.js && ' +
+             './node_modules/requirejs/bin/r.js -o src/build-no-jquery.js &&' +
+             './node_modules/requirejs/bin/r.js -o src/build-no-jquery.js optimize=none out=builds/converse.nojquery.js && ' +
              './node_modules/requirejs/bin/r.js -o src/build-no-locales-no-otr.js && ' +
+             './node_modules/requirejs/bin/r.js -o src/build-no-locales-no-otr.js optimize=none out=builds/converse-no-locales-no-otr.js && ' +
              './node_modules/requirejs/bin/r.js -o src/build-no-otr.js &&' +
+             './node_modules/requirejs/bin/r.js -o src/build-no-otr.js optimize=none out=builds/converse-no-otr.js && ' +
              './node_modules/requirejs/bin/r.js -o src/build-website-no-otr.js &&' +
              './node_modules/requirejs/bin/r.js -o src/build-website.js', callback);
+        // XXX: It might be possible to not have separate build config files. For example:
+        // 'r.js -o src/build.js paths.converse-dependencies=src/deps-no-otr paths.locales=locale/nolocales out=builds/converse-no-locales-no-otr.min.js'
     });
 
     grunt.registerTask('minify', 'Create a new release', ['cssmin', 'jsmin']);

+ 1 - 2
bower.json

@@ -16,7 +16,6 @@
     "backbone.browserStorage": "*",
     "backbone.overview": "*",
     "strophe": "~1.1.3",
-    "strophe.roster": "https://raw.github.com/strophe/strophejs-plugins/b1f364eb6e854ffe844c57add38e885cfeb9b498/roster/strophe.roster.js",
     "strophe.muc": "https://raw.githubusercontent.com/strophe/strophejs-plugins/master/muc/strophe.muc.js",
     "otr": "0.2.12",
     "crypto-js-evanvosberg": "~3.1.2",
@@ -30,7 +29,7 @@
     "bootstrapJS": "https://raw.githubusercontent.com/jcbrand/bootstrap/7d96a5f60d26c67b5348b270a775518b96a702c8/dist/js/bootstrap.js",
     "fontawesome": "~4.1.0",
     "typeahead.js": "https://raw.githubusercontent.com/jcbrand/typeahead.js/eedfb10505dd3a20123d1fafc07c1352d83f0ab3/dist/typeahead.jquery.js",
-    "strophejs-plugins": "~0.0.4"
+    "strophejs-plugins": "https://github.com/strophe/strophejs-plugins.git#a56421ff4ecf0807113ab48c46728715597df599"
   },
   "exportsOverride": {}
 }

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 13947 - 0
builds/converse-no-locales-no-otr.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
builds/converse-no-locales-no-otr.min.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 13947 - 0
builds/converse-no-otr.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
builds/converse-no-otr.min.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 21666 - 0
builds/converse.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
builds/converse.min.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 11332 - 0
builds/converse.nojquery.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 203 - 0
builds/converse.nojquery.min.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
builds/converse.website-no-otr.min.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
builds/converse.website.min.js


+ 6 - 6
builds/templates.js

@@ -42,7 +42,7 @@ __p += '<li>\n    <form class="add-xmpp-contact">\n        <input type="text"\n
 ((__t = (label_contact_username)) == null ? '' : __t) +
 '"/>\n        <button type="submit">' +
 ((__t = (label_add)) == null ? '' : __t) +
-'</button>\n    </form>\n<li>\n';
+'</button>\n    </form>\n</li>\n';
 
 }
 return __p
@@ -380,7 +380,7 @@ __p += '<form id="converse-login" method="post">\n    <label>' +
 ((__t = (label_password)) == null ? '' : __t) +
 '</label>\n    <input type="password" name="password" placeholder="Password">\n    <input class="login-submit" type="submit" value="' +
 ((__t = (label_login)) == null ? '' : __t) +
-'">\n    <span class="conn-feedback"></span>\n</form">\n';
+'">\n    <span class="conn-feedback"></span>\n</form>\n';
 
 }
 return __p
@@ -660,13 +660,13 @@ this["JST"]["roster"] = function(obj) {
 obj || (obj = {});
 var __t, __p = '', __e = _.escape;
 with (obj) {
-__p += '<input class="roster-filter" placeholder="' +
+__p += '<input style="display: none;" class="roster-filter" placeholder="' +
 ((__t = (placeholder)) == null ? '' : __t) +
-'">\n<select class="filter-type">\n    <option value="contacts">' +
+'">\n<select style="display: none;" class="filter-type">\n    <option value="contacts">' +
 ((__t = (label_contacts)) == null ? '' : __t) +
 '</option>\n    <option value="groups">' +
 ((__t = (label_groups)) == null ? '' : __t) +
-'</option>\n</select>\n<dl class="roster-contacts"></dl>\n';
+'</option>\n</select>\n';
 
 }
 return __p
@@ -700,7 +700,7 @@ __p += '<li>\n    <form class="search-xmpp-contact">\n        <input type="text"
 ((__t = (label_contact_name)) == null ? '' : __t) +
 '"/>\n        <button type="submit">' +
 ((__t = (label_search)) == null ? '' : __t) +
-'</button>\n    </form>\n<li>\n';
+'</button>\n    </form>\n</li>\n';
 
 }
 return __p

+ 152 - 68
converse.js

@@ -300,7 +300,7 @@
 
         // Translation machinery
         // ---------------------
-        var __ = utils.__;
+        var __ = $.proxy(utils.__, this);
         var ___ = utils.___;
         // Translation aware constants
         // ---------------------------
@@ -541,6 +541,7 @@
         };
 
         this.clearSession = function () {
+            this.roster.browserStorage._clear();
             this.session.browserStorage._clear();
             // XXX: this should perhaps go into the beforeunload handler
             converse.chatboxes.get('controlbox').save({'connected': false});
@@ -638,7 +639,9 @@
             if (this.debug) {
                 this.connection.xmlInput = function (body) { console.log(body); };
                 this.connection.xmlOutput = function (body) { console.log(body); };
-                Strophe.log = function (level, msg) { console.log(level+' '+msg); };
+                Strophe.log = function (level, msg) {
+                    console.log(level+' '+msg);
+                };
                 Strophe.error = function (msg) {
                     console.log('ERROR: '+msg);
                 };
@@ -906,6 +909,9 @@
 
                 if (!body) {
                     if (composing.length || paused.length) {
+                        // FIXME: use one attribute for chat states (e.g.
+                        // chatstate) instead of saving 'paused' and
+                        // 'composing' separately.
                         this.messages.add({
                             fullname: fullname,
                             sender: 'them',
@@ -1597,7 +1603,7 @@
                     label_away: __('Away'),
                     label_offline: __('Offline'),
                     label_logout: __('Log out'),
-                    allow_logout: converse.allow_logout,
+                    allow_logout: converse.allow_logout
                 });
                 this.$tabs.append(converse.templates.contacts_tab({label_contacts: LABEL_CONTACTS}));
                 if (converse.xhr_user_search) {
@@ -1933,8 +1939,16 @@
                     b64_sha1('converse.roster.groups'+converse.bare_jid));
                 converse.rosterview = new converse.RosterView({model: rostergroups});
                 this.contactspanel.$el.append(converse.rosterview.$el);
+                // TODO:
+                // See if we shouldn't also fetch the roster here... otherwise
+                // the roster is always populated by the rosterHandler method,
+                // which appears to be a less economic way.
+                // i.e. from what it seems, only groups are fetched from
+                // browserStorage, and no contacts.
+                // XXX: Make sure that if fetch is called, we don't sort on
+                // each item add...
+                // converse.roster.fetch()
                 converse.rosterview.render().fetch().update();
-                converse.connection.roster.get(function () {});
                 return this;
             },
 
@@ -2079,7 +2093,7 @@
             initialize: function (options) {
                 this.browserStorage = new Backbone.BrowserStorage[converse.storage](
                     b64_sha1('converse.occupants'+converse.bare_jid+options.nick));
-            },
+            }
         });
 
         this.ChatRoomOccupantsView = Backbone.Overview.extend({
@@ -2169,7 +2183,7 @@
                     $(ev.target).typeahead('val', '');
                 }, this));
                 return this;
-            },
+            }
 
         });
 
@@ -2210,7 +2224,7 @@
                 this);
 
                 this.occupantsview = new converse.ChatRoomOccupantsView({
-                    model: new converse.ChatRoomOccupants({nick: this.model.get('nick')}),
+                    model: new converse.ChatRoomOccupants({nick: this.model.get('nick')})
                 });
                 this.occupantsview.chatroomview = this;
                 this.render();
@@ -2243,7 +2257,7 @@
                         .append(
                             converse.templates.chatarea({
                                 'show_toolbar': converse.show_toolbar,
-                                'label_message': __('Message'),
+                                'label_message': __('Message')
                             }))
                         .append(this.occupantsview.render().$el);
                     this.renderToolbar();
@@ -2556,7 +2570,7 @@
                 172: __('This room is now non-anonymous'),
                 173: __('This room is now semi-anonymous'),
                 174: __('This room is now fully-anonymous'),
-                201: __('A new room has been created'),
+                201: __('A new room has been created')
             },
 
             disconnectMessages: {
@@ -2710,7 +2724,7 @@
                     delayed = $message.find('delay').length > 0,
                     subject = $message.children('subject').text();
 
-                if (this.model.messages.findWhere({msgid: msgid})) {
+                if (msgid && this.model.messages.findWhere({msgid: msgid})) {
                     return true; // We already have this message stored.
                 }
                 this.showStatusMessages($message);
@@ -2829,6 +2843,8 @@
             onMessage: function (message) {
                 var $message = $(message);
                 var buddy_jid, $forwarded, $received,
+                    msgid = $message.attr('id'),
+                    chatbox, resource, roster_item,
                     message_from = $message.attr('from');
                 if (message_from === converse.connection.jid) {
                     // FIXME: Forwarded messages should be sent to specific resources,
@@ -2844,8 +2860,7 @@
                     message_from = $message.attr('from');
                 }
                 var from = Strophe.getBareJidFromJid(message_from),
-                    to = Strophe.getBareJidFromJid($message.attr('to')),
-                    resource, chatbox, roster_item;
+                    to = Strophe.getBareJidFromJid($message.attr('to'));
                 if (from == converse.bare_jid) {
                     // I am the sender, so this must be a forwarded message...
                     buddy_jid = to;
@@ -2854,15 +2869,15 @@
                     buddy_jid = from;
                     resource = Strophe.getResourceFromJid(message_from);
                 }
-                chatbox = this.get(buddy_jid);
-                roster_item = converse.roster.get(buddy_jid);
 
+                roster_item = converse.roster.get(buddy_jid);
                 if (roster_item === undefined) {
                     // The buddy was likely removed
                     converse.log('Could not get roster item for JID '+buddy_jid, 'error');
                     return true;
                 }
 
+                chatbox = this.get(buddy_jid);
                 if (!chatbox) {
                     var fullname = roster_item.get('fullname');
                     fullname = _.isEmpty(fullname)? buddy_jid: fullname;
@@ -2875,6 +2890,16 @@
                         'url': roster_item.get('url')
                     });
                 }
+                if (msgid && chatbox.messages.findWhere({msgid: msgid})) {
+                    // FIXME: There's still a bug here..
+                    // If a duplicate message is received just after the chat
+                    // box was closed, then it'll open again (due to it being
+                    // created here above), with now new messages.
+                    // The solution is mostly likely to not let chat boxes show
+                    // automatically when they are created, but to require
+                    // "show" to be called explicitly.
+                    return true; // We already have this message stored.
+                }
                 if (!this.isOnlyChatStateNotification($message) && from !== converse.bare_jid) {
                     playNotification();
                 }
@@ -3032,10 +3057,14 @@
             },
 
             initialize: function () {
-                this.model.messages.on('add', this.updateUnreadMessagesCounter, this);
-                this.model.on('showSentOTRMessage', this.updateUnreadMessagesCounter, this);
-                this.model.on('showReceivedOTRMessage', this.updateUnreadMessagesCounter, this);
+                this.model.messages.on('add', function (m) {
+                    if (!(m.get('composing') || m.get('paused'))) {
+                        this.updateUnreadMessagesCounter();
+                    }
+                }, this);
                 this.model.on('change:minimized', this.clearUnreadMessagesCounter, this);
+                this.model.on('showReceivedOTRMessage', this.updateUnreadMessagesCounter, this);
+                this.model.on('showSentOTRMessage', this.updateUnreadMessagesCounter, this);
             },
 
             render: function () {
@@ -3168,7 +3197,7 @@
                 this.set({
                     'collapsed': this.get('collapsed') || false,
                     'num_minimized': this.get('num_minimized') || 0,
-                    'num_unread':  this.get('num_unread') || 0,
+                    'num_unread':  this.get('num_unread') || 0
                 });
             }
         });
@@ -3194,7 +3223,7 @@
                     this.$flyout.show();
                 }
                 return this.$el;
-            },
+            }
         });
 
         this.RosterContact = Backbone.Model.extend({
@@ -3210,6 +3239,10 @@
                     'status': ''
                 }, attributes);
                 this.set(attrs);
+            },
+
+            showInRoster: function () {
+                return (!converse.show_only_online_users || this.get('chat_status') === 'online');
             }
         });
 
@@ -3224,24 +3257,12 @@
             },
 
             initialize: function () {
-                this.model.on("change", this.onChange, this);
+                this.model.on("change", this.render, this);
                 this.model.on("remove", this.remove, this);
                 this.model.on("destroy", this.remove, this);
                 this.model.on("open", this.openChat, this);
             },
 
-            onChange: function () {
-                if (converse.show_only_online_users) {
-                    if (this.model.get('chat_status') !== 'online') {
-                        this.$el.hide();
-                    } else {
-                        this.$el.show();
-                    }
-                } else {
-                    this.render();
-                }
-            },
-
             openChat: function (ev) {
                 if (ev && ev.preventDefault) { ev.preventDefault(); }
                 // XXX: Can this.model.attributes be used here, instead of
@@ -3291,6 +3312,12 @@
             },
 
             render: function () {
+                if (!this.model.showInRoster()) {
+                    this.$el.hide();
+                    return this;
+                } else if (this.$el[0].style.display === "none") {
+                    this.$el.show();
+                }
                 var item = this.model,
                     ask = item.get('ask'),
                     chat_status = item.get('chat_status'),
@@ -3334,7 +3361,7 @@
                     this.$el.html(converse.templates.requesting_contact(
                         _.extend(item.toJSON(), {
                             'desc_accept': __("Click to accept this contact request"),
-                            'desc_decline': __("Click to decline this contact request"),
+                            'desc_decline': __("Click to decline this contact request")
                         })
                     ));
                     converse.controlboxtoggle.showControlBox();
@@ -3354,13 +3381,13 @@
 
         this.RosterContacts = Backbone.Collection.extend({
             model: converse.RosterContact,
-
             comparator: function (contact1, contact2) {
-                var name1 = contact1.get('fullname').toLowerCase();
+                var name1, name2;
                 var status1 = contact1.get('chat_status') || 'offline';
-                var name2 = contact2.get('fullname').toLowerCase();
                 var status2 = contact2.get('chat_status') || 'offline';
                 if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) {
+                    name1 = contact1.get('fullname').toLowerCase();
+                    name2 = contact2.get('fullname').toLowerCase();
                     return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
                 } else  {
                     return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1;
@@ -3368,15 +3395,13 @@
             },
 
             subscribeToSuggestedItems: function (msg) {
-                $(msg).find('item').each(function () {
+                $(msg).find('item').each(function (i, items) {
                     var $this = $(this),
                         jid = $this.attr('jid'),
                         action = $this.attr('action'),
                         fullname = $this.attr('name');
                     if (action === 'add') {
-                        converse.connection.roster.add(jid, fullname, [], function (iq) {
-                            converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
-                        });
+                        converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
                     }
                 });
                 return true;
@@ -3473,17 +3498,19 @@
                     id = this.models[i].get('id');
                     if (_.indexOf(_.pluck(items, 'jid'), id) === -1) {
                         contact = this.get(id);
-                        if (contact) {
+                        if (contact && !contact.get('requesting')) {
                             contact.destroy();
                         }
                     }
                 }
             },
 
-            rosterHandler: function (items) {
+            // TODO: see if we can only use 2nd item par
+            rosterHandler: function (items, item) {
                 converse.emit('roster', items);
                 this.clearCache(items);
-                _.each(items, function (item, index, items) {
+                var new_items = item ? [item] : items;
+                _.each(new_items, function (item, index, items) {
                     if (this.isSelf(item.jid)) { return; }
                     var model = this.get(item.jid);
                     if (!model) {
@@ -3500,7 +3527,7 @@
                             groups: item.groups,
                             jid: item.jid,
                             subscription: item.subscription
-                        });
+                        }, {sort: false});
                     } else {
                         if ((item.subscription === 'none') && (item.ask === null)) {
                             // This user is no longer in our roster
@@ -3683,11 +3710,13 @@
                 var view = new converse.RosterContactView({model: contact});
                 this.add(contact.get('id'), view);
                 view = this.positionContact(contact).render();
-                if (this.model.get('state') === CLOSED) {
-                    view.$el.hide();
-                    this.$el.show();
-                } else {
-                    this.show();
+                if (contact.showInRoster()) {
+                    if (this.model.get('state') === CLOSED) {
+                        if (view.$el[0].style.display !== "none") { view.$el.hide(); }
+                        if (this.$el[0].style.display === "none") { this.$el.show(); }
+                    } else {
+                        if (this.$el[0].style.display !== "block") { this.show(); }
+                    }
                 }
             },
 
@@ -3709,6 +3738,9 @@
             },
 
             show: function () {
+                // FIXME: There's a bug here, if show_only_online_users is true
+                // Possible solution, get the group, call _.each and check
+                // showInRoster
                 this.$el.nextUntil('dt').addBack().show();
             },
 
@@ -3726,7 +3758,7 @@
                 if (q.length === 0) {
                     if (this.model.get('state') === OPENED) {
                         this.model.contacts.each($.proxy(function (item) {
-                            if (!(converse.show_only_online_users && item.get('chat_status') === 'online')) {
+                            if (item.showInRoster()) {
                                 this.get(item.get('id')).$el.show();
                             }
                         }, this));
@@ -3846,16 +3878,20 @@
                 converse.roster.on("remove", this.update, this);
                 this.model.on("add", this.onGroupAdd, this);
                 this.model.on("reset", this.reset, this);
+                this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
             },
 
-            update: function () {
+            update: _.debounce(function () {
                 var $count = $('#online-count');
                 $count.text('('+converse.roster.getNumOnlineContacts()+')');
                 if (!$count.is(':visible')) {
                     $count.show();
                 }
+                if (this.$roster.parent().length === 0) {
+                    this.$el.append(this.$roster.show());
+                }
                 return this.showHideFilter();
-            },
+            }, converse.animate ? 100 : 0),
 
             render: function () {
                 this.$el.html(converse.templates.roster({
@@ -3868,10 +3904,47 @@
 
             fetch: function () {
                 this.model.fetch({
-                    silent: true,
-                    success: $.proxy(this.positionFetchedGroups, this)
+                    silent: true, // We use the success handler to handle groups that were added,
+                                  // we need to first have all groups before positionFetchedGroups
+                                  // will work properly.
+                    success: $.proxy(function (collection, resp, options) {
+                        if (collection.length !== 0) {
+                            this.positionFetchedGroups(collection, resp, options);
+                        }
+                        converse.roster.fetch({
+                            add: true,
+                            success: function (collection) {
+                                // XXX: Bit of a hack.
+                                // strophe.roster expects .get to be called for
+                                // every page load so that its "items" attr
+                                // gets populated.
+                                // This is very inefficient for large rosters,
+                                // and we already have the roster cached in
+                                // sessionStorage.
+                                // Therefore we manually populate the "items"
+                                // attr.
+                                // Ideally we should eventually replace
+                                // strophe.roster with something better.
+                                if (collection.length > 0) {
+                                    collection.each(function (item) {
+                                        converse.connection.roster.items.push({
+                                            name         : item.get('fullname'),
+                                            jid          : item.get('jid'),
+                                            subscription : item.get('subscription'),
+                                            ask          : item.get('ask'),
+                                            groups       : item.get('groups'),
+                                            resources    : item.get('resources')
+                                        });
+                                    });
+                                    converse.initial_presence_sent = 1;
+                                    converse.xmppstatus.sendPresence();
+                                } else {
+                                    converse.connection.roster.get();
+                                }
+                            }
+                        });
+                    }, this)
                 });
-                converse.roster.fetch({add: true});
                 return this;
             },
 
@@ -3939,7 +4012,7 @@
                     // Don't hide if user is currently filtering.
                     return;
                 }
-                if (this.$('.roster-contacts').hasScrollBar()) {
+                if (this.$roster.hasScrollBar()) {
                     if (!visible) {
                         $filter.show();
                         $type.show();
@@ -3954,6 +4027,7 @@
             reset: function () {
                 converse.roster.reset();
                 this.removeAll();
+                this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
                 this.render().update();
                 return this;
             },
@@ -3961,13 +4035,24 @@
             registerRosterHandler: function () {
                 // Register handlers that depend on the roster
                 converse.connection.roster.registerCallback(
-                    $.proxy(converse.roster.rosterHandler, converse.roster),
-                    null, 'presence', null);
+                    $.proxy(converse.roster.rosterHandler, converse.roster)
+                );
             },
 
             registerRosterXHandler: function () {
+                var t = 0;
                 converse.connection.addHandler(
-                    $.proxy(converse.roster.subscribeToSuggestedItems, converse.roster),
+                    function (msg) {
+                        window.setTimeout(
+                            function () {
+                                converse.connection.flush();
+                                $.proxy(converse.roster.subscribeToSuggestedItems, converse.roster)(msg);
+                            },
+                            t
+                        );
+                        t += $(msg).find('item').length*250;
+                        return true;
+                    },
                     'http://jabber.org/protocol/rosterx', 'message', null);
             },
 
@@ -4044,7 +4129,7 @@
                         this.add(group.get('name'), view.render());
                     }
                     if (idx === 0) {
-                        this.$('.roster-contacts').append(view.$el);
+                        this.$roster.append(view.$el);
                     } else {
                         this.appendGroup(view);
                     }
@@ -4055,13 +4140,14 @@
                 /* Place the group's DOM element in the correct alphabetical
                  * position amongst the other groups in the roster.
                  */
-                var index = this.model.indexOf(view.model);
+                var $groups = this.$roster.find('.roster-group'),
+                    index = $groups.length ? this.model.indexOf(view.model) : 0;
                 if (index === 0) {
-                    this.$('.roster-contacts').prepend(view.$el);
+                    this.$roster.prepend(view.$el);
                 } else if (index == (this.model.length-1)) {
                     this.appendGroup(view);
                 } else {
-                    $(this.$('.roster-group').eq(index)).before(view.$el);
+                    $($groups.eq(index)).before(view.$el);
                 }
                 return this;
             },
@@ -4069,7 +4155,7 @@
             appendGroup: function (view) {
                 /* Add the group at the bottom of the roster
                  */
-                var $last = this.$('.roster-group').last();
+                var $last = this.$roster.find('.roster-group').last();
                 var $siblings = $last.siblings('dd');
                 if ($siblings.length > 0) {
                     $siblings.last().after(view.$el);
@@ -4351,7 +4437,7 @@
                  converse.connection.disco.addFeature('http://jabber.org/protocol/rosterx'); // Limited support
                  converse.connection.disco.addFeature('jabber:x:conference');
                  converse.connection.disco.addFeature('urn:xmpp:carbons:2');
-                 converse.connection.disco.addFeature('vcard-temp');
+                 converse.connection.disco.addFeature(Strophe.NS.VCARD);
                  converse.connection.disco.addFeature(Strophe.NS.BOSH);
                  converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
                  converse.connection.disco.addFeature(Strophe.NS.MUC);
@@ -4560,8 +4646,6 @@
                     sid = this.session.get('sid');
                     jid = this.session.get('jid');
                     if (rid && jid && sid) {
-                        // We have the necessary tokens for resuming a session
-                        rid += 1;
                         this.session.save({rid: rid}); // The RID needs to be increased with each request.
                         this.connection.attach(jid, sid, rid, this.onConnect);
                     } else if (this.prebind) {
@@ -4701,6 +4785,6 @@
         },
         'registerPlugin': function (name, callback) {
             converse.plugins[name] = callback;
-        },
+        }
     };
 }));

+ 10 - 1
docs/CHANGES.rst

@@ -1,14 +1,23 @@
 Changelog
 =========
 
+
 0.8.4 (Unreleased)
 ------------------
 
+.. note::
+    The current API methods will be deprecated in future bugfix releases and
+    a new API will be made available for the 0.9.0 release.
+
 * Bugfix. Error when trying to use prebind and keepalive together. [heban and jcbrand]
 * Bugfix. Cannot read property "top" of undefined. [jcbrand]
 * Add new event, noResumeableSession, for when keepalive=true and there aren't
   any prebind session tokens. [jcbrand]
-* Add 2 new API methods, getChatBox and openChatBox. [jcbrand]
+* #46 Add 2 new API methods, getChatBox and openChatBox. [jcbrand]
+* #151 Browser locks/freezes with many roster users. [jcbrand]
+* #251 Non-minified builds for debugging. [jcbrand]
+* #264 Remove unnecessary commas for ie8 compatibility. [Deuteu]
+* #267 Unread messages counter wrongly gets incremented by chat state notifications. [Deuteu]
 
 0.8.3 (2014-09-22)
 ------------------

+ 12 - 0
docs/source/index.rst

@@ -808,8 +808,20 @@ After adding the string, you'll need to regenerate the POT file, like so:
 
     make pot
 
+To create a new PO file for a language in which converse.js is not yet
+translated into, do the following
+
+.. note:: In this example we use Polish (pl), you need to substitute 'pl' to your own language's code.
+
+::
+
+    mkdir -p ./locale/pl/LC_MESSAGES
+    msginit -i ./locale/converse.pot -o ./locale/pl/LC_MESSAGES/converse.po -l pl
+
 You can then create or update the PO file for a specific language by doing the following:
 
+.. note:: In this example we use German (de), you need to substitute 'de' to your own language's code.
+
 ::
 
     msgmerge ./locale/de/LC_MESSAGES/converse.po ./locale/converse.pot -U

+ 7 - 5
index.html

@@ -229,7 +229,6 @@
 <script type="text/javascript">try { var pageTracker = _gat._getTracker("UA-2128260-8"); pageTracker._trackPageview(); } catch(err) {}</script>
 
 <script>
-    // Configuration loaded, so safe to make other require calls.
     require(['converse'], function (converse) {
         (function () {
             /* XXX: This function initializes jquery.easing for the https://conversejs.org
@@ -255,14 +254,17 @@
         })();
 
         converse.initialize({
+            allow_otr: true,
+            auto_list_rooms: false,
+            auto_subscribe: false,
             bosh_service_url: 'https://bind.conversejs.org', // Please use this connection manager only for testing purposes
-            i18n: locales.en, // Refer to ./locale/locales.js to see which locales are supported
+            hide_muc_server: false,
+            i18n: locales['en'], // Refer to ./locale/locales.js to see which locales are supported
             keepalive: true,
             play_sounds: true,
-            prebind: false,
+            roster_groups: true,
             show_controlbox_by_default: true,
-            debug: true,
-            roster_groups: true
+            xhr_user_search: false
         });
     });
 </script>

+ 183 - 183
locale/converse.pot

@@ -6,9 +6,9 @@
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: Converse.js 0.7.0\n"
+"Project-Id-Version: Converse.js 0.8.3\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2014-09-22 18:14+0200\n"
+"POT-Creation-Date: 2014-10-21 13:12+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,123 +17,123 @@ msgstr ""
 "Content-Type: text/plain; charset=CHARSET\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: converse.js:338
+#: converse.js:314
 msgid "unencrypted"
 msgstr ""
 
-#: converse.js:339
+#: converse.js:315
 msgid "unverified"
 msgstr ""
 
-#: converse.js:340
+#: converse.js:316
 msgid "verified"
 msgstr ""
 
-#: converse.js:341
+#: converse.js:317
 msgid "finished"
 msgstr ""
 
-#: converse.js:344
+#: converse.js:320
 msgid "This contact is busy"
 msgstr ""
 
-#: converse.js:345
+#: converse.js:321
 msgid "This contact is online"
 msgstr ""
 
-#: converse.js:346
+#: converse.js:322
 msgid "This contact is offline"
 msgstr ""
 
-#: converse.js:347
+#: converse.js:323
 msgid "This contact is unavailable"
 msgstr ""
 
-#: converse.js:348
+#: converse.js:324
 msgid "This contact is away for an extended period"
 msgstr ""
 
-#: converse.js:349
+#: converse.js:325
 msgid "This contact is away"
 msgstr ""
 
-#: converse.js:351
+#: converse.js:327
 msgid "Click to hide these contacts"
 msgstr ""
 
-#: converse.js:353
+#: converse.js:329
 msgid "My contacts"
 msgstr ""
 
-#: converse.js:354
+#: converse.js:330
 msgid "Pending contacts"
 msgstr ""
 
-#: converse.js:355
+#: converse.js:331
 msgid "Contact requests"
 msgstr ""
 
-#: converse.js:356
+#: converse.js:332
 msgid "Ungrouped"
 msgstr ""
 
-#: converse.js:358
+#: converse.js:334
 msgid "Contacts"
 msgstr ""
 
-#: converse.js:359
+#: converse.js:335
 msgid "Groups"
 msgstr ""
 
-#: converse.js:441
+#: converse.js:417
 msgid "Reconnecting"
 msgstr ""
 
-#: converse.js:476
+#: converse.js:452
 msgid "Disconnected"
 msgstr ""
 
-#: converse.js:484
+#: converse.js:460
 msgid "Error"
 msgstr ""
 
-#: converse.js:486
+#: converse.js:462
 msgid "Connecting"
 msgstr ""
 
-#: converse.js:489
+#: converse.js:465
 msgid "Connection Failed"
 msgstr ""
 
-#: converse.js:491
+#: converse.js:467
 msgid "Authenticating"
 msgstr ""
 
-#: converse.js:494
+#: converse.js:470
 msgid "Authentication Failed"
 msgstr ""
 
-#: converse.js:499
+#: converse.js:475
 msgid "Disconnecting"
 msgstr ""
 
-#: converse.js:638 converse.js:684
+#: converse.js:614 converse.js:660
 msgid "Online Contacts"
 msgstr ""
 
-#: converse.js:802
+#: converse.js:778
 msgid "Re-establishing encrypted session"
 msgstr ""
 
-#: converse.js:814
+#: converse.js:790
 msgid "Generating private key."
 msgstr ""
 
-#: converse.js:815
+#: converse.js:791
 msgid "Your browser might become unresponsive."
 msgstr ""
 
-#: converse.js:850
+#: converse.js:826
 msgid ""
 "Authentication request from %1$s\n"
 "\n"
@@ -143,67 +143,67 @@ msgid ""
 "%2$s"
 msgstr ""
 
-#: converse.js:859
+#: converse.js:835
 msgid "Could not verify this user's identify."
 msgstr ""
 
-#: converse.js:898
+#: converse.js:874
 msgid "Exchanging private key with buddy."
 msgstr ""
 
-#: converse.js:1045
+#: converse.js:1023
 msgid "Personal message"
 msgstr ""
 
-#: converse.js:1077
+#: converse.js:1055
 msgid "Are you sure you want to clear the messages from this room?"
 msgstr ""
 
-#: converse.js:1099
+#: converse.js:1077
 msgid "me"
 msgstr ""
 
-#: converse.js:1154
+#: converse.js:1131
 msgid "is typing"
 msgstr ""
 
-#: converse.js:1157
+#: converse.js:1134
 msgid "has stopped typing"
 msgstr ""
 
-#: converse.js:1199 converse.js:2331
+#: converse.js:1176 converse.js:2314
 msgid "Show this menu"
 msgstr ""
 
-#: converse.js:1200
+#: converse.js:1177
 msgid "Write in the third person"
 msgstr ""
 
-#: converse.js:1201 converse.js:2330
+#: converse.js:1178 converse.js:2313
 msgid "Remove messages"
 msgstr ""
 
-#: converse.js:1285
+#: converse.js:1262
 msgid "Are you sure you want to clear the messages from this chat box?"
 msgstr ""
 
-#: converse.js:1320
+#: converse.js:1297
 msgid "Your message could not be sent"
 msgstr ""
 
-#: converse.js:1323
+#: converse.js:1300
 msgid "We received an unencrypted message"
 msgstr ""
 
-#: converse.js:1326
+#: converse.js:1303
 msgid "We received an unreadable encrypted message"
 msgstr ""
 
-#: converse.js:1335
+#: converse.js:1312
 msgid "This user has requested an encrypted session."
 msgstr ""
 
-#: converse.js:1357
+#: converse.js:1334
 msgid ""
 "Here are the fingerprints, please confirm them with %1$s, outside of this "
 "chat.\n"
@@ -216,7 +216,7 @@ msgid ""
 "Cancel."
 msgstr ""
 
-#: converse.js:1370
+#: converse.js:1347
 msgid ""
 "You will be prompted to provide a security question and then an answer to "
 "that question.\n"
@@ -225,555 +225,555 @@ msgid ""
 "exact same answer (case sensitive), their identity will be verified."
 msgstr ""
 
-#: converse.js:1371
+#: converse.js:1348
 msgid "What is your security question?"
 msgstr ""
 
-#: converse.js:1373
+#: converse.js:1350
 msgid "What is the answer to the security question?"
 msgstr ""
 
-#: converse.js:1377
+#: converse.js:1354
 msgid "Invalid authentication scheme provided"
 msgstr ""
 
-#: converse.js:1488
+#: converse.js:1465
 msgid "Your messages are not encrypted anymore"
 msgstr ""
 
-#: converse.js:1490
+#: converse.js:1467
 msgid ""
 "Your messages are now encrypted but your buddy's identity has not been "
 "verified."
 msgstr ""
 
-#: converse.js:1492
+#: converse.js:1469
 msgid "Your buddy's identify has been verified."
 msgstr ""
 
-#: converse.js:1494
+#: converse.js:1471
 msgid "Your buddy has ended encryption on their end, you should do the same."
 msgstr ""
 
-#: converse.js:1503
+#: converse.js:1480
 msgid "Your messages are not encrypted. Click here to enable OTR encryption."
 msgstr ""
 
-#: converse.js:1505
+#: converse.js:1482
 msgid "Your messages are encrypted, but your buddy has not been verified."
 msgstr ""
 
-#: converse.js:1507
+#: converse.js:1484
 msgid "Your messages are encrypted and your buddy verified."
 msgstr ""
 
-#: converse.js:1509
+#: converse.js:1486
 msgid ""
 "Your buddy has closed their end of the private session, you should do the "
 "same"
 msgstr ""
 
-#: converse.js:1519
+#: converse.js:1496
 msgid "Clear all messages"
 msgstr ""
 
-#: converse.js:1520
+#: converse.js:1497
 msgid "End encrypted conversation"
 msgstr ""
 
-#: converse.js:1521
+#: converse.js:1498
 msgid "Hide the list of participants"
 msgstr ""
 
-#: converse.js:1522
+#: converse.js:1499
 msgid "Refresh encrypted conversation"
 msgstr ""
 
-#: converse.js:1523
+#: converse.js:1500
 msgid "Start a call"
 msgstr ""
 
-#: converse.js:1524
+#: converse.js:1501
 msgid "Start encrypted conversation"
 msgstr ""
 
-#: converse.js:1525
+#: converse.js:1502
 msgid "Verify with fingerprints"
 msgstr ""
 
-#: converse.js:1526
+#: converse.js:1503
 msgid "Verify with SMP"
 msgstr ""
 
-#: converse.js:1527
+#: converse.js:1504
 msgid "What's this?"
 msgstr ""
 
-#: converse.js:1618
+#: converse.js:1595
 msgid "Online"
 msgstr ""
 
-#: converse.js:1619
+#: converse.js:1596
 msgid "Busy"
 msgstr ""
 
-#: converse.js:1620
+#: converse.js:1597
 msgid "Away"
 msgstr ""
 
-#: converse.js:1621
+#: converse.js:1598
 msgid "Offline"
 msgstr ""
 
-#: converse.js:1622
+#: converse.js:1599
 msgid "Log out"
 msgstr ""
 
-#: converse.js:1628
+#: converse.js:1605
 msgid "Contact name"
 msgstr ""
 
-#: converse.js:1629
+#: converse.js:1606
 msgid "Search"
 msgstr ""
 
-#: converse.js:1633
+#: converse.js:1610
 msgid "Contact username"
 msgstr ""
 
-#: converse.js:1634
+#: converse.js:1611
 msgid "Add"
 msgstr ""
 
-#: converse.js:1639
+#: converse.js:1616
 msgid "Click to add new chat contacts"
 msgstr ""
 
-#: converse.js:1640
+#: converse.js:1617
 msgid "Add a contact"
 msgstr ""
 
-#: converse.js:1664
+#: converse.js:1641
 msgid "No users found"
 msgstr ""
 
-#: converse.js:1670
+#: converse.js:1647
 msgid "Click to add as a chat contact"
 msgstr ""
 
-#: converse.js:1725
+#: converse.js:1702
 msgid "Room name"
 msgstr ""
 
-#: converse.js:1726
+#: converse.js:1703
 msgid "Nickname"
 msgstr ""
 
-#: converse.js:1727
+#: converse.js:1704
 msgid "Server"
 msgstr ""
 
-#: converse.js:1728
+#: converse.js:1705
 msgid "Join"
 msgstr ""
 
-#: converse.js:1729
+#: converse.js:1706
 msgid "Show rooms"
 msgstr ""
 
-#: converse.js:1749
+#: converse.js:1726
 msgid "Rooms"
 msgstr ""
 
 #. For translators: %1$s is a variable and will be replaced with the XMPP server name
-#: converse.js:1756
+#: converse.js:1733
 msgid "No rooms on %1$s"
 msgstr ""
 
 #. For translators: %1$s is a variable and will be
 #. replaced with the XMPP server name
-#: converse.js:1771
+#: converse.js:1748
 msgid "Rooms on %1$s"
 msgstr ""
 
-#: converse.js:1780
+#: converse.js:1757
 msgid "Click to open this room"
 msgstr ""
 
-#: converse.js:1781
+#: converse.js:1758
 msgid "Show more information on this room"
 msgstr ""
 
-#: converse.js:1843
+#: converse.js:1820
 msgid "Description:"
 msgstr ""
 
-#: converse.js:1844
+#: converse.js:1821
 msgid "Occupants:"
 msgstr ""
 
-#: converse.js:1845
+#: converse.js:1822
 msgid "Features:"
 msgstr ""
 
-#: converse.js:1846
+#: converse.js:1823
 msgid "Requires authentication"
 msgstr ""
 
-#: converse.js:1847
+#: converse.js:1824
 msgid "Hidden"
 msgstr ""
 
-#: converse.js:1848
+#: converse.js:1825
 msgid "Requires an invitation"
 msgstr ""
 
-#: converse.js:1849
+#: converse.js:1826
 msgid "Moderated"
 msgstr ""
 
-#: converse.js:1850
+#: converse.js:1827
 msgid "Non-anonymous"
 msgstr ""
 
-#: converse.js:1851
+#: converse.js:1828
 msgid "Open room"
 msgstr ""
 
-#: converse.js:1852
+#: converse.js:1829
 msgid "Permanent room"
 msgstr ""
 
-#: converse.js:1853
+#: converse.js:1830
 msgid "Public"
 msgstr ""
 
-#: converse.js:1854
+#: converse.js:1831
 msgid "Semi-anonymous"
 msgstr ""
 
-#: converse.js:1855
+#: converse.js:1832
 msgid "Temporary room"
 msgstr ""
 
-#: converse.js:1856
+#: converse.js:1833
 msgid "Unmoderated"
 msgstr ""
 
-#: converse.js:2085
+#: converse.js:2062
 msgid "This user is a moderator"
 msgstr ""
 
-#: converse.js:2086
+#: converse.js:2063
 msgid "This user can send messages in this room"
 msgstr ""
 
-#: converse.js:2087
+#: converse.js:2064
 msgid "This user can NOT send messages in this room"
 msgstr ""
 
-#: converse.js:2119
+#: converse.js:2096
 msgid "Invite..."
 msgstr ""
 
-#: converse.js:2120
+#: converse.js:2097
 msgid "Occupants"
 msgstr ""
 
-#: converse.js:2185
+#: converse.js:2162
 msgid "You are about to invite %1$s to the chat room \"%2$s\". "
 msgstr ""
 
-#: converse.js:2186
+#: converse.js:2163
 msgid ""
 "You may optionally include a message, explaining the reason for the "
 "invitation."
 msgstr ""
 
-#: converse.js:2269
+#: converse.js:2246
 msgid "Message"
 msgstr ""
 
-#: converse.js:2307
+#: converse.js:2282
 msgid "Error: could not execute the command"
 msgstr ""
 
-#: converse.js:2329
+#: converse.js:2312
 msgid "Ban user from room"
 msgstr ""
 
-#: converse.js:2332
+#: converse.js:2315
 msgid "Kick user from room"
 msgstr ""
 
-#: converse.js:2333
+#: converse.js:2316
 msgid "Write in 3rd person"
 msgstr ""
 
-#: converse.js:2334
+#: converse.js:2317
 msgid "Remove user's ability to post messages"
 msgstr ""
 
-#: converse.js:2335
+#: converse.js:2318
 msgid "Change your nickname"
 msgstr ""
 
-#: converse.js:2336
+#: converse.js:2319
 msgid "Set room topic"
 msgstr ""
 
-#: converse.js:2337
+#: converse.js:2320
 msgid "Allow muted user to post messages"
 msgstr ""
 
-#: converse.js:2441 converse.js:4262
+#: converse.js:2423 converse.js:4250
 msgid "Save"
 msgstr ""
 
-#: converse.js:2442
+#: converse.js:2424
 msgid "Cancel"
 msgstr ""
 
-#: converse.js:2487
+#: converse.js:2469
 msgid "An error occurred while trying to save the form."
 msgstr ""
 
-#: converse.js:2531
+#: converse.js:2513
 msgid "This chatroom requires a password"
 msgstr ""
 
-#: converse.js:2532
+#: converse.js:2514
 msgid "Password: "
 msgstr ""
 
-#: converse.js:2533
+#: converse.js:2515
 msgid "Submit"
 msgstr ""
 
-#: converse.js:2568
+#: converse.js:2550
 msgid "This room is not anonymous"
 msgstr ""
 
-#: converse.js:2569
+#: converse.js:2551
 msgid "This room now shows unavailable members"
 msgstr ""
 
-#: converse.js:2570
+#: converse.js:2552
 msgid "This room does not show unavailable members"
 msgstr ""
 
-#: converse.js:2571
+#: converse.js:2553
 msgid "Non-privacy-related room configuration has changed"
 msgstr ""
 
-#: converse.js:2572
+#: converse.js:2554
 msgid "Room logging is now enabled"
 msgstr ""
 
-#: converse.js:2573
+#: converse.js:2555
 msgid "Room logging is now disabled"
 msgstr ""
 
-#: converse.js:2574
+#: converse.js:2556
 msgid "This room is now non-anonymous"
 msgstr ""
 
-#: converse.js:2575
+#: converse.js:2557
 msgid "This room is now semi-anonymous"
 msgstr ""
 
-#: converse.js:2576
+#: converse.js:2558
 msgid "This room is now fully-anonymous"
 msgstr ""
 
-#: converse.js:2577
+#: converse.js:2559
 msgid "A new room has been created"
 msgstr ""
 
-#: converse.js:2581 converse.js:2681
+#: converse.js:2563 converse.js:2663
 msgid "You have been banned from this room"
 msgstr ""
 
-#: converse.js:2582
+#: converse.js:2564
 msgid "You have been kicked from this room"
 msgstr ""
 
-#: converse.js:2583
+#: converse.js:2565
 msgid "You have been removed from this room because of an affiliation change"
 msgstr ""
 
-#: converse.js:2584
+#: converse.js:2566
 msgid ""
 "You have been removed from this room because the room has changed to members-"
 "only and you're not a member"
 msgstr ""
 
-#: converse.js:2585
+#: converse.js:2567
 msgid ""
 "You have been removed from this room because the MUC (Multi-user chat) "
 "service is being shut down."
 msgstr ""
 
-#: converse.js:2599
+#: converse.js:2581
 msgid "<strong>%1$s</strong> has been banned"
 msgstr ""
 
-#: converse.js:2600
+#: converse.js:2582
 msgid "<strong>%1$s</strong>'s nickname has changed"
 msgstr ""
 
-#: converse.js:2601
+#: converse.js:2583
 msgid "<strong>%1$s</strong> has been kicked out"
 msgstr ""
 
-#: converse.js:2602
+#: converse.js:2584
 msgid "<strong>%1$s</strong> has been removed because of an affiliation change"
 msgstr ""
 
-#: converse.js:2603
+#: converse.js:2585
 msgid "<strong>%1$s</strong> has been removed for not being a member"
 msgstr ""
 
-#: converse.js:2607
+#: converse.js:2589
 msgid "Your nickname has been automatically changed to: <strong>%1$s</strong>"
 msgstr ""
 
-#: converse.js:2608
+#: converse.js:2590
 msgid "Your nickname has been changed to: <strong>%1$s</strong>"
 msgstr ""
 
-#: converse.js:2656 converse.js:2666
+#: converse.js:2638 converse.js:2648
 msgid "The reason given is: \""
 msgstr ""
 
-#: converse.js:2679
+#: converse.js:2661
 msgid "You are not on the member list of this room"
 msgstr ""
 
-#: converse.js:2685
+#: converse.js:2667
 msgid "No nickname was specified"
 msgstr ""
 
-#: converse.js:2689
+#: converse.js:2671
 msgid "You are not allowed to create new rooms"
 msgstr ""
 
-#: converse.js:2691
+#: converse.js:2673
 msgid "Your nickname doesn't conform to this room's policies"
 msgstr ""
 
-#: converse.js:2695
+#: converse.js:2677
 msgid "Your nickname is already taken"
 msgstr ""
 
-#: converse.js:2697
+#: converse.js:2679
 msgid "This room does not (yet) exist"
 msgstr ""
 
-#: converse.js:2699
+#: converse.js:2681
 msgid "This room has reached it's maximum number of occupants"
 msgstr ""
 
-#: converse.js:2736
+#: converse.js:2723
 msgid "Topic set by %1$s to: %2$s"
 msgstr ""
 
-#: converse.js:2818
+#: converse.js:2805
 msgid "%1$s has invited you to join a chat room: %2$s"
 msgstr ""
 
-#: converse.js:2822
+#: converse.js:2809
 msgid ""
 "%1$s has invited you to join a chat room: %2$s, and left the following "
 "reason: \"%3$s\""
 msgstr ""
 
-#: converse.js:3058
+#: converse.js:3044
 msgid "Click to restore this chat"
 msgstr ""
 
-#: converse.js:3202
+#: converse.js:3188
 msgid "Minimized"
 msgstr ""
 
-#: converse.js:3274
+#: converse.js:3262
 msgid "Are you sure you want to remove this contact?"
 msgstr ""
 
-#: converse.js:3297
+#: converse.js:3285
 msgid "Are you sure you want to decline this contact request?"
 msgstr ""
 
-#: converse.js:3341 converse.js:3359
+#: converse.js:3329 converse.js:3347
 msgid "Click to remove this contact"
 msgstr ""
 
-#: converse.js:3348
+#: converse.js:3336
 msgid "Click to accept this contact request"
 msgstr ""
 
-#: converse.js:3349
+#: converse.js:3337
 msgid "Click to decline this contact request"
 msgstr ""
 
-#: converse.js:3358
+#: converse.js:3346
 msgid "Click to chat with this contact"
 msgstr ""
 
-#: converse.js:3874
+#: converse.js:3862
 msgid "Type to filter"
 msgstr ""
 
 #. For translators: the %1$s part gets replaced with the status
 #. Example, I am online
-#: converse.js:4233 converse.js:4310
+#: converse.js:4221 converse.js:4298
 msgid "I am %1$s"
 msgstr ""
 
-#: converse.js:4235 converse.js:4315
+#: converse.js:4223 converse.js:4303
 msgid "Click here to write a custom status message"
 msgstr ""
 
-#: converse.js:4236 converse.js:4316
+#: converse.js:4224 converse.js:4304
 msgid "Click to change your chat status"
 msgstr ""
 
-#: converse.js:4261
+#: converse.js:4249
 msgid "Custom status"
 msgstr ""
 
-#: converse.js:4290 converse.js:4298
+#: converse.js:4278 converse.js:4286
 msgid "online"
 msgstr ""
 
-#: converse.js:4292
+#: converse.js:4280
 msgid "busy"
 msgstr ""
 
-#: converse.js:4294
+#: converse.js:4282
 msgid "away for long"
 msgstr ""
 
-#: converse.js:4296
+#: converse.js:4284
 msgid "away"
 msgstr ""
 
-#: converse.js:4419
+#: converse.js:4407
 msgid "XMPP/Jabber Username:"
 msgstr ""
 
-#: converse.js:4420
+#: converse.js:4408
 msgid "Password:"
 msgstr ""
 
-#: converse.js:4421
+#: converse.js:4409
 msgid "Log In"
 msgstr ""
 
-#: converse.js:4428
+#: converse.js:4416
 msgid "Sign in"
 msgstr ""
 
-#: converse.js:4488
+#: converse.js:4476
 msgid "Toggle chat"
 msgstr ""

+ 781 - 0
locale/pl/LC_MESSAGES/converse.po

@@ -0,0 +1,781 @@
+# Polish translations for Converse.js package.
+# Copyright (C) 2014 Jan-Carel Brand
+# This file is distributed under the same license as the Converse.js package.
+# Translators:
+# Dev Account <info@elkom.com.tw>, 2014.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Converse.js 0.8.3\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-10-21 13:12+0200\n"
+"PO-Revision-Date: 2014-10-21 13:13+0200\n"
+"Last-Translator: Dev Account <info@elkom.com.tw>\n"
+"Language-Team: Polish\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ASCII\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+
+#: converse.js:314
+msgid "unencrypted"
+msgstr ""
+
+#: converse.js:315
+msgid "unverified"
+msgstr ""
+
+#: converse.js:316
+msgid "verified"
+msgstr ""
+
+#: converse.js:317
+msgid "finished"
+msgstr ""
+
+#: converse.js:320
+msgid "This contact is busy"
+msgstr ""
+
+#: converse.js:321
+msgid "This contact is online"
+msgstr ""
+
+#: converse.js:322
+msgid "This contact is offline"
+msgstr ""
+
+#: converse.js:323
+msgid "This contact is unavailable"
+msgstr ""
+
+#: converse.js:324
+msgid "This contact is away for an extended period"
+msgstr ""
+
+#: converse.js:325
+msgid "This contact is away"
+msgstr ""
+
+#: converse.js:327
+msgid "Click to hide these contacts"
+msgstr ""
+
+#: converse.js:329
+msgid "My contacts"
+msgstr ""
+
+#: converse.js:330
+msgid "Pending contacts"
+msgstr ""
+
+#: converse.js:331
+msgid "Contact requests"
+msgstr ""
+
+#: converse.js:332
+msgid "Ungrouped"
+msgstr ""
+
+#: converse.js:334
+msgid "Contacts"
+msgstr ""
+
+#: converse.js:335
+msgid "Groups"
+msgstr ""
+
+#: converse.js:417
+msgid "Reconnecting"
+msgstr ""
+
+#: converse.js:452
+msgid "Disconnected"
+msgstr ""
+
+#: converse.js:460
+msgid "Error"
+msgstr ""
+
+#: converse.js:462
+msgid "Connecting"
+msgstr ""
+
+#: converse.js:465
+msgid "Connection Failed"
+msgstr ""
+
+#: converse.js:467
+msgid "Authenticating"
+msgstr ""
+
+#: converse.js:470
+msgid "Authentication Failed"
+msgstr ""
+
+#: converse.js:475
+msgid "Disconnecting"
+msgstr ""
+
+#: converse.js:614 converse.js:660
+msgid "Online Contacts"
+msgstr ""
+
+#: converse.js:778
+msgid "Re-establishing encrypted session"
+msgstr ""
+
+#: converse.js:790
+msgid "Generating private key."
+msgstr ""
+
+#: converse.js:791
+msgid "Your browser might become unresponsive."
+msgstr ""
+
+#: converse.js:826
+msgid ""
+"Authentication request from %1$s\n"
+"\n"
+"Your buddy is attempting to verify your identity, by asking you the question "
+"below.\n"
+"\n"
+"%2$s"
+msgstr ""
+
+#: converse.js:835
+msgid "Could not verify this user's identify."
+msgstr ""
+
+#: converse.js:874
+msgid "Exchanging private key with buddy."
+msgstr ""
+
+#: converse.js:1023
+msgid "Personal message"
+msgstr ""
+
+#: converse.js:1055
+msgid "Are you sure you want to clear the messages from this room?"
+msgstr ""
+
+#: converse.js:1077
+msgid "me"
+msgstr ""
+
+#: converse.js:1131
+msgid "is typing"
+msgstr ""
+
+#: converse.js:1134
+msgid "has stopped typing"
+msgstr ""
+
+#: converse.js:1176 converse.js:2314
+msgid "Show this menu"
+msgstr ""
+
+#: converse.js:1177
+msgid "Write in the third person"
+msgstr ""
+
+#: converse.js:1178 converse.js:2313
+msgid "Remove messages"
+msgstr ""
+
+#: converse.js:1262
+msgid "Are you sure you want to clear the messages from this chat box?"
+msgstr ""
+
+#: converse.js:1297
+msgid "Your message could not be sent"
+msgstr ""
+
+#: converse.js:1300
+msgid "We received an unencrypted message"
+msgstr ""
+
+#: converse.js:1303
+msgid "We received an unreadable encrypted message"
+msgstr ""
+
+#: converse.js:1312
+msgid "This user has requested an encrypted session."
+msgstr ""
+
+#: converse.js:1334
+msgid ""
+"Here are the fingerprints, please confirm them with %1$s, outside of this "
+"chat.\n"
+"\n"
+"Fingerprint for you, %2$s: %3$s\n"
+"\n"
+"Fingerprint for %1$s: %4$s\n"
+"\n"
+"If you have confirmed that the fingerprints match, click OK, otherwise click "
+"Cancel."
+msgstr ""
+
+#: converse.js:1347
+msgid ""
+"You will be prompted to provide a security question and then an answer to "
+"that question.\n"
+"\n"
+"Your buddy will then be prompted the same question and if they type the "
+"exact same answer (case sensitive), their identity will be verified."
+msgstr ""
+
+#: converse.js:1348
+msgid "What is your security question?"
+msgstr ""
+
+#: converse.js:1350
+msgid "What is the answer to the security question?"
+msgstr ""
+
+#: converse.js:1354
+msgid "Invalid authentication scheme provided"
+msgstr ""
+
+#: converse.js:1465
+msgid "Your messages are not encrypted anymore"
+msgstr ""
+
+#: converse.js:1467
+msgid ""
+"Your messages are now encrypted but your buddy's identity has not been "
+"verified."
+msgstr ""
+
+#: converse.js:1469
+msgid "Your buddy's identify has been verified."
+msgstr ""
+
+#: converse.js:1471
+msgid "Your buddy has ended encryption on their end, you should do the same."
+msgstr ""
+
+#: converse.js:1480
+msgid "Your messages are not encrypted. Click here to enable OTR encryption."
+msgstr ""
+
+#: converse.js:1482
+msgid "Your messages are encrypted, but your buddy has not been verified."
+msgstr ""
+
+#: converse.js:1484
+msgid "Your messages are encrypted and your buddy verified."
+msgstr ""
+
+#: converse.js:1486
+msgid ""
+"Your buddy has closed their end of the private session, you should do the "
+"same"
+msgstr ""
+
+#: converse.js:1496
+msgid "Clear all messages"
+msgstr ""
+
+#: converse.js:1497
+msgid "End encrypted conversation"
+msgstr ""
+
+#: converse.js:1498
+msgid "Hide the list of participants"
+msgstr ""
+
+#: converse.js:1499
+msgid "Refresh encrypted conversation"
+msgstr ""
+
+#: converse.js:1500
+msgid "Start a call"
+msgstr ""
+
+#: converse.js:1501
+msgid "Start encrypted conversation"
+msgstr ""
+
+#: converse.js:1502
+msgid "Verify with fingerprints"
+msgstr ""
+
+#: converse.js:1503
+msgid "Verify with SMP"
+msgstr ""
+
+#: converse.js:1504
+msgid "What's this?"
+msgstr ""
+
+#: converse.js:1595
+msgid "Online"
+msgstr ""
+
+#: converse.js:1596
+msgid "Busy"
+msgstr ""
+
+#: converse.js:1597
+msgid "Away"
+msgstr ""
+
+#: converse.js:1598
+msgid "Offline"
+msgstr ""
+
+#: converse.js:1599
+msgid "Log out"
+msgstr ""
+
+#: converse.js:1605
+msgid "Contact name"
+msgstr ""
+
+#: converse.js:1606
+msgid "Search"
+msgstr ""
+
+#: converse.js:1610
+msgid "Contact username"
+msgstr ""
+
+#: converse.js:1611
+msgid "Add"
+msgstr ""
+
+#: converse.js:1616
+msgid "Click to add new chat contacts"
+msgstr ""
+
+#: converse.js:1617
+msgid "Add a contact"
+msgstr ""
+
+#: converse.js:1641
+msgid "No users found"
+msgstr ""
+
+#: converse.js:1647
+msgid "Click to add as a chat contact"
+msgstr ""
+
+#: converse.js:1702
+msgid "Room name"
+msgstr ""
+
+#: converse.js:1703
+msgid "Nickname"
+msgstr ""
+
+#: converse.js:1704
+msgid "Server"
+msgstr ""
+
+#: converse.js:1705
+msgid "Join"
+msgstr ""
+
+#: converse.js:1706
+msgid "Show rooms"
+msgstr ""
+
+#: converse.js:1726
+msgid "Rooms"
+msgstr ""
+
+#. For translators: %1$s is a variable and will be replaced with the XMPP server name
+#: converse.js:1733
+msgid "No rooms on %1$s"
+msgstr ""
+
+#. For translators: %1$s is a variable and will be
+#. replaced with the XMPP server name
+#: converse.js:1748
+msgid "Rooms on %1$s"
+msgstr ""
+
+#: converse.js:1757
+msgid "Click to open this room"
+msgstr ""
+
+#: converse.js:1758
+msgid "Show more information on this room"
+msgstr ""
+
+#: converse.js:1820
+msgid "Description:"
+msgstr ""
+
+#: converse.js:1821
+msgid "Occupants:"
+msgstr ""
+
+#: converse.js:1822
+msgid "Features:"
+msgstr ""
+
+#: converse.js:1823
+msgid "Requires authentication"
+msgstr ""
+
+#: converse.js:1824
+msgid "Hidden"
+msgstr ""
+
+#: converse.js:1825
+msgid "Requires an invitation"
+msgstr ""
+
+#: converse.js:1826
+msgid "Moderated"
+msgstr ""
+
+#: converse.js:1827
+msgid "Non-anonymous"
+msgstr ""
+
+#: converse.js:1828
+msgid "Open room"
+msgstr ""
+
+#: converse.js:1829
+msgid "Permanent room"
+msgstr ""
+
+#: converse.js:1830
+msgid "Public"
+msgstr ""
+
+#: converse.js:1831
+msgid "Semi-anonymous"
+msgstr ""
+
+#: converse.js:1832
+msgid "Temporary room"
+msgstr ""
+
+#: converse.js:1833
+msgid "Unmoderated"
+msgstr ""
+
+#: converse.js:2062
+msgid "This user is a moderator"
+msgstr ""
+
+#: converse.js:2063
+msgid "This user can send messages in this room"
+msgstr ""
+
+#: converse.js:2064
+msgid "This user can NOT send messages in this room"
+msgstr ""
+
+#: converse.js:2096
+msgid "Invite..."
+msgstr ""
+
+#: converse.js:2097
+msgid "Occupants"
+msgstr ""
+
+#: converse.js:2162
+msgid "You are about to invite %1$s to the chat room \"%2$s\". "
+msgstr ""
+
+#: converse.js:2163
+msgid ""
+"You may optionally include a message, explaining the reason for the "
+"invitation."
+msgstr ""
+
+#: converse.js:2246
+msgid "Message"
+msgstr ""
+
+#: converse.js:2282
+msgid "Error: could not execute the command"
+msgstr ""
+
+#: converse.js:2312
+msgid "Ban user from room"
+msgstr ""
+
+#: converse.js:2315
+msgid "Kick user from room"
+msgstr ""
+
+#: converse.js:2316
+msgid "Write in 3rd person"
+msgstr ""
+
+#: converse.js:2317
+msgid "Remove user's ability to post messages"
+msgstr ""
+
+#: converse.js:2318
+msgid "Change your nickname"
+msgstr ""
+
+#: converse.js:2319
+msgid "Set room topic"
+msgstr ""
+
+#: converse.js:2320
+msgid "Allow muted user to post messages"
+msgstr ""
+
+#: converse.js:2423 converse.js:4250
+msgid "Save"
+msgstr ""
+
+#: converse.js:2424
+msgid "Cancel"
+msgstr ""
+
+#: converse.js:2469
+msgid "An error occurred while trying to save the form."
+msgstr ""
+
+#: converse.js:2513
+msgid "This chatroom requires a password"
+msgstr ""
+
+#: converse.js:2514
+msgid "Password: "
+msgstr ""
+
+#: converse.js:2515
+msgid "Submit"
+msgstr ""
+
+#: converse.js:2550
+msgid "This room is not anonymous"
+msgstr ""
+
+#: converse.js:2551
+msgid "This room now shows unavailable members"
+msgstr ""
+
+#: converse.js:2552
+msgid "This room does not show unavailable members"
+msgstr ""
+
+#: converse.js:2553
+msgid "Non-privacy-related room configuration has changed"
+msgstr ""
+
+#: converse.js:2554
+msgid "Room logging is now enabled"
+msgstr ""
+
+#: converse.js:2555
+msgid "Room logging is now disabled"
+msgstr ""
+
+#: converse.js:2556
+msgid "This room is now non-anonymous"
+msgstr ""
+
+#: converse.js:2557
+msgid "This room is now semi-anonymous"
+msgstr ""
+
+#: converse.js:2558
+msgid "This room is now fully-anonymous"
+msgstr ""
+
+#: converse.js:2559
+msgid "A new room has been created"
+msgstr ""
+
+#: converse.js:2563 converse.js:2663
+msgid "You have been banned from this room"
+msgstr ""
+
+#: converse.js:2564
+msgid "You have been kicked from this room"
+msgstr ""
+
+#: converse.js:2565
+msgid "You have been removed from this room because of an affiliation change"
+msgstr ""
+
+#: converse.js:2566
+msgid ""
+"You have been removed from this room because the room has changed to members-"
+"only and you're not a member"
+msgstr ""
+
+#: converse.js:2567
+msgid ""
+"You have been removed from this room because the MUC (Multi-user chat) "
+"service is being shut down."
+msgstr ""
+
+#: converse.js:2581
+msgid "<strong>%1$s</strong> has been banned"
+msgstr ""
+
+#: converse.js:2582
+msgid "<strong>%1$s</strong>'s nickname has changed"
+msgstr ""
+
+#: converse.js:2583
+msgid "<strong>%1$s</strong> has been kicked out"
+msgstr ""
+
+#: converse.js:2584
+msgid "<strong>%1$s</strong> has been removed because of an affiliation change"
+msgstr ""
+
+#: converse.js:2585
+msgid "<strong>%1$s</strong> has been removed for not being a member"
+msgstr ""
+
+#: converse.js:2589
+msgid "Your nickname has been automatically changed to: <strong>%1$s</strong>"
+msgstr ""
+
+#: converse.js:2590
+msgid "Your nickname has been changed to: <strong>%1$s</strong>"
+msgstr ""
+
+#: converse.js:2638 converse.js:2648
+msgid "The reason given is: \""
+msgstr ""
+
+#: converse.js:2661
+msgid "You are not on the member list of this room"
+msgstr ""
+
+#: converse.js:2667
+msgid "No nickname was specified"
+msgstr ""
+
+#: converse.js:2671
+msgid "You are not allowed to create new rooms"
+msgstr ""
+
+#: converse.js:2673
+msgid "Your nickname doesn't conform to this room's policies"
+msgstr ""
+
+#: converse.js:2677
+msgid "Your nickname is already taken"
+msgstr ""
+
+#: converse.js:2679
+msgid "This room does not (yet) exist"
+msgstr ""
+
+#: converse.js:2681
+msgid "This room has reached it's maximum number of occupants"
+msgstr ""
+
+#: converse.js:2723
+msgid "Topic set by %1$s to: %2$s"
+msgstr ""
+
+#: converse.js:2805
+msgid "%1$s has invited you to join a chat room: %2$s"
+msgstr ""
+
+#: converse.js:2809
+msgid ""
+"%1$s has invited you to join a chat room: %2$s, and left the following "
+"reason: \"%3$s\""
+msgstr ""
+
+#: converse.js:3044
+msgid "Click to restore this chat"
+msgstr ""
+
+#: converse.js:3188
+msgid "Minimized"
+msgstr ""
+
+#: converse.js:3262
+msgid "Are you sure you want to remove this contact?"
+msgstr ""
+
+#: converse.js:3285
+msgid "Are you sure you want to decline this contact request?"
+msgstr ""
+
+#: converse.js:3329 converse.js:3347
+msgid "Click to remove this contact"
+msgstr ""
+
+#: converse.js:3336
+msgid "Click to accept this contact request"
+msgstr ""
+
+#: converse.js:3337
+msgid "Click to decline this contact request"
+msgstr ""
+
+#: converse.js:3346
+msgid "Click to chat with this contact"
+msgstr ""
+
+#: converse.js:3862
+msgid "Type to filter"
+msgstr ""
+
+#. For translators: the %1$s part gets replaced with the status
+#. Example, I am online
+#: converse.js:4221 converse.js:4298
+msgid "I am %1$s"
+msgstr ""
+
+#: converse.js:4223 converse.js:4303
+msgid "Click here to write a custom status message"
+msgstr ""
+
+#: converse.js:4224 converse.js:4304
+msgid "Click to change your chat status"
+msgstr ""
+
+#: converse.js:4249
+msgid "Custom status"
+msgstr ""
+
+#: converse.js:4278 converse.js:4286
+msgid "online"
+msgstr ""
+
+#: converse.js:4280
+msgid "busy"
+msgstr ""
+
+#: converse.js:4282
+msgid "away for long"
+msgstr ""
+
+#: converse.js:4284
+msgid "away"
+msgstr ""
+
+#: converse.js:4407
+msgid "XMPP/Jabber Username:"
+msgstr ""
+
+#: converse.js:4408
+msgid "Password:"
+msgstr ""
+
+#: converse.js:4409
+msgid "Log In"
+msgstr ""
+
+#: converse.js:4416
+msgid "Sign in"
+msgstr ""
+
+#: converse.js:4476
+msgid "Toggle chat"
+msgstr ""

+ 7 - 10
main.js

@@ -1,4 +1,4 @@
-config = {
+require.config({
     baseUrl: '.',
     paths: {
         "backbone":                 "components/backbone/backbone",
@@ -17,7 +17,7 @@ config = {
         "strophe":                  "components/strophe/strophe",
         "strophe.disco":            "components/strophejs-plugins/disco/strophe.disco",
         "strophe.muc":              "components/strophe.muc/index",
-        "strophe.roster":           "components/strophe.roster/index",
+        "strophe.roster":           "src/strophe.roster",
         "strophe.vcard":            "components/strophejs-plugins/vcard/strophe.vcard",
         "text":                     'components/requirejs-text/text',
         "tpl":                      'components/requirejs-tpl-jcbrand/tpl',
@@ -139,16 +139,13 @@ config = {
         'crypto.sha1':          { deps: ['crypto.core'] },
         'crypto.sha256':        { deps: ['crypto.core'] },
         'bigint':               { deps: ['crypto'] },
+        'strophe':              { exports: 'Strophe' },
         'strophe.disco':        { deps: ['strophe'] },
         'strophe.muc':          { deps: ['strophe'] },
         'strophe.roster':       { deps: ['strophe'] },
         'strophe.vcard':        { deps: ['strophe'] }
     }
-};
-
-if (typeof(require) !== 'undefined') {
-    require.config(config);
-    require(["jquery", "converse"], function($, converse) {
-        window.converse = converse;
-    });
-}
+});
+require(["converse"], function(converse) {
+    window.converse = converse;
+});

+ 32 - 21
spec/chatbox.js

@@ -57,23 +57,26 @@
                 expect($("#conversejs .chatbox").length).toBe(1); // Controlbox is open
 
                 // Test that they can be trimmed
-                var online_contacts = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact').find('a.open-chat');
-                for (i=0; i<online_contacts.length; i++) {
-                    $el = $(online_contacts[i]);
-                    jid = $el.text().replace(/ /g,'.').toLowerCase() + '@localhost';
-                    $el.click();
-                    expect(this.chatboxviews.trimChats).toHaveBeenCalled();
-
-                    chatboxview = this.chatboxviews.get(jid);
-                    spyOn(chatboxview, 'hide').andCallThrough();
-                    chatboxview.model.set({'minimized': true});
-                    expect(trimmed_chatboxes.addChat).toHaveBeenCalled();
-                    expect(chatboxview.hide).toHaveBeenCalled();
-                    trimmedview = trimmed_chatboxes.get(jid);
-                }
-
-                // Test that they can be maximized again
                 runs($.proxy(function () {
+                    converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced.
+                }, this));
+                waits(50);
+                runs($.proxy(function () {
+                    // Test that they can be maximized again
+                    var online_contacts = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact').find('a.open-chat');
+                    for (i=0; i<online_contacts.length; i++) {
+                        $el = $(online_contacts[i]);
+                        jid = $el.text().replace(/ /g,'.').toLowerCase() + '@localhost';
+                        $el.click();
+                        expect(this.chatboxviews.trimChats).toHaveBeenCalled();
+
+                        chatboxview = this.chatboxviews.get(jid);
+                        spyOn(chatboxview, 'hide').andCallThrough();
+                        chatboxview.model.set({'minimized': true});
+                        expect(trimmed_chatboxes.addChat).toHaveBeenCalled();
+                        expect(chatboxview.hide).toHaveBeenCalled();
+                        trimmedview = trimmed_chatboxes.get(jid);
+                    }
                     var key = this.chatboxviews.keys()[1];
                     trimmedview = trimmed_chatboxes.get(key);
                     chatbox = trimmedview.model;
@@ -99,11 +102,19 @@
                 chatbox = test_utils.openChatBoxFor(contact_jid);
                 chatboxview = this.chatboxviews.get(contact_jid);
                 spyOn(chatboxview, 'focus');
-                $el = this.rosterview.$el.find('a.open-chat:contains("'+chatbox.get('fullname')+'")');
-                jid = $el.text().replace(/ /g,'.').toLowerCase() + '@localhost';
-                $el.click();
-                expect(this.chatboxes.length).toEqual(2);
-                expect(chatboxview.focus).toHaveBeenCalled();
+
+                // Test that they can be trimmed
+                runs($.proxy(function () {
+                    converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced.
+                }, this));
+                waits(50);
+                runs($.proxy(function () {
+                    $el = this.rosterview.$el.find('a.open-chat:contains("'+chatbox.get('fullname')+'")');
+                    jid = $el.text().replace(/ /g,'.').toLowerCase() + '@localhost';
+                    $el.click();
+                    expect(this.chatboxes.length).toEqual(2);
+                    expect(chatboxview.focus).toHaveBeenCalled();
+                }, this));
             }, converse));
 
             it("can be saved to, and retrieved from, browserStorage", $.proxy(function () {

+ 429 - 264
spec/controlbox.js

@@ -145,7 +145,7 @@
                         subscription: 'both'
                     });
                     converse.rosterview.update(); // XXX: Will normally called as event handler
-                    if (converse.rosterview.$('.roster-contacts').hasScrollBar()) {
+                    if (converse.rosterview.$roster.hasScrollBar()) {
                         expect($filter.is(':visible')).toBeTruthy();
                     } else {
                         expect($filter.is(':visible')).toBeFalsy();
@@ -154,11 +154,16 @@
             }, converse));
 
             it("can be used to filter the contacts shown", function () {
-                converse.roster_groups = true;
-                _clearContacts();
-                utils.createGroupedContacts();
-                var $filter = converse.rosterview.$('.roster-filter');
-                var $roster = converse.rosterview.$('.roster-contacts');
+                var $filter;
+                var $roster;
+                runs(function () {
+                    _clearContacts();
+                    converse.roster_groups = true;
+                    utils.createGroupedContacts();
+                    $filter = converse.rosterview.$('.roster-filter');
+                    $roster = converse.rosterview.$roster;
+                });
+                waits(350); // Needed, due to debounce
                 runs(function () {
                     expect($roster.find('dd:visible').length).toBe(15);
                     expect($roster.find('dt:visible').length).toBe(5);
@@ -199,13 +204,19 @@
             });
 
             it("can be used to filter the groups shown", function () {
-                converse.roster_groups = true;
-                _clearContacts();
-                utils.createGroupedContacts();
-                var $filter = converse.rosterview.$('.roster-filter');
-                var $roster = converse.rosterview.$('.roster-contacts');
-                var $type = converse.rosterview.$('.filter-type');
-                $type.val('groups');
+                var $filter;
+                var $roster;
+                var $type;
+                runs(function () {
+                    converse.roster_groups = true;
+                    _clearContacts();
+                    utils.createGroupedContacts();
+                    $filter = converse.rosterview.$('.roster-filter');
+                    $roster = converse.rosterview.$roster;
+                    $type = converse.rosterview.$('.filter-type');
+                    $type.val('groups');
+                });
+                waits(350); // Needed, due to debounce
                 runs(function () {
                     expect($roster.find('dd:visible').length).toBe(15);
                     expect($roster.find('dt:visible').length).toBe(5);
@@ -242,7 +253,7 @@
                 _clearContacts();
                 utils.createGroupedContacts();
                 var $filter = converse.rosterview.$('.roster-filter');
-                var $roster = converse.rosterview.$('.roster-contacts');
+                var $roster = converse.rosterview.$roster;
                 runs (function () {
                     $filter.val("xxx");
                     $filter.trigger('keydown');
@@ -267,56 +278,66 @@
             });
 
             it("can be used to organize existing contacts", $.proxy(function () {
-                _clearContacts();
-                spyOn(converse, 'emit');
-                spyOn(this.rosterview, 'update').andCallThrough();
-                converse.rosterview.render();
-                utils.createContacts('pending');
-                utils.createContacts('requesting');
-                utils.createGroupedContacts();
-                // Check that the groups appear alphabetically and that
-                // requesting and pending contacts are last.
-                var group_titles = $.map(this.rosterview.$el.find('dt'), function (o) { return $(o).text().trim(); });
-                expect(group_titles).toEqual([
-                    "colleagues",
-                    "Family",
-                    "friends & acquaintences",
-                    "ænemies",
-                    "Ungrouped",
-                    "Contact requests",
-                    "Pending contacts"
-                ]);
-                // Check that usernames appear alphabetically per group
-                _.each(_.keys(mock.groups), $.proxy(function (name) {
-                    var $contacts = this.rosterview.$('dt.roster-group[data-group="'+name+'"]').nextUntil('dt', 'dd');
-                    var names = $.map($contacts, function (o) { return $(o).text().trim(); });
-                    expect(names).toEqual(_.clone(names).sort());
-                }, converse));
+                runs($.proxy(function () {
+                    _clearContacts();
+                    spyOn(converse, 'emit');
+                    spyOn(this.rosterview, 'update').andCallThrough();
+                    converse.rosterview.render();
+                    utils.createContacts('pending');
+                    utils.createContacts('requesting');
+                    utils.createGroupedContacts();
+                }, this));
+                waits(50); // Needed, due to debounce
+                runs($.proxy(function () {
+                    // Check that the groups appear alphabetically and that
+                    // requesting and pending contacts are last.
+                    var group_titles = $.map(this.rosterview.$el.find('dt'), function (o) { return $(o).text().trim(); });
+                    expect(group_titles).toEqual([
+                        "colleagues",
+                        "Family",
+                        "friends & acquaintences",
+                        "ænemies",
+                        "Ungrouped",
+                        "Contact requests",
+                        "Pending contacts"
+                    ]);
+                    // Check that usernames appear alphabetically per group
+                    _.each(_.keys(mock.groups), $.proxy(function (name) {
+                        var $contacts = this.rosterview.$('dt.roster-group[data-group="'+name+'"]').nextUntil('dt', 'dd');
+                        var names = $.map($contacts, function (o) { return $(o).text().trim(); });
+                        expect(names).toEqual(_.clone(names).sort());
+                    }, converse));
+                }, this));
             }, converse));
 
             it("can share contacts with other roster groups", $.proxy(function () {
-                _clearContacts();
-                var i=0, j=0;
-                spyOn(converse, 'emit');
-                spyOn(this.rosterview, 'update').andCallThrough();
-                converse.rosterview.render();
                 var groups = ['colleagues', 'friends'];
-                for (i=0; i<mock.cur_names.length; i++) {
-                    this.roster.create({
-                        jid: mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
-                        subscription: 'both',
-                        ask: null,
-                        groups: groups,
-                        fullname: mock.cur_names[i]
-                    });
-                }
-                // Check that usernames appear alphabetically per group
-                _.each(groups, $.proxy(function (name) {
-                    var $contacts = this.rosterview.$('dt.roster-group[data-group="'+name+'"]').nextUntil('dt', 'dd');
-                    var names = $.map($contacts, function (o) { return $(o).text().trim(); });
-                    expect(names).toEqual(_.clone(names).sort());
-                    expect(names.length).toEqual(mock.cur_names.length);
-                }, converse));
+                runs($.proxy(function () {
+                    _clearContacts();
+                    var i=0, j=0;
+                    spyOn(converse, 'emit');
+                    spyOn(this.rosterview, 'update').andCallThrough();
+                    converse.rosterview.render();
+                    for (i=0; i<mock.cur_names.length; i++) {
+                        this.roster.create({
+                            jid: mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
+                            subscription: 'both',
+                            ask: null,
+                            groups: groups,
+                            fullname: mock.cur_names[i]
+                        });
+                    }
+                }, this));
+                waits(50); // Needed, due to debounce
+                runs($.proxy(function () {
+                    // Check that usernames appear alphabetically per group
+                    _.each(groups, $.proxy(function (name) {
+                        var $contacts = this.rosterview.$('dt.roster-group[data-group="'+name+'"]').nextUntil('dt', 'dd');
+                        var names = $.map($contacts, function (o) { return $(o).text().trim(); });
+                        expect(names).toEqual(_.clone(names).sort());
+                        expect(names.length).toEqual(mock.cur_names.length);
+                    }, this));
+                }, this));
             }, converse));
 
             it("remembers whether it is closed or opened", $.proxy(function () {
@@ -361,8 +382,13 @@
             }
 
             it("can be collapsed under their own header", $.proxy(function () {
-                _addContacts();
-                checkHeaderToggling.apply(this, [this.rosterview.get('Pending contacts').$el]);
+                runs(function () {
+                    _addContacts();
+                });
+                waits(50);
+                runs($.proxy(function () {
+                    checkHeaderToggling.apply(this, [this.rosterview.get('Pending contacts').$el]);
+                }, this));
             }, converse));
 
             it("can be added to the roster", $.proxy(function () {
@@ -385,40 +411,62 @@
             }, converse));
 
             it("can be removed by the user", $.proxy(function () {
-                _addContacts();
-                var name = mock.pend_names[0];
-                var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
-                spyOn(window, 'confirm').andReturn(true);
-                spyOn(converse, 'emit');
-                spyOn(this.connection.roster, 'remove').andCallThrough();
-                spyOn(this.connection.roster, 'unauthorize');
-                spyOn(this.rosterview.model, 'remove').andCallThrough();
+                runs($.proxy(function () {
+                    _addContacts();
+                }, this));
+                waits(50);
+                runs($.proxy(function () {
+                    /* FIXME: Monkepatch
+                    * After refactoring the mock connection to use a
+                    * Strophe.Connection object, these tests fail because "remove"
+                    * function in strophe.roster (line 292) gets called and it
+                    * then tries to actually remove the user which is not in the roster...
+                    */
+                    var old_remove = this.connection.roster.remove;
+                    this.connection.roster.remove = function (jid, callback) { callback(); };
 
-                converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')")
-                    .siblings('.remove-xmpp-contact').click();
+                    var name = mock.pend_names[0];
+                    var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
+                    spyOn(window, 'confirm').andReturn(true);
+                    spyOn(converse, 'emit');
+                    spyOn(this.connection.roster, 'remove').andCallThrough();
+                    spyOn(this.connection.roster, 'unauthorize');
+                    spyOn(this.rosterview.model, 'remove').andCallThrough();
 
-                expect(window.confirm).toHaveBeenCalled();
-                expect(this.connection.roster.remove).toHaveBeenCalled();
-                expect(this.connection.roster.unauthorize).toHaveBeenCalled();
-                expect(this.rosterview.model.remove).toHaveBeenCalled();
-                expect(converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')").length).toEqual(0);
+                    converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')")
+                        .siblings('.remove-xmpp-contact').click();
+
+                    expect(window.confirm).toHaveBeenCalled();
+                    expect(this.connection.roster.remove).toHaveBeenCalled();
+                    expect(this.connection.roster.unauthorize).toHaveBeenCalled();
+                    expect(this.rosterview.model.remove).toHaveBeenCalled();
+                    expect(converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')").length).toEqual(0);
+
+                    /* XXX Restore Monkeypatch */
+                    this.connection.roster.remove = old_remove;
+                }, this));
             }, converse));
 
             it("do not have a header if there aren't any", $.proxy(function () {
                 var name = mock.pend_names[0];
-                _clearContacts();
-                spyOn(window, 'confirm').andReturn(true);
-                this.roster.create({
-                    jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
-                    subscription: 'none',
-                    ask: 'subscribe',
-                    fullname: name
-                });
-                expect(this.rosterview.get('Pending contacts').$el.is(':visible')).toEqual(true);
-                converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')")
-                    .siblings('.remove-xmpp-contact').click();
-                expect(window.confirm).toHaveBeenCalled();
-                expect(this.rosterview.get('Pending contacts').$el.is(':visible')).toEqual(false);
+                runs($.proxy(function () {
+                    _clearContacts();
+                }, this));
+                waits(50);
+                runs($.proxy(function () {
+                    spyOn(window, 'confirm').andReturn(true);
+                    this.roster.create({
+                        jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
+                        subscription: 'none',
+                        ask: 'subscribe',
+                        fullname: name
+                    });
+                    expect(this.rosterview.get('Pending contacts').$el.is(':visible')).toEqual(true);
+                    converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')")
+                        .siblings('.remove-xmpp-contact').click();
+                    expect(window.confirm).toHaveBeenCalled();
+                    expect(this.rosterview.get('Pending contacts').$el.is(':visible')).toEqual(false);
+                }, this));
             }, converse));
 
 
@@ -467,8 +515,13 @@
             };
 
             it("can be collapsed under their own header", $.proxy(function () {
-                _addContacts();
-                checkHeaderToggling.apply(this, [this.rosterview.$el.find('dt.roster-group')]);
+                runs(function () {
+                    _addContacts();
+                });
+                waits(50);
+                runs($.proxy(function () {
+                    checkHeaderToggling.apply(this, [this.rosterview.$el.find('dt.roster-group')]);
+                }, this));
             }, converse));
 
             it("will be hidden when appearing under a collapsed group", $.proxy(function () {
@@ -488,180 +541,237 @@
             }, converse));
 
             it("can be added to the roster and they will be sorted alphabetically", $.proxy(function () {
-                _clearContacts();
-                var i, t;
-                spyOn(converse, 'emit');
-                spyOn(this.rosterview, 'update').andCallThrough();
-                for (i=0; i<mock.cur_names.length; i++) {
-                    this.roster.create({
-                        jid: mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
-                        subscription: 'both',
-                        ask: null,
-                        fullname: mock.cur_names[i]
-                    });
-                    expect(this.rosterview.update).toHaveBeenCalled();
-                }
-                // Check that they are sorted alphabetically
-                t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.offline').find('a.open-chat').text();
-                expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
+                runs(function () {
+                    _clearContacts();
+                });
+                waits(50);
+                runs($.proxy(function () {
+                    var i, t;
+                    spyOn(converse, 'emit');
+                    spyOn(this.rosterview, 'update').andCallThrough();
+                    for (i=0; i<mock.cur_names.length; i++) {
+                        this.roster.create({
+                            jid: mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
+                            subscription: 'both',
+                            ask: null,
+                            fullname: mock.cur_names[i]
+                        });
+                        expect(this.rosterview.update).toHaveBeenCalled();
+                    }
+                    // Check that they are sorted alphabetically
+                    t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.offline').find('a.open-chat').text();
+                    expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
+                }, this));
             }, converse));
 
             it("can be removed by the user", $.proxy(function () {
-                _addContacts();
-                var name = mock.cur_names[0];
-                var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
-                spyOn(window, 'confirm').andReturn(true);
-                spyOn(converse, 'emit');
-                spyOn(this.connection.roster, 'remove').andCallThrough();
-                spyOn(this.connection.roster, 'unauthorize');
-                spyOn(this.rosterview.model, 'remove').andCallThrough();
+                runs(function () {
+                    _addContacts();
+                });
+                waits(50);
+                runs($.proxy(function () {
+                    /* FIXME: Monkepatch
+                    * After refactoring the mock connection to use a
+                    * Strophe.Connection object, these tests fail because "remove"
+                    * function in strophe.roster (line 292) gets called and it
+                    * then tries to actually remove the user which is not in the roster...
+                    */
+                    var old_remove = this.connection.roster.remove;
+                    this.connection.roster.remove = function (jid, callback) { callback(); };
 
-                converse.rosterview.$el.find(".open-chat:contains('"+name+"')")
-                    .siblings('.remove-xmpp-contact').click();
+                    var name = mock.cur_names[0];
+                    var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
+                    spyOn(window, 'confirm').andReturn(true);
+                    spyOn(converse, 'emit');
+                    spyOn(this.connection.roster, 'remove').andCallThrough();
+                    spyOn(this.connection.roster, 'unauthorize');
+                    spyOn(this.rosterview.model, 'remove').andCallThrough();
+
+                    converse.rosterview.$el.find(".open-chat:contains('"+name+"')")
+                        .siblings('.remove-xmpp-contact').click();
 
-                expect(window.confirm).toHaveBeenCalled();
-                expect(this.connection.roster.remove).toHaveBeenCalled();
-                expect(this.connection.roster.unauthorize).toHaveBeenCalled();
-                expect(this.rosterview.model.remove).toHaveBeenCalled();
-                expect(converse.rosterview.$el.find(".open-chat:contains('"+name+"')").length).toEqual(0);
+                    expect(window.confirm).toHaveBeenCalled();
+                    expect(this.connection.roster.remove).toHaveBeenCalled();
+                    expect(this.connection.roster.unauthorize).toHaveBeenCalled();
+                    expect(this.rosterview.model.remove).toHaveBeenCalled();
+                    expect(converse.rosterview.$el.find(".open-chat:contains('"+name+"')").length).toEqual(0);
+
+                    /* XXX Restore Monkeypatch */
+                    this.connection.roster.remove = old_remove;
+                }, this));
             }, converse));
 
 
             it("do not have a header if there aren't any", $.proxy(function () {
                 var name = mock.cur_names[0];
-                _clearContacts();
-                spyOn(window, 'confirm').andReturn(true);
-                this.roster.create({
-                    jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
-                    subscription: 'both',
-                    ask: null,
-                    fullname: name
+                runs(function () {
+                    _clearContacts();
                 });
-                expect(this.rosterview.$el.find('dt.roster-group').css('display')).toEqual('block');
-                converse.rosterview.$el.find(".open-chat:contains('"+name+"')")
-                    .siblings('.remove-xmpp-contact').click();
-                expect(window.confirm).toHaveBeenCalled();
-                expect(this.rosterview.$el.find('dt.roster-group').css('display')).toEqual('none');
+                waits(50);
+                runs($.proxy(function () {
+                    spyOn(window, 'confirm').andReturn(true);
+                    this.roster.create({
+                        jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
+                        subscription: 'both',
+                        ask: null,
+                        fullname: name
+                    });
+                    expect(this.rosterview.$el.find('dt.roster-group').css('display')).toEqual('block');
+                    converse.rosterview.$el.find(".open-chat:contains('"+name+"')")
+                        .siblings('.remove-xmpp-contact').click();
+                    expect(window.confirm).toHaveBeenCalled();
+                    expect(this.rosterview.$el.find('dt.roster-group').css('display')).toEqual('none');
+                }, this));
             }, converse));
 
             it("can change their status to online and be sorted alphabetically", $.proxy(function () {
-                _addContacts();
-                var jid, t;
-                spyOn(converse, 'emit');
-                spyOn(this.rosterview, 'update').andCallThrough();
-                for (i=0; i<mock.cur_names.length; i++) {
-                    jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    this.roster.get(jid).set('chat_status', 'online');
-                    expect(this.rosterview.update).toHaveBeenCalled();
-                    // Check that they are sorted alphabetically
-                    t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.online').find('a.open-chat').text();
-                    expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
-                }
+                runs(function () {
+                    _addContacts();
+                });
+                waits(50);
+                runs($.proxy(function () {
+                    var jid, t;
+                    spyOn(converse, 'emit');
+                    spyOn(this.rosterview, 'update').andCallThrough();
+                    for (i=0; i<mock.cur_names.length; i++) {
+                        jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        this.roster.get(jid).set('chat_status', 'online');
+                        expect(this.rosterview.update).toHaveBeenCalled();
+                        // Check that they are sorted alphabetically
+                        t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.online').find('a.open-chat').text();
+                        expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
+                    }
+                }, this));
             }, converse));
 
             it("can change their status to busy and be sorted alphabetically", $.proxy(function () {
-                _addContacts();
-                var jid, t;
-                spyOn(converse, 'emit');
-                spyOn(this.rosterview, 'update').andCallThrough();
-                for (i=0; i<mock.cur_names.length; i++) {
-                    jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    this.roster.get(jid).set('chat_status', 'dnd');
-                    expect(this.rosterview.update).toHaveBeenCalled();
-                    // Check that they are sorted alphabetically
-                    t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.dnd').find('a.open-chat').text();
-                    expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
-                }
+                runs(function () {
+                    _addContacts();
+                });
+                waits(50);
+                runs($.proxy(function () {
+                    var jid, t;
+                    spyOn(converse, 'emit');
+                    spyOn(this.rosterview, 'update').andCallThrough();
+                    for (i=0; i<mock.cur_names.length; i++) {
+                        jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        this.roster.get(jid).set('chat_status', 'dnd');
+                        expect(this.rosterview.update).toHaveBeenCalled();
+                        // Check that they are sorted alphabetically
+                        t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.dnd').find('a.open-chat').text();
+                        expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
+                    }
+                }, this));
             }, converse));
 
             it("can change their status to away and be sorted alphabetically", $.proxy(function () {
-                _addContacts();
-                var jid, t;
-                spyOn(converse, 'emit');
-                spyOn(this.rosterview, 'update').andCallThrough();
-                for (i=0; i<mock.cur_names.length; i++) {
-                    jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    this.roster.get(jid).set('chat_status', 'away');
-                    expect(this.rosterview.update).toHaveBeenCalled();
-                    // Check that they are sorted alphabetically
-                    t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.away').find('a.open-chat').text();
-                    expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
-                }
+                runs(function () {
+                    _addContacts();
+                });
+                waits(50);
+                runs($.proxy(function () {
+                    var jid, t;
+                    spyOn(converse, 'emit');
+                    spyOn(this.rosterview, 'update').andCallThrough();
+                    for (i=0; i<mock.cur_names.length; i++) {
+                        jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        this.roster.get(jid).set('chat_status', 'away');
+                        expect(this.rosterview.update).toHaveBeenCalled();
+                        // Check that they are sorted alphabetically
+                        t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.away').find('a.open-chat').text();
+                        expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
+                    }
+                }, this));
             }, converse));
 
             it("can change their status to xa and be sorted alphabetically", $.proxy(function () {
-                _addContacts();
-                var jid, t;
-                spyOn(converse, 'emit');
-                spyOn(this.rosterview, 'update').andCallThrough();
-                for (i=0; i<mock.cur_names.length; i++) {
-                    jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    this.roster.get(jid).set('chat_status', 'xa');
-                    expect(this.rosterview.update).toHaveBeenCalled();
-                    // Check that they are sorted alphabetically
-                    t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.xa').find('a.open-chat').text();
-                    expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
-                }
+                runs(function () {
+                    _addContacts();
+                });
+                waits(50);
+                runs($.proxy(function () {
+                    var jid, t;
+                    spyOn(converse, 'emit');
+                    spyOn(this.rosterview, 'update').andCallThrough();
+                    for (i=0; i<mock.cur_names.length; i++) {
+                        jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        this.roster.get(jid).set('chat_status', 'xa');
+                        expect(this.rosterview.update).toHaveBeenCalled();
+                        // Check that they are sorted alphabetically
+                        t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.xa').find('a.open-chat').text();
+                        expect(t).toEqual(mock.cur_names.slice(0,i+1).sort().join(''));
+                    }
+                }, this));
             }, converse));
 
             it("can change their status to unavailable and be sorted alphabetically", $.proxy(function () {
-                _addContacts();
-                var jid, t;
-                spyOn(converse, 'emit');
-                spyOn(this.rosterview, 'update').andCallThrough();
-                for (i=0; i<mock.cur_names.length; i++) {
-                    jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    this.roster.get(jid).set('chat_status', 'unavailable');
-                    expect(this.rosterview.update).toHaveBeenCalled();
-                    // Check that they are sorted alphabetically
-                    t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.unavailable').find('a.open-chat').text();
-                    expect(t).toEqual(mock.cur_names.slice(0, i+1).sort().join(''));
-                }
+                runs(function () {
+                    _addContacts();
+                });
+                waits(50);
+                runs($.proxy(function () {
+                    var jid, t;
+                    spyOn(converse, 'emit');
+                    spyOn(this.rosterview, 'update').andCallThrough();
+                    for (i=0; i<mock.cur_names.length; i++) {
+                        jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        this.roster.get(jid).set('chat_status', 'unavailable');
+                        expect(this.rosterview.update).toHaveBeenCalled();
+                        // Check that they are sorted alphabetically
+                        t = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact.unavailable').find('a.open-chat').text();
+                        expect(t).toEqual(mock.cur_names.slice(0, i+1).sort().join(''));
+                    }
+                }, this));
             }, converse));
 
             it("are ordered according to status: online, busy, away, xa, unavailable, offline", $.proxy(function () {
-                _addContacts();
-                var i;
-                for (i=0; i<3; i++) {
-                    jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    this.roster.get(jid).set('chat_status', 'online');
-                }
-                for (i=3; i<6; i++) {
-                    jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    this.roster.get(jid).set('chat_status', 'dnd');
-                }
-                for (i=6; i<9; i++) {
-                    jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    this.roster.get(jid).set('chat_status', 'away');
-                }
-                for (i=9; i<12; i++) {
-                    jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    this.roster.get(jid).set('chat_status', 'xa');
-                }
-                for (i=12; i<15; i++) {
-                    jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    this.roster.get(jid).set('chat_status', 'unavailable');
-                }
+                runs(function () {
+                    _addContacts();
+                });
+                waits(50);
+                runs($.proxy(function () {
+                    var i;
+                    for (i=0; i<3; i++) {
+                        jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        this.roster.get(jid).set('chat_status', 'online');
+                    }
+                    for (i=3; i<6; i++) {
+                        jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        this.roster.get(jid).set('chat_status', 'dnd');
+                    }
+                    for (i=6; i<9; i++) {
+                        jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        this.roster.get(jid).set('chat_status', 'away');
+                    }
+                    for (i=9; i<12; i++) {
+                        jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        this.roster.get(jid).set('chat_status', 'xa');
+                    }
+                    for (i=12; i<15; i++) {
+                        jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        this.roster.get(jid).set('chat_status', 'unavailable');
+                    }
 
-                var contacts = this.rosterview.$el.find('dd.current-xmpp-contact');
-                for (i=0; i<3; i++) {
-                    expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('online');
-                }
-                for (i=3; i<6; i++) {
-                    expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('dnd');
-                }
-                for (i=6; i<9; i++) {
-                    expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('away');
-                }
-                for (i=9; i<12; i++) {
-                    expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('xa');
-                }
-                for (i=12; i<15; i++) {
-                    expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('unavailable');
-                }
-                for (i=15; i<mock.cur_names.length; i++) {
-                    expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('offline');
-                }
+                    var contacts = this.rosterview.$el.find('dd.current-xmpp-contact');
+                    for (i=0; i<3; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('online');
+                    }
+                    for (i=3; i<6; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('dnd');
+                    }
+                    for (i=6; i<9; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('away');
+                    }
+                    for (i=9; i<12; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('xa');
+                    }
+                    for (i=12; i<15; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('unavailable');
+                    }
+                    for (i=15; i<mock.cur_names.length; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('offline');
+                    }
+                }, this));
             }, converse));
         }, converse));
 
@@ -713,20 +823,25 @@
             it("do not have a header if there aren't any", $.proxy(function () {
                 converse.rosterview.model.reset(); // We want to manually create users so that we can spy
                 var name = mock.req_names[0];
-                spyOn(window, 'confirm').andReturn(true);
-                this.roster.create({
-                    jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
-                    subscription: 'none',
-                    ask: null,
-                    requesting: true,
-                    fullname: name
-                });
-                expect(this.rosterview.get('Contact requests').$el.is(':visible')).toEqual(true);
-                converse.rosterview.$el.find(".req-contact-name:contains('"+name+"')")
-                    .siblings('.request-actions')
-                    .find('.decline-xmpp-request').click();
-                expect(window.confirm).toHaveBeenCalled();
-                expect(this.rosterview.get('Contact requests').$el.is(':visible')).toEqual(false);
+                runs($.proxy(function () {
+                    spyOn(window, 'confirm').andReturn(true);
+                    this.roster.create({
+                        jid: name.replace(/ /g,'.').toLowerCase() + '@localhost',
+                        subscription: 'none',
+                        ask: null,
+                        requesting: true,
+                        fullname: name
+                    });
+                }, this));
+                waits(50);
+                runs($.proxy(function () {
+                    expect(this.rosterview.get('Contact requests').$el.is(':visible')).toEqual(true);
+                    converse.rosterview.$el.find(".req-contact-name:contains('"+name+"')")
+                        .siblings('.request-actions')
+                        .find('.decline-xmpp-request').click();
+                    expect(window.confirm).toHaveBeenCalled();
+                    expect(this.rosterview.get('Contact requests').$el.is(':visible')).toEqual(false);
+                }, this));
             }, converse));
 
             it("can be collapsed under their own header", $.proxy(function () {
@@ -750,20 +865,70 @@
 
             it("can have their requests denied by the user", $.proxy(function () {
                 this.rosterview.model.reset();
-                spyOn(converse, 'emit');
-                spyOn(this.connection.roster, 'unauthorize');
-                spyOn(window, 'confirm').andReturn(true);
-                utils.createContacts('requesting').openControlBox();
-                var name = mock.req_names.sort()[1];
-                var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
-                converse.rosterview.$el.find(".req-contact-name:contains('"+name+"')")
-                    .siblings('.request-actions')
-                    .find('.decline-xmpp-request').click();
-                expect(window.confirm).toHaveBeenCalled();
-                expect(this.connection.roster.unauthorize).toHaveBeenCalled();
-                // There should now be one less contact
-                expect(this.roster.length).toEqual(mock.req_names.length-1);
+                runs($.proxy(function () {
+                    spyOn(converse, 'emit');
+                    spyOn(this.connection.roster, 'unauthorize');
+                    spyOn(window, 'confirm').andReturn(true);
+                    utils.createContacts('requesting').openControlBox();
+                    converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced.
+                }, this));
+                waits(50);
+                runs($.proxy(function () {
+                    var name = mock.req_names.sort()[1];
+                    converse.rosterview.$el.find(".req-contact-name:contains('"+name+"')")
+                        .siblings('.request-actions')
+                        .find('.decline-xmpp-request').click();
+                }, this));
+                waits(50);
+                runs($.proxy(function () {
+                    expect(window.confirm).toHaveBeenCalled();
+                    expect(this.connection.roster.unauthorize).toHaveBeenCalled();
+                    // There should now be one less contact
+                    expect(this.roster.length).toEqual(mock.req_names.length-1);
+                }, this));
             }, converse));
+
+            it("are persisted even if other contacts' change their presence ", $.proxy(function() {
+                /* This is a regression test.
+                 * https://github.com/jcbrand/converse.js/issues/262
+                 */
+                this.rosterview.model.reset();
+                spyOn(this.roster, 'clearCache').andCallThrough();
+                expect(this.roster.pluck('jid').length).toBe(0);
+
+                var stanza = $pres({from: 'data@enterprise/resource', type: 'subscribe'});
+                this.connection._dataRecv(test_utils.createRequest(stanza));
+                expect(this.roster.pluck('jid').length).toBe(1);
+                expect(_.contains(this.roster.pluck('jid'), 'data@enterprise')).toBeTruthy();
+
+                // Taken from the spec
+                // http://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3
+                stanza = $iq({
+                    to: this.connection.jid,
+                    type: 'result',
+                    id: 'roster_1'
+                }).c('query', {
+                    xmlns: 'jabber:iq:roster',
+                }).c('item', {
+                    jid: 'romeo@example.net',
+                    name: 'Romeo',
+                    subscription:'both'
+                }).c('group').t('Friends').up().up()
+                .c('item', {
+                    jid: 'mercutio@example.org',
+                    name: 'Mercutio',
+                    subscription:'from'
+                }).c('group').t('Friends').up().up()
+                .c('item', {
+                    jid: 'benvolio@example.org',
+                    name: 'Benvolio',
+                    subscription:'both'
+                }).c('group').t('Friends');
+                this.connection.roster._onReceiveRosterSuccess(null, stanza.tree());
+                expect(this.roster.clearCache).toHaveBeenCalled();
+                expect(_.contains(this.roster.pluck('jid'), 'data@enterprise')).toBeTruthy();
+            }, converse));
+
         }, converse));
 
         describe("All Contacts", $.proxy(function () {
@@ -775,7 +940,7 @@
             }, converse));
 
             it("are saved to, and can be retrieved from, browserStorage", $.proxy(function () {
-                var new_attrs, old_attrs, attrs, old_roster;
+                var new_attrs, old_attrs, attrs;
                 var num_contacts = this.roster.length;
                 new_roster = new this.RosterContacts();
                 // Roster items are yet to be fetched from browserStorage

+ 36 - 0
spec/minchats.js

@@ -84,6 +84,42 @@
                 expect(this.minimized_chats.toggleview.$('.unread-message-count').is(':visible')).toBeTruthy();
                 expect(this.minimized_chats.toggleview.$('.unread-message-count').text()).toBe((i+1).toString());
             }
+            // Chat state notifications don't increment the unread messages counter
+            // <composing> state
+            this.chatboxes.onMessage($msg({
+                from: contact_jid,
+                to: this.connection.jid,
+                type: 'chat',
+                id: (new Date()).getTime()
+            }).c('composing', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+            expect(this.minimized_chats.toggleview.$('.unread-message-count').text()).toBe((i).toString());
+
+            // <paused> state
+            this.chatboxes.onMessage($msg({
+                from: contact_jid,
+                to: this.connection.jid,
+                type: 'chat',
+                id: (new Date()).getTime()
+            }).c('paused', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+            expect(this.minimized_chats.toggleview.$('.unread-message-count').text()).toBe((i).toString());
+
+            // <gone> state
+            this.chatboxes.onMessage($msg({
+                from: contact_jid,
+                to: this.connection.jid,
+                type: 'chat',
+                id: (new Date()).getTime()
+            }).c('gone', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+            expect(this.minimized_chats.toggleview.$('.unread-message-count').text()).toBe((i).toString());
+
+            // <inactive> state
+            this.chatboxes.onMessage($msg({
+                from: contact_jid,
+                to: this.connection.jid,
+                type: 'chat',
+                id: (new Date()).getTime()
+            }).c('inactive', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+            expect(this.minimized_chats.toggleview.$('.unread-message-count').text()).toBe((i).toString());
         }, converse));
 
     }, converse, mock, test_utils));

+ 71 - 0
spec/profiling.js

@@ -0,0 +1,71 @@
+(function (root, factory) {
+    define([
+        "jquery",
+        "mock",
+        "test_utils"
+        ], function ($, mock, test_utils) {
+            return factory($, mock, test_utils);
+        }
+    );
+} (this, function ($, mock, test_utils) {
+    describe("Profiling", function() {
+        beforeEach(function() {
+            converse.connection.roster.items = [];
+            converse.connection._changeConnectStatus(Strophe.Status.CONNECTED);
+        });
+
+        xit("adds hundreds of contacts to the roster", $.proxy(function() {
+            converse.roster_groups = false;
+            spyOn(this.roster, 'clearCache').andCallThrough();
+            expect(this.roster.pluck('jid').length).toBe(0);
+            var stanza = $iq({
+                to: this.connection.jid,
+                type: 'result',
+                id: 'roster_1'
+            }).c('query', {
+                xmlns: 'jabber:iq:roster'
+            });
+            _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) {
+                var i;
+                for (i=0; i<100; i++) {
+                    stanza = stanza.c('item', {
+                        jid: Math.random().toString().replace('0.', '')+'@example.net',
+                        subscription:'both'
+                    }).c('group').t(group).up().up();
+                }
+            });
+            this.connection.roster._onReceiveRosterSuccess(null, stanza.tree());
+            expect(this.roster.clearCache).toHaveBeenCalled();
+            expect(this.roster.pluck('jid').length).toBe(400);
+        }, converse));
+
+        xit("adds hundreds of contacts to the roster, with roster groups", $.proxy(function() {
+            // converse.show_only_online_users = true;
+            converse.roster_groups = true;
+            spyOn(this.roster, 'clearCache').andCallThrough();
+            expect(this.roster.pluck('jid').length).toBe(0);
+            var stanza = $iq({
+                to: this.connection.jid,
+                type: 'result',
+                id: 'roster_1'
+            }).c('query', {
+                xmlns: 'jabber:iq:roster'
+            });
+            _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) {
+                var i;
+                for (i=0; i<100; i++) {
+                    stanza = stanza.c('item', {
+                        jid: Math.random().toString().replace('0.', '')+'@example.net',
+                        subscription:'both'
+                    }).c('group').t(group).up().up();
+                }
+            });
+            this.connection.roster._onReceiveRosterSuccess(null, stanza.tree());
+            expect(this.roster.clearCache).toHaveBeenCalled();
+            //expect(this.roster.pluck('jid').length).toBe(400);
+        }, converse));
+
+        it("contacts in a very large roster change their statuses", $.proxy(function() {
+        }, converse));
+    });
+}));

+ 12 - 0
src/build-no-jquery.js

@@ -0,0 +1,12 @@
+({
+    baseUrl: "../",
+    name: "components/almond/almond.js",
+    out: "../builds/converse.nojquery.min.js",
+    include: ['main'],
+    mainConfigFile: '../main.js',
+    paths: {
+        "converse-dependencies":    "src/deps-full",
+        "jquery":                   "src/jquery-external",
+        "jquery-private":           "src/jquery-private-external",
+    }
+})

+ 2 - 86
src/build-no-locales-no-otr.js

@@ -3,93 +3,9 @@
     name: "components/almond/almond.js",
     out: "../builds/converse-no-locales-no-otr.min.js",
     include: ['main'],
-    tpl: {
-        // Use Mustache style syntax for variable interpolation
-        templateSettings: {
-            evaluate : /\{\[([\s\S]+?)\]\}/g,
-            interpolate : /\{\{([\s\S]+?)\}\}/g
-        }
-    },
-    map: {
-        // '*' means all modules will get 'jquery-private'
-        // for their 'jquery' dependency.
-        '*': { 'jquery': 'jquery-private' },
-        // 'jquery-private' wants the real jQuery module
-        // though. If this line was not here, there would
-        // be an unresolvable cyclic dependency.
-        'jquery-private': { 'jquery': 'jquery' }
-    },
+    mainConfigFile: '../main.js',
     paths: {
-        "backbone":                 "components/backbone/backbone",
-        "backbone.browserStorage":  "components/backbone.browserStorage/backbone.browserStorage",
-        "backbone.overview":        "components/backbone.overview/backbone.overview",
-        "bootstrap":                "components/bootstrap/dist/js/bootstrap",                  // XXX: Only required for https://conversejs.org website
         "converse-dependencies":    "src/deps-no-otr",
-        "converse-templates":       "src/templates",
-        "eventemitter":             "components/otr/build/dep/eventemitter",
-        "jquery":                   "components/jquery/dist/jquery",
-        "jquery-private":           "src/jquery-private",
-        "jquery.browser":           "components/jquery.browser/index",
-        "jquery.easing":            "components/jquery-easing-original/index", // XXX: Only required for https://conversejs.org website
-        "moment":                   "components/momentjs/moment",
-        "strophe":                  "components/strophe/strophe",
-        "strophe.disco":            "components/strophe.disco/index",
-        "strophe.muc":              "components/strophe.muc/index",
-        "strophe.roster":           "components/strophe.roster/index",
-        "strophe.vcard":            "components/strophe.vcard/index",
-        "text":                     'components/requirejs-text/text',
-        "tpl":                      'components/requirejs-tpl-jcbrand/tpl',
-        "typeahead":                "components/typeahead.js/index",
-        "underscore":               "components/underscore/underscore",
-        "utils":                    "src/utils",
-
-        // Locales paths
-        "locales":   "locale/nolocales",
-        "jed":       "components/jed/jed",
-
-        // Templates
-        "action":                   "src/templates/action",
-        "add_contact_dropdown":     "src/templates/add_contact_dropdown",
-        "add_contact_form":         "src/templates/add_contact_form",
-        "change_status_message":    "src/templates/change_status_message",
-        "chat_status":              "src/templates/chat_status",
-        "chatarea":                 "src/templates/chatarea",
-        "chatbox":                  "src/templates/chatbox",
-        "chatroom":                 "src/templates/chatroom",
-        "chatroom_password_form":   "src/templates/chatroom_password_form",
-        "chatroom_sidebar":         "src/templates/chatroom_sidebar",
-        "chatrooms_tab":            "src/templates/chatrooms_tab",
-        "chats_panel":              "src/templates/chats_panel",
-        "choose_status":            "src/templates/choose_status",
-        "contacts_panel":           "src/templates/contacts_panel",
-        "contacts_tab":             "src/templates/contacts_tab",
-        "controlbox":               "src/templates/controlbox",
-        "controlbox_toggle":        "src/templates/controlbox_toggle",
-        "field":                    "src/templates/field",
-        "form_checkbox":            "src/templates/form_checkbox",
-        "form_input":               "src/templates/form_input",
-        "form_select":              "src/templates/form_select",
-        "group_header":             "src/templates/group_header",
-        "info":                     "src/templates/info",
-        "login_panel":              "src/templates/login_panel",
-        "login_tab":                "src/templates/login_tab",
-        "message":                  "src/templates/message",
-        "new_day":                  "src/templates/new_day",
-        "occupant":                 "src/templates/occupant",
-        "pending_contact":          "src/templates/pending_contact",
-        "pending_contacts":         "src/templates/pending_contacts",
-        "requesting_contact":       "src/templates/requesting_contact",
-        "requesting_contacts":      "src/templates/requesting_contacts",
-        "room_description":         "src/templates/room_description",
-        "room_item":                "src/templates/room_item",
-        "room_panel":               "src/templates/room_panel",
-        "roster":                   "src/templates/roster",
-        "roster_item":              "src/templates/roster_item",
-        "search_contact":           "src/templates/search_contact",
-        "select_option":            "src/templates/select_option",
-        "status_option":            "src/templates/status_option",
-        "toggle_chats":             "src/templates/toggle_chats",
-        "toolbar":                  "src/templates/toolbar",
-        "trimmed_chat":             "src/templates/trimmed_chat"
+        "locales":   "locale/nolocales"
     }
 })

+ 2 - 99
src/build-no-otr.js

@@ -3,105 +3,8 @@
     name: "components/almond/almond.js",
     out: "../builds/converse-no-otr.min.js",
     include: ['main'],
-    tpl: {
-        // Use Mustache style syntax for variable interpolation
-        templateSettings: {
-            evaluate : /\{\[([\s\S]+?)\]\}/g,
-            interpolate : /\{\{([\s\S]+?)\}\}/g
-        }
-    },
-    map: {
-        // '*' means all modules will get 'jquery-private'
-        // for their 'jquery' dependency.
-        '*': { 'jquery': 'jquery-private' },
-        // 'jquery-private' wants the real jQuery module
-        // though. If this line was not here, there would
-        // be an unresolvable cyclic dependency.
-        'jquery-private': { 'jquery': 'jquery' }
-    },
+    mainConfigFile: '../main.js',
     paths: {
-        "backbone":                 "components/backbone/backbone",
-        "backbone.browserStorage":  "components/backbone.browserStorage/backbone.browserStorage",
-        "backbone.overview":        "components/backbone.overview/backbone.overview",
-        "converse-dependencies":    "src/deps-no-otr",
-        "converse-templates":       "src/templates",
-        "eventemitter":             "components/otr/build/dep/eventemitter",
-        "jquery":                   "components/jquery/dist/jquery",
-        "jquery-private":           "src/jquery-private",
-        "jquery.browser":           "components/jquery.browser/index",
-        "moment":                   "components/momentjs/moment",
-        "strophe":                  "components/strophe/strophe",
-        "strophe.disco":            "components/strophe.disco/index",
-        "strophe.muc":              "components/strophe.muc/index",
-        "strophe.roster":           "components/strophe.roster/index",
-        "strophe.vcard":            "components/strophe.vcard/index",
-        "text":                     'components/requirejs-text/text',
-        "tpl":                      'components/requirejs-tpl-jcbrand/tpl',
-        "typeahead":                "components/typeahead.js/index",
-        "underscore":               "components/underscore/underscore",
-        "utils":                    "src/utils",
-
-        // Locales paths
-        "locales":   "locale/locales",
-        "jed":       "components/jed/jed",
-        "af":        "locale/af/LC_MESSAGES/af",
-        "de":        "locale/de/LC_MESSAGES/de",
-        "en":        "locale/en/LC_MESSAGES/en",
-        "es":        "locale/es/LC_MESSAGES/es",
-        "fr":        "locale/fr/LC_MESSAGES/fr",
-        "he":        "locale/he/LC_MESSAGES/he",
-        "hu":        "locale/hu/LC_MESSAGES/hu",
-        "id":        "locale/id/LC_MESSAGES/id",
-        "it":        "locale/it/LC_MESSAGES/it",
-        "ja":        "locale/ja/LC_MESSAGES/ja",
-        "nl":        "locale/nl/LC_MESSAGES/nl",
-        "pt_BR":     "locale/pt_BR/LC_MESSAGES/pt_BR",
-        "ru":        "locale/ru/LC_MESSAGES/ru",
-        "zh":        "locale/zh/LC_MESSAGES/zh",
-
-        // Templates
-        "action":                   "src/templates/action",
-        "add_contact_dropdown":     "src/templates/add_contact_dropdown",
-        "add_contact_form":         "src/templates/add_contact_form",
-        "change_status_message":    "src/templates/change_status_message",
-        "chat_status":              "src/templates/chat_status",
-        "chatarea":                 "src/templates/chatarea",
-        "chatbox":                  "src/templates/chatbox",
-        "chatroom":                 "src/templates/chatroom",
-        "chatroom_password_form":   "src/templates/chatroom_password_form",
-        "chatroom_sidebar":         "src/templates/chatroom_sidebar",
-        "chatrooms_tab":            "src/templates/chatrooms_tab",
-        "chats_panel":              "src/templates/chats_panel",
-        "choose_status":            "src/templates/choose_status",
-        "contacts_panel":           "src/templates/contacts_panel",
-        "contacts_tab":             "src/templates/contacts_tab",
-        "controlbox":               "src/templates/controlbox",
-        "controlbox_toggle":        "src/templates/controlbox_toggle",
-        "field":                    "src/templates/field",
-        "form_checkbox":            "src/templates/form_checkbox",
-        "form_input":               "src/templates/form_input",
-        "form_select":              "src/templates/form_select",
-        "group_header":             "src/templates/group_header",
-        "info":                     "src/templates/info",
-        "login_panel":              "src/templates/login_panel",
-        "login_tab":                "src/templates/login_tab",
-        "message":                  "src/templates/message",
-        "new_day":                  "src/templates/new_day",
-        "occupant":                 "src/templates/occupant",
-        "pending_contact":          "src/templates/pending_contact",
-        "pending_contacts":         "src/templates/pending_contacts",
-        "requesting_contact":       "src/templates/requesting_contact",
-        "requesting_contacts":      "src/templates/requesting_contacts",
-        "room_description":         "src/templates/room_description",
-        "room_item":                "src/templates/room_item",
-        "room_panel":               "src/templates/room_panel",
-        "roster":                   "src/templates/roster",
-        "roster_item":              "src/templates/roster_item",
-        "search_contact":           "src/templates/search_contact",
-        "select_option":            "src/templates/select_option",
-        "status_option":            "src/templates/status_option",
-        "toggle_chats":             "src/templates/toggle_chats",
-        "toolbar":                  "src/templates/toolbar",
-        "trimmed_chat":             "src/templates/trimmed_chat"
+        "converse-dependencies":    "src/deps-no-otr"
     }
 })

+ 2 - 101
src/build-website-no-otr.js

@@ -3,107 +3,8 @@
     name: "components/almond/almond.js",
     out: "../builds/converse.website-no-otr.min.js",
     include: ['main'],
-    tpl: {
-        // Use Mustache style syntax for variable interpolation
-        templateSettings: {
-            evaluate : /\{\[([\s\S]+?)\]\}/g,
-            interpolate : /\{\{([\s\S]+?)\}\}/g
-        }
-    },
-    map: {
-        // '*' means all modules will get 'jquery-private'
-        // for their 'jquery' dependency.
-        '*': { 'jquery': 'jquery-private' },
-        // 'jquery-private' wants the real jQuery module
-        // though. If this line was not here, there would
-        // be an unresolvable cyclic dependency.
-        'jquery-private': { 'jquery': 'jquery' }
-    },
+    mainConfigFile: '../main.js',
     paths: {
-        "backbone":                 "components/backbone/backbone",
-        "backbone.browserStorage":  "components/backbone.browserStorage/backbone.browserStorage",
-        "backbone.overview":        "components/backbone.overview/backbone.overview",
-        "bootstrapJS":              "components/bootstrapJS/index",                   // XXX: Only required for https://conversejs.org website
-        "converse-dependencies":    "src/deps-website-no-otr",
-        "converse-templates":       "src/templates",
-        "eventemitter":             "components/otr/build/dep/eventemitter",
-        "jquery":                   "components/jquery/dist/jquery",
-        "jquery-private":           "src/jquery-private",
-        "jquery.browser":           "components/jquery.browser/index",
-        "jquery.easing":            "components/jquery-easing-original/index",                  // XXX: Only required for https://conversejs.org website
-        "moment":                   "components/momentjs/moment",
-        "strophe":                  "components/strophe/strophe",
-        "strophe.disco":            "components/strophe.disco/index",
-        "strophe.muc":              "components/strophe.muc/index",
-        "strophe.roster":           "components/strophe.roster/index",
-        "strophe.vcard":            "components/strophe.vcard/index",
-        "text":                     'components/requirejs-text/text',
-        "tpl":                      'components/requirejs-tpl-jcbrand/tpl',
-        "typeahead":                "components/typeahead.js/index",
-        "underscore":               "components/underscore/underscore",
-        "utils":                    "src/utils",
-
-        // Locales paths
-        "locales":   "locale/locales",
-        "jed":       "components/jed/jed",
-        "af":        "locale/af/LC_MESSAGES/af",
-        "de":        "locale/de/LC_MESSAGES/de",
-        "en":        "locale/en/LC_MESSAGES/en",
-        "es":        "locale/es/LC_MESSAGES/es",
-        "fr":        "locale/fr/LC_MESSAGES/fr",
-        "he":        "locale/he/LC_MESSAGES/he",
-        "hu":        "locale/hu/LC_MESSAGES/hu",
-        "id":        "locale/id/LC_MESSAGES/id",
-        "it":        "locale/it/LC_MESSAGES/it",
-        "ja":        "locale/ja/LC_MESSAGES/ja",
-        "nl":        "locale/nl/LC_MESSAGES/nl",
-        "pt_BR":     "locale/pt_BR/LC_MESSAGES/pt_BR",
-        "ru":        "locale/ru/LC_MESSAGES/ru",
-        "zh":        "locale/zh/LC_MESSAGES/zh",
-
-        // Templates
-        "action":                   "src/templates/action",
-        "add_contact_dropdown":     "src/templates/add_contact_dropdown",
-        "add_contact_form":         "src/templates/add_contact_form",
-        "change_status_message":    "src/templates/change_status_message",
-        "chat_status":              "src/templates/chat_status",
-        "chatarea":                 "src/templates/chatarea",
-        "chatbox":                  "src/templates/chatbox",
-        "chatroom":                 "src/templates/chatroom",
-        "chatroom_password_form":   "src/templates/chatroom_password_form",
-        "chatroom_sidebar":         "src/templates/chatroom_sidebar",
-        "chatrooms_tab":            "src/templates/chatrooms_tab",
-        "chats_panel":              "src/templates/chats_panel",
-        "choose_status":            "src/templates/choose_status",
-        "contacts_panel":           "src/templates/contacts_panel",
-        "contacts_tab":             "src/templates/contacts_tab",
-        "controlbox":               "src/templates/controlbox",
-        "controlbox_toggle":        "src/templates/controlbox_toggle",
-        "field":                    "src/templates/field",
-        "form_checkbox":            "src/templates/form_checkbox",
-        "form_input":               "src/templates/form_input",
-        "form_select":              "src/templates/form_select",
-        "group_header":             "src/templates/group_header",
-        "info":                     "src/templates/info",
-        "login_panel":              "src/templates/login_panel",
-        "login_tab":                "src/templates/login_tab",
-        "message":                  "src/templates/message",
-        "new_day":                  "src/templates/new_day",
-        "occupant":                 "src/templates/occupant",
-        "pending_contact":          "src/templates/pending_contact",
-        "pending_contacts":         "src/templates/pending_contacts",
-        "requesting_contact":       "src/templates/requesting_contact",
-        "requesting_contacts":      "src/templates/requesting_contacts",
-        "room_description":         "src/templates/room_description",
-        "room_item":                "src/templates/room_item",
-        "room_panel":               "src/templates/room_panel",
-        "roster":                   "src/templates/roster",
-        "roster_item":              "src/templates/roster_item",
-        "search_contact":           "src/templates/search_contact",
-        "select_option":            "src/templates/select_option",
-        "status_option":            "src/templates/status_option",
-        "toggle_chats":             "src/templates/toggle_chats",
-        "toolbar":                  "src/templates/toolbar",
-        "trimmed_chat":             "src/templates/trimmed_chat"
+        "converse-dependencies":    "src/deps-website-no-otr"
     }
 })

+ 2 - 118
src/build-website.js

@@ -3,124 +3,8 @@
     name: "components/almond/almond.js",
     out: "../builds/converse.website.min.js",
     include: ['main'],
-    tpl: {
-        // Use Mustache style syntax for variable interpolation
-        templateSettings: {
-            evaluate : /\{\[([\s\S]+?)\]\}/g,
-            interpolate : /\{\{([\s\S]+?)\}\}/g
-        }
-    },
-    map: {
-        // '*' means all modules will get 'jquery-private'
-        // for their 'jquery' dependency.
-        '*': { 'jquery': 'jquery-private' },
-        // 'jquery-private' wants the real jQuery module
-        // though. If this line was not here, there would
-        // be an unresolvable cyclic dependency.
-        'jquery-private': { 'jquery': 'jquery' }
-    },
+    mainConfigFile: '../main.js',
     paths: {
-        "backbone":                 "components/backbone/backbone",
-        "backbone.browserStorage":  "components/backbone.browserStorage/backbone.browserStorage",
-        "backbone.overview":        "components/backbone.overview/backbone.overview",
-        "bootstrapJS":              "components/bootstrapJS/index",                  // XXX: Only required for https://conversejs.org website
-        "converse-dependencies":    "src/deps-website",
-        "converse-templates":       "src/templates",
-        "eventemitter":             "components/otr/build/dep/eventemitter",
-        "jquery":                   "components/jquery/dist/jquery",
-        "jquery-private":           "src/jquery-private",
-        "jquery.browser":           "components/jquery.browser/index",
-        "jquery.easing":            "components/jquery-easing-original/index", // XXX: Only required for https://conversejs.org website
-        "moment":                   "components/momentjs/moment",
-        "strophe":                  "components/strophe/strophe",
-        "strophe.disco":            "components/strophe.disco/index",
-        "strophe.muc":              "components/strophe.muc/index",
-        "strophe.roster":           "components/strophe.roster/index",
-        "strophe.vcard":            "components/strophe.vcard/index",
-        "text":                     'components/requirejs-text/text',
-        "tpl":                      'components/requirejs-tpl-jcbrand/tpl',
-        "typeahead":                "components/typeahead.js/index",
-        "underscore":               "components/underscore/underscore",
-        "utils":                    "src/utils",
-
-        // Off-the-record-encryption
-        "bigint":               "src/bigint",
-        "crypto":               "src/crypto",
-        "crypto.aes":           "components/otr/vendor/cryptojs/aes",
-        "crypto.cipher-core":   "components/otr/vendor/cryptojs/cipher-core",
-        "crypto.core":          "components/otr/vendor/cryptojs/core",
-        "crypto.enc-base64":    "components/otr/vendor/cryptojs/enc-base64",
-        "crypto.evpkdf":        "components/crypto-js-evanvosberg/src/evpkdf",
-        "crypto.hmac":          "components/otr/vendor/cryptojs/hmac",
-        "crypto.md5":           "components/crypto-js-evanvosberg/src/md5",
-        "crypto.mode-ctr":      "components/otr/vendor/cryptojs/mode-ctr",
-        "crypto.pad-nopadding": "components/otr/vendor/cryptojs/pad-nopadding",
-        "crypto.sha1":         "components/otr/vendor/cryptojs/sha1",
-        "crypto.sha256":        "components/otr/vendor/cryptojs/sha256",
-        "salsa20":              "components/otr/build/dep/salsa20",
-        "otr":                  "src/otr",
-
-        // Locales paths
-        "locales":   "locale/locales",
-        "jed":       "components/jed/jed",
-        "af":        "locale/af/LC_MESSAGES/af",
-        "de":        "locale/de/LC_MESSAGES/de",
-        "en":        "locale/en/LC_MESSAGES/en",
-        "es":        "locale/es/LC_MESSAGES/es",
-        "fr":        "locale/fr/LC_MESSAGES/fr",
-        "he":        "locale/he/LC_MESSAGES/he",
-        "hu":        "locale/hu/LC_MESSAGES/hu",
-        "id":        "locale/id/LC_MESSAGES/id",
-        "it":        "locale/it/LC_MESSAGES/it",
-        "ja":        "locale/ja/LC_MESSAGES/ja",
-        "nl":        "locale/nl/LC_MESSAGES/nl",
-        "pt_BR":     "locale/pt_BR/LC_MESSAGES/pt_BR",
-        "ru":        "locale/ru/LC_MESSAGES/ru",
-        "zh":        "locale/zh/LC_MESSAGES/zh",
-
-        // Templates
-        "action":                   "src/templates/action",
-        "add_contact_dropdown":     "src/templates/add_contact_dropdown",
-        "add_contact_form":         "src/templates/add_contact_form",
-        "change_status_message":    "src/templates/change_status_message",
-        "chat_status":              "src/templates/chat_status",
-        "chatarea":                 "src/templates/chatarea",
-        "chatbox":                  "src/templates/chatbox",
-        "chatroom":                 "src/templates/chatroom",
-        "chatroom_password_form":   "src/templates/chatroom_password_form",
-        "chatroom_sidebar":         "src/templates/chatroom_sidebar",
-        "chatrooms_tab":            "src/templates/chatrooms_tab",
-        "chats_panel":              "src/templates/chats_panel",
-        "choose_status":            "src/templates/choose_status",
-        "contacts_panel":           "src/templates/contacts_panel",
-        "contacts_tab":             "src/templates/contacts_tab",
-        "controlbox":               "src/templates/controlbox",
-        "controlbox_toggle":        "src/templates/controlbox_toggle",
-        "field":                    "src/templates/field",
-        "form_checkbox":            "src/templates/form_checkbox",
-        "form_input":               "src/templates/form_input",
-        "form_select":              "src/templates/form_select",
-        "group_header":             "src/templates/group_header",
-        "info":                     "src/templates/info",
-        "login_panel":              "src/templates/login_panel",
-        "login_tab":                "src/templates/login_tab",
-        "message":                  "src/templates/message",
-        "new_day":                  "src/templates/new_day",
-        "occupant":                 "src/templates/occupant",
-        "pending_contact":          "src/templates/pending_contact",
-        "pending_contacts":         "src/templates/pending_contacts",
-        "requesting_contact":       "src/templates/requesting_contact",
-        "requesting_contacts":      "src/templates/requesting_contacts",
-        "room_description":         "src/templates/room_description",
-        "room_item":                "src/templates/room_item",
-        "room_panel":               "src/templates/room_panel",
-        "roster":                   "src/templates/roster",
-        "roster_item":              "src/templates/roster_item",
-        "search_contact":           "src/templates/search_contact",
-        "select_option":            "src/templates/select_option",
-        "status_option":            "src/templates/status_option",
-        "toggle_chats":             "src/templates/toggle_chats",
-        "toolbar":                  "src/templates/toolbar",
-        "trimmed_chat":             "src/templates/trimmed_chat"
+        "converse-dependencies":    "src/deps-website"
     }
 })

+ 2 - 116
src/build.js

@@ -3,122 +3,8 @@
     name: "components/almond/almond.js",
     out: "../builds/converse.min.js",
     include: ['main'],
-    tpl: {
-        // Use Mustache style syntax for variable interpolation
-        templateSettings: {
-            evaluate : /\{\[([\s\S]+?)\]\}/g,
-            interpolate : /\{\{([\s\S]+?)\}\}/g
-        }
-    },
-    map: {
-        // '*' means all modules will get 'jquery-private'
-        // for their 'jquery' dependency.
-        '*': { 'jquery': 'jquery-private' },
-        // 'jquery-private' wants the real jQuery module
-        // though. If this line was not here, there would
-        // be an unresolvable cyclic dependency.
-        'jquery-private': { 'jquery': 'jquery' }
-    },
+    mainConfigFile: '../main.js',
     paths: {
-        "backbone":                 "components/backbone/backbone",
-        "backbone.browserStorage":  "components/backbone.browserStorage/backbone.browserStorage",
-        "backbone.overview":        "components/backbone.overview/backbone.overview",
-        "converse-dependencies":    "src/deps-full",
-        "converse-templates":       "src/templates",
-        "eventemitter":             "components/otr/build/dep/eventemitter",
-        "jquery":                   "components/jquery/dist/jquery",
-        "jquery-private":           "src/jquery-private",
-        "jquery.browser":           "components/jquery.browser/index",
-        "moment":                   "components/momentjs/moment",
-        "strophe":                  "components/strophe/strophe",
-        "strophe.disco":            "components/strophe.disco/index",
-        "strophe.muc":              "components/strophe.muc/index",
-        "strophe.roster":           "components/strophe.roster/index",
-        "strophe.vcard":            "components/strophe.vcard/index",
-        "text":                     'components/requirejs-text/text',
-        "tpl":                      'components/requirejs-tpl-jcbrand/tpl',
-        "typeahead":                "components/typeahead.js/index",
-        "underscore":               "components/underscore/underscore",
-        "utils":                    "src/utils",
-
-        // Off-the-record-encryption
-        "bigint":               "src/bigint",
-        "crypto":               "src/crypto",
-        "crypto.aes":           "components/otr/vendor/cryptojs/aes",
-        "crypto.cipher-core":   "components/otr/vendor/cryptojs/cipher-core",
-        "crypto.core":          "components/otr/vendor/cryptojs/core",
-        "crypto.enc-base64":    "components/otr/vendor/cryptojs/enc-base64",
-        "crypto.evpkdf":        "components/crypto-js-evanvosberg/src/evpkdf",
-        "crypto.hmac":          "components/otr/vendor/cryptojs/hmac",
-        "crypto.md5":           "components/crypto-js-evanvosberg/src/md5",
-        "crypto.mode-ctr":      "components/otr/vendor/cryptojs/mode-ctr",
-        "crypto.pad-nopadding": "components/otr/vendor/cryptojs/pad-nopadding",
-        "crypto.sha1":         "components/otr/vendor/cryptojs/sha1",
-        "crypto.sha256":        "components/otr/vendor/cryptojs/sha256",
-        "salsa20":              "components/otr/build/dep/salsa20",
-        "otr":                  "src/otr",
-
-        // Locales paths
-        "locales":   "locale/locales",
-        "jed":       "components/jed/jed",
-        "af":        "locale/af/LC_MESSAGES/af",
-        "de":        "locale/de/LC_MESSAGES/de",
-        "en":        "locale/en/LC_MESSAGES/en",
-        "es":        "locale/es/LC_MESSAGES/es",
-        "fr":        "locale/fr/LC_MESSAGES/fr",
-        "he":        "locale/he/LC_MESSAGES/he",
-        "hu":        "locale/hu/LC_MESSAGES/hu",
-        "id":        "locale/id/LC_MESSAGES/id",
-        "it":        "locale/it/LC_MESSAGES/it",
-        "ja":        "locale/ja/LC_MESSAGES/ja",
-        "nl":        "locale/nl/LC_MESSAGES/nl",
-        "pt_BR":     "locale/pt_BR/LC_MESSAGES/pt_BR",
-        "ru":        "locale/ru/LC_MESSAGES/ru",
-        "zh":        "locale/zh/LC_MESSAGES/zh",
-
-        // Templates
-        "action":                   "src/templates/action",
-        "add_contact_dropdown":     "src/templates/add_contact_dropdown",
-        "add_contact_form":         "src/templates/add_contact_form",
-        "change_status_message":    "src/templates/change_status_message",
-        "chat_status":              "src/templates/chat_status",
-        "chatarea":                 "src/templates/chatarea",
-        "chatbox":                  "src/templates/chatbox",
-        "chatroom":                 "src/templates/chatroom",
-        "chatroom_password_form":   "src/templates/chatroom_password_form",
-        "chatroom_sidebar":         "src/templates/chatroom_sidebar",
-        "chatrooms_tab":            "src/templates/chatrooms_tab",
-        "chats_panel":              "src/templates/chats_panel",
-        "choose_status":            "src/templates/choose_status",
-        "contacts_panel":           "src/templates/contacts_panel",
-        "contacts_tab":             "src/templates/contacts_tab",
-        "controlbox":               "src/templates/controlbox",
-        "controlbox_toggle":        "src/templates/controlbox_toggle",
-        "field":                    "src/templates/field",
-        "form_checkbox":            "src/templates/form_checkbox",
-        "form_input":               "src/templates/form_input",
-        "form_select":              "src/templates/form_select",
-        "group_header":             "src/templates/group_header",
-        "info":                     "src/templates/info",
-        "login_panel":              "src/templates/login_panel",
-        "login_tab":                "src/templates/login_tab",
-        "message":                  "src/templates/message",
-        "new_day":                  "src/templates/new_day",
-        "occupant":                 "src/templates/occupant",
-        "pending_contact":          "src/templates/pending_contact",
-        "pending_contacts":         "src/templates/pending_contacts",
-        "requesting_contact":       "src/templates/requesting_contact",
-        "requesting_contacts":      "src/templates/requesting_contacts",
-        "room_description":         "src/templates/room_description",
-        "room_item":                "src/templates/room_item",
-        "room_panel":               "src/templates/room_panel",
-        "roster":                   "src/templates/roster",
-        "roster_item":              "src/templates/roster_item",
-        "search_contact":           "src/templates/search_contact",
-        "select_option":            "src/templates/select_option",
-        "status_option":            "src/templates/status_option",
-        "toggle_chats":             "src/templates/toggle_chats",
-        "toolbar":                  "src/templates/toolbar",
-        "trimmed_chat":             "src/templates/trimmed_chat"
+        "converse-dependencies":    "src/deps-full"
     }
 })

+ 3 - 0
src/jquery-external.js

@@ -0,0 +1,3 @@
+define('jquery', [], function () {
+    return jQuery;
+});

+ 3 - 0
src/jquery-private-external.js

@@ -0,0 +1,3 @@
+define(['jquery'], function (jq) {
+    return jq;
+});

+ 447 - 0
src/strophe.roster.js

@@ -0,0 +1,447 @@
+/*
+  Copyright 2010, François de Metz <francois@2metz.fr>
+*/
+/**
+ * Roster Plugin
+ * Allow easily roster management
+ *
+ *  Features
+ *  * Get roster from server
+ *  * handle presence
+ *  * handle roster iq
+ *  * subscribe/unsubscribe
+ *  * authorize/unauthorize
+ *  * roster versioning (xep 237)
+ */
+Strophe.addConnectionPlugin('roster',
+{
+    /** Function: init
+     * Plugin init
+     *
+     * Parameters:
+     *   (Strophe.Connection) conn - Strophe connection
+     */
+    init: function(conn)
+    {
+        this._connection = conn;
+        this._callbacks = [];
+        /** Property: items
+         *  Roster items
+         *  [
+         *    {
+         *        name         : "",
+         *        jid          : "",
+         *        subscription : "",
+         *        ask          : "",
+         *        groups       : ["", ""],
+         *        resources    : {
+         *            myresource : {
+         *                show   : "",
+         *                status : "",
+         *                priority : ""
+         *            }
+         *        }
+         *    }
+         * ]
+         */
+        this.items = [];
+        /** Property: ver
+        * current roster revision
+        * always null if server doesn't support xep 237
+        */
+        this.ver = null;
+        // Override the connect and attach methods to always add presence and roster handlers.
+        // They are removed when the connection disconnects, so must be added on connection.
+        var oldCallback, roster = this, _connect = conn.connect, _attach = conn.attach;
+        var newCallback = function(status)
+        {
+            if (status == Strophe.Status.ATTACHED || status == Strophe.Status.CONNECTED)
+            {
+                try
+                {
+                    // Presence subscription
+                    conn.addHandler(roster._onReceivePresence.bind(roster), null, 'presence', null, null, null);
+                    conn.addHandler(roster._onReceiveIQ.bind(roster), Strophe.NS.ROSTER, 'iq', "set", null, null);
+                }
+                catch (e)
+                {
+                    Strophe.error(e);
+                }
+            }
+            if (typeof oldCallback === "function") {
+                oldCallback.apply(this, arguments);
+            }
+        };
+        conn.connect = function(jid, pass, callback, wait, hold, route)
+        {
+            oldCallback = callback;
+            if (typeof jid  == "undefined")
+                jid  = null;
+            if (typeof pass == "undefined")
+                pass = null;
+            callback = newCallback;
+            _connect.apply(conn, [jid, pass, callback, wait, hold, route]);
+        };
+        conn.attach = function(jid, sid, rid, callback, wait, hold, wind)
+        {
+            oldCallback = callback;
+            if (typeof jid == "undefined")
+                jid = null;
+            if (typeof sid == "undefined")
+                sid = null;
+            if (typeof rid == "undefined")
+                rid = null;
+            callback = newCallback;
+            _attach.apply(conn, [jid, sid, rid, callback, wait, hold, wind]);
+        };
+
+        Strophe.addNamespace('ROSTER_VER', 'urn:xmpp:features:rosterver');
+        Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
+    },
+    /** Function: supportVersioning
+     * return true if roster versioning is enabled on server
+     */
+    supportVersioning: function()
+    {
+        return (this._connection.features && this._connection.features.getElementsByTagName('ver').length > 0);
+    },
+    /** Function: get
+     * Get Roster on server
+     *
+     * Parameters:
+     *   (Function) userCallback - callback on roster result
+     *   (String) ver - current rev of roster
+     *      (only used if roster versioning is enabled)
+     *   (Array) items - initial items of ver
+     *      (only used if roster versioning is enabled)
+     *     In browser context you can use sessionStorage
+     *     to store your roster in json (JSON.stringify())
+     */
+    get: function(userCallback, ver, items)
+    {
+        var attrs = {xmlns: Strophe.NS.ROSTER};
+        if (this.supportVersioning())
+        {
+            // empty rev because i want an rev attribute in the result
+            attrs.ver = ver || '';
+            this.items = items || [];
+        }
+        var iq = $iq({type: 'get',  'id' : this._connection.getUniqueId('roster')}).c('query', attrs);
+        return this._connection.sendIQ(iq,
+                                this._onReceiveRosterSuccess.bind(this, userCallback),
+                                this._onReceiveRosterError.bind(this, userCallback));
+    },
+    /** Function: registerCallback
+     * register callback on roster (presence and iq)
+     *
+     * Parameters:
+     *   (Function) call_back
+     */
+    registerCallback: function(call_back)
+    {
+        this._callbacks.push(call_back);
+    },
+    /** Function: findItem
+     * Find item by JID
+     *
+     * Parameters:
+     *     (String) jid
+     */
+    findItem : function(jid)
+    {
+        try {
+            for (var i = 0; i < this.items.length; i++)
+            {
+                if (this.items[i] && this.items[i].jid == jid)
+                {
+                    return this.items[i];
+                }
+            }
+        } catch (e)
+        {
+            Strophe.error(e);
+        }
+        return false;
+    },
+    /** Function: removeItem
+     * Remove item by JID
+     *
+     * Parameters:
+     *     (String) jid
+     */
+    removeItem : function(jid)
+    {
+        for (var i = 0; i < this.items.length; i++)
+        {
+            if (this.items[i] && this.items[i].jid == jid)
+            {
+                this.items.splice(i, 1);
+                return true;
+            }
+        }
+        return false;
+    },
+    /** Function: subscribe
+     * Subscribe presence
+     *
+     * Parameters:
+     *     (String) jid
+     *     (String) message (optional)
+     *     (String) nick  (optional)
+     */
+    subscribe: function(jid, message, nick) {
+        var pres = $pres({to: jid, type: "subscribe"});
+        if (message && message !== "") {
+            pres.c("status").t(message).up();
+        }
+        if (nick && nick !== "") {
+            pres.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
+        }
+        this._connection.send(pres);
+    },
+    /** Function: unsubscribe
+     * Unsubscribe presence
+     *
+     * Parameters:
+     *     (String) jid
+     *     (String) message
+     */
+    unsubscribe: function(jid, message)
+    {
+        var pres = $pres({to: jid, type: "unsubscribe"});
+        if (message && message !== "")
+            pres.c("status").t(message);
+        this._connection.send(pres);
+    },
+    /** Function: authorize
+     * Authorize presence subscription
+     *
+     * Parameters:
+     *     (String) jid
+     *     (String) message
+     */
+    authorize: function(jid, message)
+    {
+        var pres = $pres({to: jid, type: "subscribed"});
+        if (message && message !== "")
+            pres.c("status").t(message);
+        this._connection.send(pres);
+    },
+    /** Function: unauthorize
+     * Unauthorize presence subscription
+     *
+     * Parameters:
+     *     (String) jid
+     *     (String) message
+     */
+    unauthorize: function(jid, message)
+    {
+        var pres = $pres({to: jid, type: "unsubscribed"});
+        if (message && message !== "")
+            pres.c("status").t(message);
+        this._connection.send(pres);
+    },
+    /** Function: add
+     * Add roster item
+     *
+     * Parameters:
+     *   (String) jid - item jid
+     *   (String) name - name
+     *   (Array) groups
+     *   (Function) call_back
+     */
+    add: function(jid, name, groups, call_back)
+    {
+        var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: jid,
+                                                                                      name: name});
+        for (var i = 0; i < groups.length; i++)
+        {
+            iq.c('group').t(groups[i]).up();
+        }
+        this._connection.sendIQ(iq, call_back, call_back);
+    },
+    /** Function: update
+     * Update roster item
+     *
+     * Parameters:
+     *   (String) jid - item jid
+     *   (String) name - name
+     *   (Array) groups
+     *   (Function) call_back
+     */
+    update: function(jid, name, groups, call_back)
+    {
+        var item = this.findItem(jid);
+        if (!item)
+        {
+            throw "item not found";
+        }
+        var newName = name || item.name;
+        var newGroups = groups || item.groups;
+        var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: item.jid,
+                                                                                      name: newName});
+        for (var i = 0; i < newGroups.length; i++)
+        {
+            iq.c('group').t(newGroups[i]).up();
+        }
+        return this._connection.sendIQ(iq, call_back, call_back);
+    },
+    /** Function: remove
+     * Remove roster item
+     *
+     * Parameters:
+     *   (String) jid - item jid
+     *   (Function) call_back
+     */
+    remove: function(jid, call_back)
+    {
+        var item = this.findItem(jid);
+        if (!item)
+        {
+            throw "item not found";
+        }
+        var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: item.jid,
+                                                                                      subscription: "remove"});
+        this._connection.sendIQ(iq, call_back, call_back);
+    },
+    /** PrivateFunction: _onReceiveRosterSuccess
+     *
+     */
+    _onReceiveRosterSuccess: function(userCallback, stanza)
+    {
+        this._updateItems(stanza);
+        if (typeof userCallback === "function") {
+            userCallback(this.items);
+        }
+    },
+    /** PrivateFunction: _onReceiveRosterError
+     *
+     */
+    _onReceiveRosterError: function(userCallback, stanza)
+    {
+        userCallback(this.items);
+    },
+    /** PrivateFunction: _onReceivePresence
+     * Handle presence
+     */
+    _onReceivePresence : function(presence)
+    {
+        // TODO: from is optional
+        var jid = presence.getAttribute('from');
+        var from = Strophe.getBareJidFromJid(jid);
+        var item = this.findItem(from);
+        // not in roster
+        if (!item)
+        {
+            return true;
+        }
+        var type = presence.getAttribute('type');
+        if (type == 'unavailable')
+        {
+            delete item.resources[Strophe.getResourceFromJid(jid)];
+        }
+        else if (!type)
+        {
+            // TODO: add timestamp
+            item.resources[Strophe.getResourceFromJid(jid)] = {
+                show     : (presence.getElementsByTagName('show').length !== 0) ? Strophe.getText(presence.getElementsByTagName('show')[0]) : "",
+                status   : (presence.getElementsByTagName('status').length !== 0) ? Strophe.getText(presence.getElementsByTagName('status')[0]) : "",
+                priority : (presence.getElementsByTagName('priority').length !== 0) ? Strophe.getText(presence.getElementsByTagName('priority')[0]) : ""
+            };
+        }
+        else
+        {
+            // Stanza is not a presence notification. (It's probably a subscription type stanza.)
+            return true;
+        }
+        this._call_backs(this.items, item);
+        return true;
+    },
+    /** PrivateFunction: _call_backs
+     *
+     */
+    _call_backs : function(items, item)
+    {
+        for (var i = 0; i < this._callbacks.length; i++) // [].forEach my love ...
+        {
+            this._callbacks[i](items, item);
+        }
+    },
+    /** PrivateFunction: _onReceiveIQ
+     * Handle roster push.
+     */
+    _onReceiveIQ : function(iq)
+    {
+        var id = iq.getAttribute('id');
+        var from = iq.getAttribute('from');
+        // Receiving client MUST ignore stanza unless it has no from or from = user's JID.
+        if (from && from !== "" && from != this._connection.jid && from != Strophe.getBareJidFromJid(this._connection.jid))
+            return true;
+        var iqresult = $iq({type: 'result', id: id, from: this._connection.jid});
+        this._connection.send(iqresult);
+        this._updateItems(iq);
+        return true;
+    },
+    /** PrivateFunction: _updateItems
+     * Update items from iq
+     */
+    _updateItems : function(iq)
+    {
+        var query = iq.getElementsByTagName('query');
+        if (query.length !== 0)
+        {
+            this.ver = query.item(0).getAttribute('ver');
+            var self = this;
+            Strophe.forEachChild(query.item(0), 'item',
+                function (item)
+                {
+                    self._updateItem(item);
+                }
+           );
+        }
+        this._call_backs(this.items);
+    },
+    /** PrivateFunction: _updateItem
+     * Update internal representation of roster item
+     */
+    _updateItem : function(item)
+    {
+        var jid           = item.getAttribute("jid");
+        var name          = item.getAttribute("name");
+        var subscription  = item.getAttribute("subscription");
+        var ask           = item.getAttribute("ask");
+        var groups        = [];
+        Strophe.forEachChild(item, 'group',
+            function(group)
+            {
+                groups.push(Strophe.getText(group));
+            }
+        );
+
+        if (subscription == "remove")
+        {
+            this.removeItem(jid);
+            return;
+        }
+
+        item = this.findItem(jid);
+        if (!item)
+        {
+            this.items.push({
+                name         : name,
+                jid          : jid,
+                subscription : subscription,
+                ask          : ask,
+                groups       : groups,
+                resources    : {}
+            });
+        }
+        else
+        {
+            item.name = name;
+            item.subscription = subscription;
+            item.ask = ask;
+            item.groups = groups;
+        }
+    }
+});

+ 2 - 3
src/templates/roster.html

@@ -1,6 +1,5 @@
-<input class="roster-filter" placeholder="{{placeholder}}">
-<select class="filter-type">
+<input style="display: none;" class="roster-filter" placeholder="{{placeholder}}">
+<select style="display: none;" class="filter-type">
     <option value="contacts">{{label_contacts}}</option>
     <option value="groups">{{label_groups}}</option>
 </select>
-<dl class="roster-contacts"></dl>

+ 2 - 2
src/utils.js

@@ -30,7 +30,7 @@ define(["jquery"], function ($) {
     var utils = {
         // Translation machinery
         // ---------------------
-        __: $.proxy(function (str) {
+        __: function (str) {
             // Translation factory
             if (this.i18n === undefined) {
                 this.i18n = locales.en;
@@ -41,7 +41,7 @@ define(["jquery"], function ($) {
             } else {
                 return t.fetch();
             }
-        }, this),
+        },
 
         ___: function (str) {
             /* XXX: This is part of a hack to get gettext to scan strings to be

+ 153 - 4
tests/main.js

@@ -1,3 +1,152 @@
+config = {
+    baseUrl: '.',
+    paths: {
+        "backbone":                 "components/backbone/backbone",
+        "backbone.browserStorage":  "components/backbone.browserStorage/backbone.browserStorage",
+        "backbone.overview":        "components/backbone.overview/backbone.overview",
+        "bootstrap":                "components/bootstrap/dist/js/bootstrap",           // XXX: Only required for https://conversejs.org website
+        "bootstrapJS":              "components/bootstrapJS/index",                     // XXX: Only required for https://conversejs.org website
+        "converse-dependencies":    "src/deps-website",
+        "converse-templates":       "src/templates",
+        "eventemitter":             "components/otr/build/dep/eventemitter",
+        "jquery":                   "components/jquery/dist/jquery",
+        "jquery-private":           "src/jquery-private",
+        "jquery.browser":           "components/jquery.browser/index",
+        "jquery.easing":            "components/jquery-easing-original/index",          // XXX: Only required for https://conversejs.org website
+        "moment":                   "components/momentjs/moment",
+        "strophe":                  "components/strophe/strophe",
+        "strophe.disco":            "components/strophejs-plugins/disco/strophe.disco",
+        "strophe.muc":              "components/strophe.muc/index",
+        "strophe.roster":           "src/strophe.roster",
+        "strophe.vcard":            "components/strophejs-plugins/vcard/strophe.vcard",
+        "text":                     'components/requirejs-text/text',
+        "tpl":                      'components/requirejs-tpl-jcbrand/tpl',
+        "typeahead":                "components/typeahead.js/index",
+        "underscore":               "components/underscore/underscore",
+        "utils":                    "src/utils",
+
+        // Off-the-record-encryption
+        "bigint":               "src/bigint",
+        "crypto":               "src/crypto",
+        "crypto.aes":           "components/otr/vendor/cryptojs/aes",
+        "crypto.cipher-core":   "components/otr/vendor/cryptojs/cipher-core",
+        "crypto.core":          "components/otr/vendor/cryptojs/core",
+        "crypto.enc-base64":    "components/otr/vendor/cryptojs/enc-base64",
+        "crypto.evpkdf":        "components/crypto-js-evanvosberg/src/evpkdf",
+        "crypto.hmac":          "components/otr/vendor/cryptojs/hmac",
+        "crypto.md5":           "components/crypto-js-evanvosberg/src/md5",
+        "crypto.mode-ctr":      "components/otr/vendor/cryptojs/mode-ctr",
+        "crypto.pad-nopadding": "components/otr/vendor/cryptojs/pad-nopadding",
+        "crypto.sha1":         "components/otr/vendor/cryptojs/sha1",
+        "crypto.sha256":        "components/otr/vendor/cryptojs/sha256",
+        "salsa20":              "components/otr/build/dep/salsa20",
+        "otr":                  "src/otr",
+
+        // Locales paths
+        "locales":   "locale/locales",
+        "jed":       "components/jed/jed",
+        "af":        "locale/af/LC_MESSAGES/af",
+        "de":        "locale/de/LC_MESSAGES/de",
+        "en":        "locale/en/LC_MESSAGES/en",
+        "es":        "locale/es/LC_MESSAGES/es",
+        "fr":        "locale/fr/LC_MESSAGES/fr",
+        "he":        "locale/he/LC_MESSAGES/he",
+        "hu":        "locale/hu/LC_MESSAGES/hu",
+        "id":        "locale/id/LC_MESSAGES/id",
+        "it":        "locale/it/LC_MESSAGES/it",
+        "ja":        "locale/ja/LC_MESSAGES/ja",
+        "nl":        "locale/nl/LC_MESSAGES/nl",
+        "pt_BR":     "locale/pt_BR/LC_MESSAGES/pt_BR",
+        "ru":        "locale/ru/LC_MESSAGES/ru",
+        "zh":        "locale/zh/LC_MESSAGES/zh",
+
+        // Templates
+        "action":                   "src/templates/action",
+        "add_contact_dropdown":     "src/templates/add_contact_dropdown",
+        "add_contact_form":         "src/templates/add_contact_form",
+        "change_status_message":    "src/templates/change_status_message",
+        "chat_status":              "src/templates/chat_status",
+        "chatarea":                 "src/templates/chatarea",
+        "chatbox":                  "src/templates/chatbox",
+        "chatroom":                 "src/templates/chatroom",
+        "chatroom_password_form":   "src/templates/chatroom_password_form",
+        "chatroom_sidebar":         "src/templates/chatroom_sidebar",
+        "chatrooms_tab":            "src/templates/chatrooms_tab",
+        "chats_panel":              "src/templates/chats_panel",
+        "choose_status":            "src/templates/choose_status",
+        "contacts_panel":           "src/templates/contacts_panel",
+        "contacts_tab":             "src/templates/contacts_tab",
+        "controlbox":               "src/templates/controlbox",
+        "controlbox_toggle":        "src/templates/controlbox_toggle",
+        "field":                    "src/templates/field",
+        "form_checkbox":            "src/templates/form_checkbox",
+        "form_input":               "src/templates/form_input",
+        "form_select":              "src/templates/form_select",
+        "group_header":             "src/templates/group_header",
+        "info":                     "src/templates/info",
+        "login_panel":              "src/templates/login_panel",
+        "login_tab":                "src/templates/login_tab",
+        "message":                  "src/templates/message",
+        "new_day":                  "src/templates/new_day",
+        "occupant":                 "src/templates/occupant",
+        "pending_contact":          "src/templates/pending_contact",
+        "pending_contacts":         "src/templates/pending_contacts",
+        "requesting_contact":       "src/templates/requesting_contact",
+        "requesting_contacts":      "src/templates/requesting_contacts",
+        "room_description":         "src/templates/room_description",
+        "room_item":                "src/templates/room_item",
+        "room_panel":               "src/templates/room_panel",
+        "roster":                   "src/templates/roster",
+        "roster_item":              "src/templates/roster_item",
+        "search_contact":           "src/templates/search_contact",
+        "select_option":            "src/templates/select_option",
+        "status_option":            "src/templates/status_option",
+        "toggle_chats":             "src/templates/toggle_chats",
+        "toolbar":                  "src/templates/toolbar",
+        "trimmed_chat":             "src/templates/trimmed_chat"
+    },
+
+    map: {
+        // '*' means all modules will get 'jquery-private'
+        // for their 'jquery' dependency.
+        '*': { 'jquery': 'jquery-private' },
+        // 'jquery-private' wants the real jQuery module
+        // though. If this line was not here, there would
+        // be an unresolvable cyclic dependency.
+        'jquery-private': { 'jquery': 'jquery' }
+    },
+
+    tpl: {
+        // Configuration for requirejs-tpl
+        // Use Mustache style syntax for variable interpolation
+        templateSettings: {
+            evaluate : /\{\[([\s\S]+?)\]\}/g,
+            interpolate : /\{\{([\s\S]+?)\}\}/g
+        }
+    },
+
+    // define module dependencies for modules not using define
+    shim: {
+        'underscore':           { exports: '_' },
+        'crypto.aes':           { deps: ['crypto.cipher-core'] },
+        'crypto.cipher-core':   { deps: ['crypto.enc-base64', 'crypto.evpkdf'] },
+        'crypto.enc-base64':    { deps: ['crypto.core'] },
+        'crypto.evpkdf':        { deps: ['crypto.md5'] },
+        'crypto.hmac':          { deps: ['crypto.core'] },
+        'crypto.md5':           { deps: ['crypto.core'] },
+        'crypto.mode-ctr':      { deps: ['crypto.cipher-core'] },
+        'crypto.pad-nopadding': { deps: ['crypto.cipher-core'] },
+        'crypto.sha1':          { deps: ['crypto.core'] },
+        'crypto.sha256':        { deps: ['crypto.core'] },
+        'bigint':               { deps: ['crypto'] },
+        'strophe':              { exports: 'Strophe' },
+        'strophe.disco':        { deps: ['strophe'] },
+        'strophe.muc':          { deps: ['strophe'] },
+        'strophe.roster':       { deps: ['strophe'] },
+        'strophe.vcard':        { deps: ['strophe'] }
+    }
+};
+
 // Extra test dependencies
 config.paths.mock = "tests/mock";
 config.paths.test_utils = "tests/utils";
@@ -40,8 +189,6 @@ require([
         window.converse_api = converse;
         window.localStorage.clear();
         window.sessionStorage.clear();
-        // XXX: call this to initialize Strophe plugins
-        new Strophe.Connection('localhost');
 
         converse.initialize({
             prebind: false,
@@ -49,7 +196,8 @@ require([
             auto_subscribe: false,
             animate: false,
             connection: mock.mock_connection,
-            no_trimming: true
+            no_trimming: true,
+            debug: true
         }, function (converse) {
             window.converse = converse;
             window.crypto = {
@@ -68,7 +216,8 @@ require([
                 "spec/controlbox",
                 "spec/chatbox",
                 "spec/chatroom",
-                "spec/minchats"
+                "spec/minchats",
+                "spec/profiling"
             ], function () {
                 // Make sure this callback is only called once.
                 delete converse.callback;

+ 32 - 6
tests/mock.js

@@ -36,11 +36,37 @@
         'preventDefault': function () {}
     };
 
-    mock.mock_connection = {
-        '_proto': {},
-        'connected': true,
-        'authenticated': true,
-        'mock': true,
+    mock.mock_connection = function ()  {
+        Strophe.Bosh.prototype._processRequest = function () {}; // Don't attempt to send out stanzas
+        var c = new Strophe.Connection('jasmine tests');
+        c.authenticated = true;
+        c.connected = true;
+        c.mock = true;
+        c.jid = 'dummy@localhost/resource';
+        c.vcard = {
+            'get': function (callback, jid) {
+                var fullname;
+                if (!jid) {
+                    jid = 'dummy@localhost';
+                    fullname = 'Max Mustermann' ;
+                } else {
+                    var name = jid.split('@')[0].replace(/\./g, ' ').split(' ');
+                    var last = name.length-1;
+                    name[0] =  name[0].charAt(0).toUpperCase()+name[0].slice(1);
+                    name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1);
+                    fullname = name.join(' ');
+                }
+                var vcard = $iq().c('vCard').c('FN').t(fullname);
+                callback(vcard.tree());
+            }
+        };
+        c._changeConnectStatus(Strophe.Status.CONNECTED);
+        c.attach(c.jid);
+        return c;
+    }();
+
+    /*
+    {
         'muc': {
             'listRooms': function () {},
             'join': function () {},
@@ -49,7 +75,6 @@
             'groupchat': function () {return String((new Date()).getTime()); }
         },
         'service': 'jasmine tests',
-        'jid': 'dummy@localhost',
         'addHandler': function (handler, ns, name, type, id, from, options) {
             return function () {};
         },
@@ -87,5 +112,6 @@
             'items': function () {}
         }
     };
+    */
     return mock;
 }));

+ 11 - 0
tests/utils.js

@@ -8,6 +8,17 @@
         });
 }(this, function ($, mock) {
     var utils = {};
+
+    utils.createRequest = function (iq) {
+        iq = typeof iq.tree == "function" ? iq.tree() : iq;
+        var req = new Strophe.Request(iq, function() {});
+        req.getResponse = function() { 
+            var env = new Strophe.Builder('env', {type: 'mock'}).tree();
+            env.appendChild(iq);
+            return env;
+        };
+        return req;
+    };
     
     utils.closeAllChatBoxes = function () {
         var i, chatbox;

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно