Forráskód Böngészése

Merge branch 'master' into plugin-api

JC Brand 10 éve
szülő
commit
36db4c8b27

+ 7 - 0
Gruntfile.js

@@ -99,10 +99,17 @@ module.exports = function(grunt) {
             done();
             done();
         };
         };
         exec('./node_modules/requirejs/bin/r.js -o src/build.js && ' +
         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 && ' +
+             './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 &&' +
+             './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-no-otr.js &&' +
              './node_modules/requirejs/bin/r.js -o src/build-website.js', callback);
              './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']);
     grunt.registerTask('minify', 'Create a new release', ['cssmin', 'jsmin']);

+ 1 - 2
bower.json

@@ -16,7 +16,6 @@
     "backbone.browserStorage": "*",
     "backbone.browserStorage": "*",
     "backbone.overview": "*",
     "backbone.overview": "*",
     "strophe": "~1.1.3",
     "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",
     "strophe.muc": "https://raw.githubusercontent.com/strophe/strophejs-plugins/master/muc/strophe.muc.js",
     "otr": "0.2.12",
     "otr": "0.2.12",
     "crypto-js-evanvosberg": "~3.1.2",
     "crypto-js-evanvosberg": "~3.1.2",
@@ -30,7 +29,7 @@
     "bootstrapJS": "https://raw.githubusercontent.com/jcbrand/bootstrap/7d96a5f60d26c67b5348b270a775518b96a702c8/dist/js/bootstrap.js",
     "bootstrapJS": "https://raw.githubusercontent.com/jcbrand/bootstrap/7d96a5f60d26c67b5348b270a775518b96a702c8/dist/js/bootstrap.js",
     "fontawesome": "~4.1.0",
     "fontawesome": "~4.1.0",
     "typeahead.js": "https://raw.githubusercontent.com/jcbrand/typeahead.js/eedfb10505dd3a20123d1fafc07c1352d83f0ab3/dist/typeahead.jquery.js",
     "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": {}
   "exportsOverride": {}
 }
 }

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 13947 - 0
builds/converse-no-locales-no-otr.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
builds/converse-no-locales-no-otr.min.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 13947 - 0
builds/converse-no-otr.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
builds/converse-no-otr.min.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 21666 - 0
builds/converse.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
builds/converse.min.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 11332 - 0
builds/converse.nojquery.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 203 - 0
builds/converse.nojquery.min.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
builds/converse.website-no-otr.min.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 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) +
 ((__t = (label_contact_username)) == null ? '' : __t) +
 '"/>\n        <button type="submit">' +
 '"/>\n        <button type="submit">' +
 ((__t = (label_add)) == null ? '' : __t) +
 ((__t = (label_add)) == null ? '' : __t) +
-'</button>\n    </form>\n<li>\n';
+'</button>\n    </form>\n</li>\n';
 
 
 }
 }
 return __p
 return __p
@@ -380,7 +380,7 @@ __p += '<form id="converse-login" method="post">\n    <label>' +
 ((__t = (label_password)) == null ? '' : __t) +
 ((__t = (label_password)) == null ? '' : __t) +
 '</label>\n    <input type="password" name="password" placeholder="Password">\n    <input class="login-submit" type="submit" value="' +
 '</label>\n    <input type="password" name="password" placeholder="Password">\n    <input class="login-submit" type="submit" value="' +
 ((__t = (label_login)) == null ? '' : __t) +
 ((__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
 return __p
@@ -660,13 +660,13 @@ this["JST"]["roster"] = function(obj) {
 obj || (obj = {});
 obj || (obj = {});
 var __t, __p = '', __e = _.escape;
 var __t, __p = '', __e = _.escape;
 with (obj) {
 with (obj) {
-__p += '<input class="roster-filter" placeholder="' +
+__p += '<input style="display: none;" class="roster-filter" placeholder="' +
 ((__t = (placeholder)) == null ? '' : __t) +
 ((__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) +
 ((__t = (label_contacts)) == null ? '' : __t) +
 '</option>\n    <option value="groups">' +
 '</option>\n    <option value="groups">' +
 ((__t = (label_groups)) == null ? '' : __t) +
 ((__t = (label_groups)) == null ? '' : __t) +
-'</option>\n</select>\n<dl class="roster-contacts"></dl>\n';
+'</option>\n</select>\n';
 
 
 }
 }
 return __p
 return __p
@@ -700,7 +700,7 @@ __p += '<li>\n    <form class="search-xmpp-contact">\n        <input type="text"
 ((__t = (label_contact_name)) == null ? '' : __t) +
 ((__t = (label_contact_name)) == null ? '' : __t) +
 '"/>\n        <button type="submit">' +
 '"/>\n        <button type="submit">' +
 ((__t = (label_search)) == null ? '' : __t) +
 ((__t = (label_search)) == null ? '' : __t) +
-'</button>\n    </form>\n<li>\n';
+'</button>\n    </form>\n</li>\n';
 
 
 }
 }
 return __p
 return __p

+ 152 - 68
converse.js

@@ -300,7 +300,7 @@
 
 
         // Translation machinery
         // Translation machinery
         // ---------------------
         // ---------------------
-        var __ = utils.__;
+        var __ = $.proxy(utils.__, this);
         var ___ = utils.___;
         var ___ = utils.___;
         // Translation aware constants
         // Translation aware constants
         // ---------------------------
         // ---------------------------
@@ -541,6 +541,7 @@
         };
         };
 
 
         this.clearSession = function () {
         this.clearSession = function () {
+            this.roster.browserStorage._clear();
             this.session.browserStorage._clear();
             this.session.browserStorage._clear();
             // XXX: this should perhaps go into the beforeunload handler
             // XXX: this should perhaps go into the beforeunload handler
             converse.chatboxes.get('controlbox').save({'connected': false});
             converse.chatboxes.get('controlbox').save({'connected': false});
@@ -638,7 +639,9 @@
             if (this.debug) {
             if (this.debug) {
                 this.connection.xmlInput = function (body) { console.log(body); };
                 this.connection.xmlInput = function (body) { console.log(body); };
                 this.connection.xmlOutput = 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) {
                 Strophe.error = function (msg) {
                     console.log('ERROR: '+msg);
                     console.log('ERROR: '+msg);
                 };
                 };
@@ -906,6 +909,9 @@
 
 
                 if (!body) {
                 if (!body) {
                     if (composing.length || paused.length) {
                     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({
                         this.messages.add({
                             fullname: fullname,
                             fullname: fullname,
                             sender: 'them',
                             sender: 'them',
@@ -1597,7 +1603,7 @@
                     label_away: __('Away'),
                     label_away: __('Away'),
                     label_offline: __('Offline'),
                     label_offline: __('Offline'),
                     label_logout: __('Log out'),
                     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}));
                 this.$tabs.append(converse.templates.contacts_tab({label_contacts: LABEL_CONTACTS}));
                 if (converse.xhr_user_search) {
                 if (converse.xhr_user_search) {
@@ -1933,8 +1939,16 @@
                     b64_sha1('converse.roster.groups'+converse.bare_jid));
                     b64_sha1('converse.roster.groups'+converse.bare_jid));
                 converse.rosterview = new converse.RosterView({model: rostergroups});
                 converse.rosterview = new converse.RosterView({model: rostergroups});
                 this.contactspanel.$el.append(converse.rosterview.$el);
                 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.rosterview.render().fetch().update();
-                converse.connection.roster.get(function () {});
                 return this;
                 return this;
             },
             },
 
 
@@ -2079,7 +2093,7 @@
             initialize: function (options) {
             initialize: function (options) {
                 this.browserStorage = new Backbone.BrowserStorage[converse.storage](
                 this.browserStorage = new Backbone.BrowserStorage[converse.storage](
                     b64_sha1('converse.occupants'+converse.bare_jid+options.nick));
                     b64_sha1('converse.occupants'+converse.bare_jid+options.nick));
-            },
+            }
         });
         });
 
 
         this.ChatRoomOccupantsView = Backbone.Overview.extend({
         this.ChatRoomOccupantsView = Backbone.Overview.extend({
@@ -2169,7 +2183,7 @@
                     $(ev.target).typeahead('val', '');
                     $(ev.target).typeahead('val', '');
                 }, this));
                 }, this));
                 return this;
                 return this;
-            },
+            }
 
 
         });
         });
 
 
@@ -2210,7 +2224,7 @@
                 this);
                 this);
 
 
                 this.occupantsview = new converse.ChatRoomOccupantsView({
                 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.occupantsview.chatroomview = this;
                 this.render();
                 this.render();
@@ -2243,7 +2257,7 @@
                         .append(
                         .append(
                             converse.templates.chatarea({
                             converse.templates.chatarea({
                                 'show_toolbar': converse.show_toolbar,
                                 'show_toolbar': converse.show_toolbar,
-                                'label_message': __('Message'),
+                                'label_message': __('Message')
                             }))
                             }))
                         .append(this.occupantsview.render().$el);
                         .append(this.occupantsview.render().$el);
                     this.renderToolbar();
                     this.renderToolbar();
@@ -2556,7 +2570,7 @@
                 172: __('This room is now non-anonymous'),
                 172: __('This room is now non-anonymous'),
                 173: __('This room is now semi-anonymous'),
                 173: __('This room is now semi-anonymous'),
                 174: __('This room is now fully-anonymous'),
                 174: __('This room is now fully-anonymous'),
-                201: __('A new room has been created'),
+                201: __('A new room has been created')
             },
             },
 
 
             disconnectMessages: {
             disconnectMessages: {
@@ -2710,7 +2724,7 @@
                     delayed = $message.find('delay').length > 0,
                     delayed = $message.find('delay').length > 0,
                     subject = $message.children('subject').text();
                     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.
                     return true; // We already have this message stored.
                 }
                 }
                 this.showStatusMessages($message);
                 this.showStatusMessages($message);
@@ -2829,6 +2843,8 @@
             onMessage: function (message) {
             onMessage: function (message) {
                 var $message = $(message);
                 var $message = $(message);
                 var buddy_jid, $forwarded, $received,
                 var buddy_jid, $forwarded, $received,
+                    msgid = $message.attr('id'),
+                    chatbox, resource, roster_item,
                     message_from = $message.attr('from');
                     message_from = $message.attr('from');
                 if (message_from === converse.connection.jid) {
                 if (message_from === converse.connection.jid) {
                     // FIXME: Forwarded messages should be sent to specific resources,
                     // FIXME: Forwarded messages should be sent to specific resources,
@@ -2844,8 +2860,7 @@
                     message_from = $message.attr('from');
                     message_from = $message.attr('from');
                 }
                 }
                 var from = Strophe.getBareJidFromJid(message_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) {
                 if (from == converse.bare_jid) {
                     // I am the sender, so this must be a forwarded message...
                     // I am the sender, so this must be a forwarded message...
                     buddy_jid = to;
                     buddy_jid = to;
@@ -2854,15 +2869,15 @@
                     buddy_jid = from;
                     buddy_jid = from;
                     resource = Strophe.getResourceFromJid(message_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) {
                 if (roster_item === undefined) {
                     // The buddy was likely removed
                     // The buddy was likely removed
                     converse.log('Could not get roster item for JID '+buddy_jid, 'error');
                     converse.log('Could not get roster item for JID '+buddy_jid, 'error');
                     return true;
                     return true;
                 }
                 }
 
 
+                chatbox = this.get(buddy_jid);
                 if (!chatbox) {
                 if (!chatbox) {
                     var fullname = roster_item.get('fullname');
                     var fullname = roster_item.get('fullname');
                     fullname = _.isEmpty(fullname)? buddy_jid: fullname;
                     fullname = _.isEmpty(fullname)? buddy_jid: fullname;
@@ -2875,6 +2890,16 @@
                         'url': roster_item.get('url')
                         '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) {
                 if (!this.isOnlyChatStateNotification($message) && from !== converse.bare_jid) {
                     playNotification();
                     playNotification();
                 }
                 }
@@ -3032,10 +3057,14 @@
             },
             },
 
 
             initialize: function () {
             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('change:minimized', this.clearUnreadMessagesCounter, this);
+                this.model.on('showReceivedOTRMessage', this.updateUnreadMessagesCounter, this);
+                this.model.on('showSentOTRMessage', this.updateUnreadMessagesCounter, this);
             },
             },
 
 
             render: function () {
             render: function () {
@@ -3168,7 +3197,7 @@
                 this.set({
                 this.set({
                     'collapsed': this.get('collapsed') || false,
                     'collapsed': this.get('collapsed') || false,
                     'num_minimized': this.get('num_minimized') || 0,
                     '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();
                     this.$flyout.show();
                 }
                 }
                 return this.$el;
                 return this.$el;
-            },
+            }
         });
         });
 
 
         this.RosterContact = Backbone.Model.extend({
         this.RosterContact = Backbone.Model.extend({
@@ -3210,6 +3239,10 @@
                     'status': ''
                     'status': ''
                 }, attributes);
                 }, attributes);
                 this.set(attrs);
                 this.set(attrs);
+            },
+
+            showInRoster: function () {
+                return (!converse.show_only_online_users || this.get('chat_status') === 'online');
             }
             }
         });
         });
 
 
@@ -3224,24 +3257,12 @@
             },
             },
 
 
             initialize: function () {
             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("remove", this.remove, this);
                 this.model.on("destroy", this.remove, this);
                 this.model.on("destroy", this.remove, this);
                 this.model.on("open", this.openChat, 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) {
             openChat: function (ev) {
                 if (ev && ev.preventDefault) { ev.preventDefault(); }
                 if (ev && ev.preventDefault) { ev.preventDefault(); }
                 // XXX: Can this.model.attributes be used here, instead of
                 // XXX: Can this.model.attributes be used here, instead of
@@ -3291,6 +3312,12 @@
             },
             },
 
 
             render: function () {
             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,
                 var item = this.model,
                     ask = item.get('ask'),
                     ask = item.get('ask'),
                     chat_status = item.get('chat_status'),
                     chat_status = item.get('chat_status'),
@@ -3334,7 +3361,7 @@
                     this.$el.html(converse.templates.requesting_contact(
                     this.$el.html(converse.templates.requesting_contact(
                         _.extend(item.toJSON(), {
                         _.extend(item.toJSON(), {
                             'desc_accept': __("Click to accept this contact request"),
                             '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();
                     converse.controlboxtoggle.showControlBox();
@@ -3354,13 +3381,13 @@
 
 
         this.RosterContacts = Backbone.Collection.extend({
         this.RosterContacts = Backbone.Collection.extend({
             model: converse.RosterContact,
             model: converse.RosterContact,
-
             comparator: function (contact1, contact2) {
             comparator: function (contact1, contact2) {
-                var name1 = contact1.get('fullname').toLowerCase();
+                var name1, name2;
                 var status1 = contact1.get('chat_status') || 'offline';
                 var status1 = contact1.get('chat_status') || 'offline';
-                var name2 = contact2.get('fullname').toLowerCase();
                 var status2 = contact2.get('chat_status') || 'offline';
                 var status2 = contact2.get('chat_status') || 'offline';
                 if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) {
                 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);
                     return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
                 } else  {
                 } else  {
                     return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1;
                     return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1;
@@ -3368,15 +3395,13 @@
             },
             },
 
 
             subscribeToSuggestedItems: function (msg) {
             subscribeToSuggestedItems: function (msg) {
-                $(msg).find('item').each(function () {
+                $(msg).find('item').each(function (i, items) {
                     var $this = $(this),
                     var $this = $(this),
                         jid = $this.attr('jid'),
                         jid = $this.attr('jid'),
                         action = $this.attr('action'),
                         action = $this.attr('action'),
                         fullname = $this.attr('name');
                         fullname = $this.attr('name');
                     if (action === 'add') {
                     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;
                 return true;
@@ -3473,17 +3498,19 @@
                     id = this.models[i].get('id');
                     id = this.models[i].get('id');
                     if (_.indexOf(_.pluck(items, 'jid'), id) === -1) {
                     if (_.indexOf(_.pluck(items, 'jid'), id) === -1) {
                         contact = this.get(id);
                         contact = this.get(id);
-                        if (contact) {
+                        if (contact && !contact.get('requesting')) {
                             contact.destroy();
                             contact.destroy();
                         }
                         }
                     }
                     }
                 }
                 }
             },
             },
 
 
-            rosterHandler: function (items) {
+            // TODO: see if we can only use 2nd item par
+            rosterHandler: function (items, item) {
                 converse.emit('roster', items);
                 converse.emit('roster', items);
                 this.clearCache(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; }
                     if (this.isSelf(item.jid)) { return; }
                     var model = this.get(item.jid);
                     var model = this.get(item.jid);
                     if (!model) {
                     if (!model) {
@@ -3500,7 +3527,7 @@
                             groups: item.groups,
                             groups: item.groups,
                             jid: item.jid,
                             jid: item.jid,
                             subscription: item.subscription
                             subscription: item.subscription
-                        });
+                        }, {sort: false});
                     } else {
                     } else {
                         if ((item.subscription === 'none') && (item.ask === null)) {
                         if ((item.subscription === 'none') && (item.ask === null)) {
                             // This user is no longer in our roster
                             // This user is no longer in our roster
@@ -3683,11 +3710,13 @@
                 var view = new converse.RosterContactView({model: contact});
                 var view = new converse.RosterContactView({model: contact});
                 this.add(contact.get('id'), view);
                 this.add(contact.get('id'), view);
                 view = this.positionContact(contact).render();
                 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 () {
             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();
                 this.$el.nextUntil('dt').addBack().show();
             },
             },
 
 
@@ -3726,7 +3758,7 @@
                 if (q.length === 0) {
                 if (q.length === 0) {
                     if (this.model.get('state') === OPENED) {
                     if (this.model.get('state') === OPENED) {
                         this.model.contacts.each($.proxy(function (item) {
                         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.get(item.get('id')).$el.show();
                             }
                             }
                         }, this));
                         }, this));
@@ -3846,16 +3878,20 @@
                 converse.roster.on("remove", this.update, this);
                 converse.roster.on("remove", this.update, this);
                 this.model.on("add", this.onGroupAdd, this);
                 this.model.on("add", this.onGroupAdd, this);
                 this.model.on("reset", this.reset, 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');
                 var $count = $('#online-count');
                 $count.text('('+converse.roster.getNumOnlineContacts()+')');
                 $count.text('('+converse.roster.getNumOnlineContacts()+')');
                 if (!$count.is(':visible')) {
                 if (!$count.is(':visible')) {
                     $count.show();
                     $count.show();
                 }
                 }
+                if (this.$roster.parent().length === 0) {
+                    this.$el.append(this.$roster.show());
+                }
                 return this.showHideFilter();
                 return this.showHideFilter();
-            },
+            }, converse.animate ? 100 : 0),
 
 
             render: function () {
             render: function () {
                 this.$el.html(converse.templates.roster({
                 this.$el.html(converse.templates.roster({
@@ -3868,10 +3904,47 @@
 
 
             fetch: function () {
             fetch: function () {
                 this.model.fetch({
                 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;
                 return this;
             },
             },
 
 
@@ -3939,7 +4012,7 @@
                     // Don't hide if user is currently filtering.
                     // Don't hide if user is currently filtering.
                     return;
                     return;
                 }
                 }
-                if (this.$('.roster-contacts').hasScrollBar()) {
+                if (this.$roster.hasScrollBar()) {
                     if (!visible) {
                     if (!visible) {
                         $filter.show();
                         $filter.show();
                         $type.show();
                         $type.show();
@@ -3954,6 +4027,7 @@
             reset: function () {
             reset: function () {
                 converse.roster.reset();
                 converse.roster.reset();
                 this.removeAll();
                 this.removeAll();
+                this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
                 this.render().update();
                 this.render().update();
                 return this;
                 return this;
             },
             },
@@ -3961,13 +4035,24 @@
             registerRosterHandler: function () {
             registerRosterHandler: function () {
                 // Register handlers that depend on the roster
                 // Register handlers that depend on the roster
                 converse.connection.roster.registerCallback(
                 converse.connection.roster.registerCallback(
-                    $.proxy(converse.roster.rosterHandler, converse.roster),
-                    null, 'presence', null);
+                    $.proxy(converse.roster.rosterHandler, converse.roster)
+                );
             },
             },
 
 
             registerRosterXHandler: function () {
             registerRosterXHandler: function () {
+                var t = 0;
                 converse.connection.addHandler(
                 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);
                     'http://jabber.org/protocol/rosterx', 'message', null);
             },
             },
 
 
@@ -4044,7 +4129,7 @@
                         this.add(group.get('name'), view.render());
                         this.add(group.get('name'), view.render());
                     }
                     }
                     if (idx === 0) {
                     if (idx === 0) {
-                        this.$('.roster-contacts').append(view.$el);
+                        this.$roster.append(view.$el);
                     } else {
                     } else {
                         this.appendGroup(view);
                         this.appendGroup(view);
                     }
                     }
@@ -4055,13 +4140,14 @@
                 /* Place the group's DOM element in the correct alphabetical
                 /* Place the group's DOM element in the correct alphabetical
                  * position amongst the other groups in the roster.
                  * 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) {
                 if (index === 0) {
-                    this.$('.roster-contacts').prepend(view.$el);
+                    this.$roster.prepend(view.$el);
                 } else if (index == (this.model.length-1)) {
                 } else if (index == (this.model.length-1)) {
                     this.appendGroup(view);
                     this.appendGroup(view);
                 } else {
                 } else {
-                    $(this.$('.roster-group').eq(index)).before(view.$el);
+                    $($groups.eq(index)).before(view.$el);
                 }
                 }
                 return this;
                 return this;
             },
             },
@@ -4069,7 +4155,7 @@
             appendGroup: function (view) {
             appendGroup: function (view) {
                 /* Add the group at the bottom of the roster
                 /* 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');
                 var $siblings = $last.siblings('dd');
                 if ($siblings.length > 0) {
                 if ($siblings.length > 0) {
                     $siblings.last().after(view.$el);
                     $siblings.last().after(view.$el);
@@ -4351,7 +4437,7 @@
                  converse.connection.disco.addFeature('http://jabber.org/protocol/rosterx'); // Limited support
                  converse.connection.disco.addFeature('http://jabber.org/protocol/rosterx'); // Limited support
                  converse.connection.disco.addFeature('jabber:x:conference');
                  converse.connection.disco.addFeature('jabber:x:conference');
                  converse.connection.disco.addFeature('urn:xmpp:carbons:2');
                  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.BOSH);
                  converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
                  converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
                  converse.connection.disco.addFeature(Strophe.NS.MUC);
                  converse.connection.disco.addFeature(Strophe.NS.MUC);
@@ -4560,8 +4646,6 @@
                     sid = this.session.get('sid');
                     sid = this.session.get('sid');
                     jid = this.session.get('jid');
                     jid = this.session.get('jid');
                     if (rid && jid && sid) {
                     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.session.save({rid: rid}); // The RID needs to be increased with each request.
                         this.connection.attach(jid, sid, rid, this.onConnect);
                         this.connection.attach(jid, sid, rid, this.onConnect);
                     } else if (this.prebind) {
                     } else if (this.prebind) {
@@ -4701,6 +4785,6 @@
         },
         },
         'registerPlugin': function (name, callback) {
         'registerPlugin': function (name, callback) {
             converse.plugins[name] = callback;
             converse.plugins[name] = callback;
-        },
+        }
     };
     };
 }));
 }));

+ 10 - 1
docs/CHANGES.rst

@@ -1,14 +1,23 @@
 Changelog
 Changelog
 =========
 =========
 
 
+
 0.8.4 (Unreleased)
 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. Error when trying to use prebind and keepalive together. [heban and jcbrand]
 * Bugfix. Cannot read property "top" of undefined. [jcbrand]
 * Bugfix. Cannot read property "top" of undefined. [jcbrand]
 * Add new event, noResumeableSession, for when keepalive=true and there aren't
 * Add new event, noResumeableSession, for when keepalive=true and there aren't
   any prebind session tokens. [jcbrand]
   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)
 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
     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:
 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
     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 type="text/javascript">try { var pageTracker = _gat._getTracker("UA-2128260-8"); pageTracker._trackPageview(); } catch(err) {}</script>
 
 
 <script>
 <script>
-    // Configuration loaded, so safe to make other require calls.
     require(['converse'], function (converse) {
     require(['converse'], function (converse) {
         (function () {
         (function () {
             /* XXX: This function initializes jquery.easing for the https://conversejs.org
             /* XXX: This function initializes jquery.easing for the https://conversejs.org
@@ -255,14 +254,17 @@
         })();
         })();
 
 
         converse.initialize({
         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
             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,
             keepalive: true,
             play_sounds: true,
             play_sounds: true,
-            prebind: false,
+            roster_groups: true,
             show_controlbox_by_default: true,
             show_controlbox_by_default: true,
-            debug: true,
-            roster_groups: true
+            xhr_user_search: false
         });
         });
     });
     });
 </script>
 </script>

+ 183 - 183
locale/converse.pot

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

+ 429 - 264
spec/controlbox.js

@@ -145,7 +145,7 @@
                         subscription: 'both'
                         subscription: 'both'
                     });
                     });
                     converse.rosterview.update(); // XXX: Will normally called as event handler
                     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();
                         expect($filter.is(':visible')).toBeTruthy();
                     } else {
                     } else {
                         expect($filter.is(':visible')).toBeFalsy();
                         expect($filter.is(':visible')).toBeFalsy();
@@ -154,11 +154,16 @@
             }, converse));
             }, converse));
 
 
             it("can be used to filter the contacts shown", function () {
             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 () {
                 runs(function () {
                     expect($roster.find('dd:visible').length).toBe(15);
                     expect($roster.find('dd:visible').length).toBe(15);
                     expect($roster.find('dt:visible').length).toBe(5);
                     expect($roster.find('dt:visible').length).toBe(5);
@@ -199,13 +204,19 @@
             });
             });
 
 
             it("can be used to filter the groups shown", function () {
             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 () {
                 runs(function () {
                     expect($roster.find('dd:visible').length).toBe(15);
                     expect($roster.find('dd:visible').length).toBe(15);
                     expect($roster.find('dt:visible').length).toBe(5);
                     expect($roster.find('dt:visible').length).toBe(5);
@@ -242,7 +253,7 @@
                 _clearContacts();
                 _clearContacts();
                 utils.createGroupedContacts();
                 utils.createGroupedContacts();
                 var $filter = converse.rosterview.$('.roster-filter');
                 var $filter = converse.rosterview.$('.roster-filter');
-                var $roster = converse.rosterview.$('.roster-contacts');
+                var $roster = converse.rosterview.$roster;
                 runs (function () {
                 runs (function () {
                     $filter.val("xxx");
                     $filter.val("xxx");
                     $filter.trigger('keydown');
                     $filter.trigger('keydown');
@@ -267,56 +278,66 @@
             });
             });
 
 
             it("can be used to organize existing contacts", $.proxy(function () {
             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));
             }, converse));
 
 
             it("can share contacts with other roster groups", $.proxy(function () {
             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'];
                 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));
             }, converse));
 
 
             it("remembers whether it is closed or opened", $.proxy(function () {
             it("remembers whether it is closed or opened", $.proxy(function () {
@@ -361,8 +382,13 @@
             }
             }
 
 
             it("can be collapsed under their own header", $.proxy(function () {
             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));
             }, converse));
 
 
             it("can be added to the roster", $.proxy(function () {
             it("can be added to the roster", $.proxy(function () {
@@ -385,40 +411,62 @@
             }, converse));
             }, converse));
 
 
             it("can be removed by the user", $.proxy(function () {
             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));
             }, converse));
 
 
             it("do not have a header if there aren't any", $.proxy(function () {
             it("do not have a header if there aren't any", $.proxy(function () {
                 var name = mock.pend_names[0];
                 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));
             }, converse));
 
 
 
 
@@ -467,8 +515,13 @@
             };
             };
 
 
             it("can be collapsed under their own header", $.proxy(function () {
             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));
             }, converse));
 
 
             it("will be hidden when appearing under a collapsed group", $.proxy(function () {
             it("will be hidden when appearing under a collapsed group", $.proxy(function () {
@@ -488,180 +541,237 @@
             }, converse));
             }, converse));
 
 
             it("can be added to the roster and they will be sorted alphabetically", $.proxy(function () {
             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));
             }, converse));
 
 
             it("can be removed by the user", $.proxy(function () {
             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));
             }, converse));
 
 
 
 
             it("do not have a header if there aren't any", $.proxy(function () {
             it("do not have a header if there aren't any", $.proxy(function () {
                 var name = mock.cur_names[0];
                 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));
             }, converse));
 
 
             it("can change their status to online and be sorted alphabetically", $.proxy(function () {
             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));
             }, converse));
 
 
             it("can change their status to busy and be sorted alphabetically", $.proxy(function () {
             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));
             }, converse));
 
 
             it("can change their status to away and be sorted alphabetically", $.proxy(function () {
             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));
             }, converse));
 
 
             it("can change their status to xa and be sorted alphabetically", $.proxy(function () {
             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));
             }, converse));
 
 
             it("can change their status to unavailable and be sorted alphabetically", $.proxy(function () {
             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));
             }, converse));
 
 
             it("are ordered according to status: online, busy, away, xa, unavailable, offline", $.proxy(function () {
             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));
         }, converse));
         }, converse));
 
 
@@ -713,20 +823,25 @@
             it("do not have a header if there aren't any", $.proxy(function () {
             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
                 converse.rosterview.model.reset(); // We want to manually create users so that we can spy
                 var name = mock.req_names[0];
                 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));
             }, converse));
 
 
             it("can be collapsed under their own header", $.proxy(function () {
             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 () {
             it("can have their requests denied by the user", $.proxy(function () {
                 this.rosterview.model.reset();
                 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));
             }, 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));
         }, converse));
 
 
         describe("All Contacts", $.proxy(function () {
         describe("All Contacts", $.proxy(function () {
@@ -775,7 +940,7 @@
             }, converse));
             }, converse));
 
 
             it("are saved to, and can be retrieved from, browserStorage", $.proxy(function () {
             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;
                 var num_contacts = this.roster.length;
                 new_roster = new this.RosterContacts();
                 new_roster = new this.RosterContacts();
                 // Roster items are yet to be fetched from browserStorage
                 // 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').is(':visible')).toBeTruthy();
                 expect(this.minimized_chats.toggleview.$('.unread-message-count').text()).toBe((i+1).toString());
                 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));
 
 
     }, converse, mock, test_utils));
     }, 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",
     name: "components/almond/almond.js",
     out: "../builds/converse-no-locales-no-otr.min.js",
     out: "../builds/converse-no-locales-no-otr.min.js",
     include: ['main'],
     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: {
     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-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",
     name: "components/almond/almond.js",
     out: "../builds/converse-no-otr.min.js",
     out: "../builds/converse-no-otr.min.js",
     include: ['main'],
     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: {
     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",
     name: "components/almond/almond.js",
     out: "../builds/converse.website-no-otr.min.js",
     out: "../builds/converse.website-no-otr.min.js",
     include: ['main'],
     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: {
     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",
     name: "components/almond/almond.js",
     out: "../builds/converse.website.min.js",
     out: "../builds/converse.website.min.js",
     include: ['main'],
     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: {
     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",
     name: "components/almond/almond.js",
     out: "../builds/converse.min.js",
     out: "../builds/converse.min.js",
     include: ['main'],
     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: {
     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="contacts">{{label_contacts}}</option>
     <option value="groups">{{label_groups}}</option>
     <option value="groups">{{label_groups}}</option>
 </select>
 </select>
-<dl class="roster-contacts"></dl>

+ 2 - 2
src/utils.js

@@ -30,7 +30,7 @@ define(["jquery"], function ($) {
     var utils = {
     var utils = {
         // Translation machinery
         // Translation machinery
         // ---------------------
         // ---------------------
-        __: $.proxy(function (str) {
+        __: function (str) {
             // Translation factory
             // Translation factory
             if (this.i18n === undefined) {
             if (this.i18n === undefined) {
                 this.i18n = locales.en;
                 this.i18n = locales.en;
@@ -41,7 +41,7 @@ define(["jquery"], function ($) {
             } else {
             } else {
                 return t.fetch();
                 return t.fetch();
             }
             }
-        }, this),
+        },
 
 
         ___: function (str) {
         ___: function (str) {
             /* XXX: This is part of a hack to get gettext to scan strings to be
             /* 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
 // Extra test dependencies
 config.paths.mock = "tests/mock";
 config.paths.mock = "tests/mock";
 config.paths.test_utils = "tests/utils";
 config.paths.test_utils = "tests/utils";
@@ -40,8 +189,6 @@ require([
         window.converse_api = converse;
         window.converse_api = converse;
         window.localStorage.clear();
         window.localStorage.clear();
         window.sessionStorage.clear();
         window.sessionStorage.clear();
-        // XXX: call this to initialize Strophe plugins
-        new Strophe.Connection('localhost');
 
 
         converse.initialize({
         converse.initialize({
             prebind: false,
             prebind: false,
@@ -49,7 +196,8 @@ require([
             auto_subscribe: false,
             auto_subscribe: false,
             animate: false,
             animate: false,
             connection: mock.mock_connection,
             connection: mock.mock_connection,
-            no_trimming: true
+            no_trimming: true,
+            debug: true
         }, function (converse) {
         }, function (converse) {
             window.converse = converse;
             window.converse = converse;
             window.crypto = {
             window.crypto = {
@@ -68,7 +216,8 @@ require([
                 "spec/controlbox",
                 "spec/controlbox",
                 "spec/chatbox",
                 "spec/chatbox",
                 "spec/chatroom",
                 "spec/chatroom",
-                "spec/minchats"
+                "spec/minchats",
+                "spec/profiling"
             ], function () {
             ], function () {
                 // Make sure this callback is only called once.
                 // Make sure this callback is only called once.
                 delete converse.callback;
                 delete converse.callback;

+ 32 - 6
tests/mock.js

@@ -36,11 +36,37 @@
         'preventDefault': function () {}
         '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': {
         'muc': {
             'listRooms': function () {},
             'listRooms': function () {},
             'join': function () {},
             'join': function () {},
@@ -49,7 +75,6 @@
             'groupchat': function () {return String((new Date()).getTime()); }
             'groupchat': function () {return String((new Date()).getTime()); }
         },
         },
         'service': 'jasmine tests',
         'service': 'jasmine tests',
-        'jid': 'dummy@localhost',
         'addHandler': function (handler, ns, name, type, id, from, options) {
         'addHandler': function (handler, ns, name, type, id, from, options) {
             return function () {};
             return function () {};
         },
         },
@@ -87,5 +112,6 @@
             'items': function () {}
             'items': function () {}
         }
         }
     };
     };
+    */
     return mock;
     return mock;
 }));
 }));

+ 11 - 0
tests/utils.js

@@ -8,6 +8,17 @@
         });
         });
 }(this, function ($, mock) {
 }(this, function ($, mock) {
     var utils = {};
     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 () {
     utils.closeAllChatBoxes = function () {
         var i, chatbox;
         var i, chatbox;

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott