Ver Fonte

Merge branch 'soft-dependencie'

JC Brand há 9 anos atrás
pai
commit
2c6f6bfef2

+ 2 - 1
converse.js

@@ -56,6 +56,7 @@ require.config({
         "converse-notification":    "src/converse-notification",
         "converse-otr":             "src/converse-otr",
         "converse-ping":            "src/converse-ping",
+        "converse-pluggable":       "src/converse-pluggable",
         "converse-register":        "src/converse-register",
         "converse-rosterview":      "src/converse-rosterview",
         "converse-templates":       "src/converse-templates",
@@ -235,11 +236,11 @@ if (typeof define !== 'undefined') {
                                 // translations that you care about.
 
         "converse-chatview",    // Renders standalone chat boxes for single user chat
+        "converse-controlbox",  // The control box
         "converse-mam",         // XEP-0313 Message Archive Management
         "converse-muc",         // XEP-0045 Multi-user chat
         "converse-vcard",       // XEP-0054 VCard-temp
         "converse-otr",         // Off-the-record encryption for one-on-one messages
-        "converse-controlbox",  // The control box
         "converse-register",    // XEP-0077 In-band registration
         "converse-ping",        // XEP-0199 XMPP Ping
         "converse-notification",// HTML5 Notifications

+ 2 - 2
css/converse.css

@@ -2004,9 +2004,9 @@
         #conversejs .chatroom .box-flyout .chatroom-body .chat-area .chat-content {
           padding: 0 0.5em 0 0.5em; }
         #conversejs .chatroom .box-flyout .chatroom-body .chat-area.full {
-          max-width: 100%; }
+          min-width: 100%; }
           #conversejs .chatroom .box-flyout .chatroom-body .chat-area.full .new-msgs-indicator {
-            max-width: 100%; }
+            min-width: 100%; }
       #conversejs .chatroom .box-flyout .chatroom-body .mentioned {
         font-weight: bold; }
       #conversejs .chatroom .box-flyout .chatroom-body .chat-msg-room {

+ 1 - 0
dev.html

@@ -68,6 +68,7 @@
             play_sounds: true,
             roster_groups: true,
             show_controlbox_by_default: true,
+            strict_plugin_dependencies: false,
             chatstate_notification_blacklist: ['mulles@movim.eu'],
             xhr_user_search: false,
             debug: true

+ 4 - 1
docs/CHANGES.md

@@ -1,6 +1,8 @@
 # Changelog
 
 ## 1.0.3 (Unreleased)
+
+- Update the plugin architecture to allow plugins to have optional dependencies [jcbrand]
 - Bugfix. Login form doesn't render after logging out, when `auto_reconnect = false` [jcbrand]
 - Also indicate new day for the first day's messages. [jcbrand]
 - Chat bot messages don't appear when they have the same ids as their commands. [jcbrand]
@@ -10,9 +12,10 @@
 - Don't use sound and desktop notifications for OTR messages (when setting up the session) [jcbrand]
 - New config option [default_state](https://conversejs.org/docs/html/configuration.html#default_state) [jcbrand]
 - New API method `converse.rooms.close()`
+- New configuration setting [allow_muc_invites](https://conversejs.org/docs/html/configuration.html#allow-muc-invites) [jcbrand]
 - #553 Add processing hints to OTR messages [jcbrand]
 - #650 Don't ignore incoming messages with same JID as current user (might be MAM archived) [jcbrand]
-- #656 online users count in minimized chat window on initialization corrected 
+- #656 online users count in minimized chat window on initialization corrected
 
 
 ## 1.0.2 (2016-05-24)

+ 9 - 0
docs/source/configuration.rst

@@ -141,6 +141,15 @@ allow_muc
 Allow multi-user chat (muc) in chatrooms. Setting this to ``false`` will remove
 the ``Chatrooms`` tab from the control box.
 
+allow_muc_invitations
+---------------------
+
+* Default:  ``true``
+
+Allows users to be invited to join MUC chat rooms. An "Invite" widget will
+appear in the sidebar of the chat room where you can type in the JID of a user
+to invite into the chat room.
+
 allow_otr
 ---------
 

+ 2 - 2
sass/_chatrooms.scss

@@ -59,9 +59,9 @@
                         padding: 0 0.5em 0 0.5em;
                     }
                     &.full {
-                        max-width: 100%;
+                        min-width: 100%;
                         .new-msgs-indicator {
-                            max-width: 100%;
+                            min-width: 100%;
                         }
                     }
                 }

+ 7 - 5
spec/chatbox.js

@@ -406,11 +406,13 @@
             describe("A Chat Message", function () {
 
                 beforeEach(function () {
-                    runs(function () {
-                        test_utils.closeAllChatBoxes();
-                    });
-                    waits(250);
-                    runs(function () {});
+                    test_utils.closeAllChatBoxes();
+                    test_utils.removeControlBox();
+                    converse.roster.browserStorage._clear();
+                    test_utils.initConverse();
+                    test_utils.createContacts('current');
+                    test_utils.openControlBox();
+                    test_utils.openContactsPanel();
                 });
 
                 describe("when received from someone else", function () {

+ 1 - 1
spec/converse.js

@@ -20,7 +20,7 @@
                 var connection = converse.connection;
                 delete converse.bosh_service_url;
                 delete converse.connection;
-                expect(converse.initConnection.bind({})).toThrow(
+                expect(converse.initConnection.bind(converse)).toThrow(
                     new Error("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both."));
                 converse.bosh_service_url = url;
                 converse.connection = connection;

+ 6 - 3
spec/protocol.js

@@ -17,6 +17,12 @@
     // https://xmpp.org/rfcs/rfc3921.html
 
     describe("The Protocol", $.proxy(function (mock, test_utils) {
+        beforeEach(function () {
+            test_utils.removeControlBox();
+            converse.roster.browserStorage._clear();
+            test_utils.initConverse();
+        });
+
         describe("Integration of Roster Items and Presence Subscriptions", $.proxy(function (mock, test_utils) {
             /* Some level of integration between roster items and presence
             * subscriptions is normally expected by an instant messaging user
@@ -48,9 +54,6 @@
             */
             beforeEach(function () {
                 test_utils.closeAllChatBoxes();
-                test_utils.removeControlBox();
-                converse.roster.browserStorage._clear();
-                test_utils.initConverse();
                 test_utils.openControlBox();
                 test_utils.openContactsPanel();
             });

+ 10 - 3
src/converse-api.js

@@ -23,7 +23,7 @@
     var Strophe = strophe.Strophe;
     return {
         'initialize': function (settings, callback) {
-            converse.initialize(settings, callback);
+            return converse.initialize(settings, callback);
         },
         'log': converse.log,
         'connection': {
@@ -133,7 +133,13 @@
                 } else if (typeof jids === "string") {
                     return converse.wrappedChatBox(converse.chatboxes.getChatBox(jids, true));
                 }
-                return _.map(jids, _.partial(_.compose(converse.wrappedChatBox, converse.chatboxes.getChatBox.bind(converse.chatboxes)), _, true));
+                return _.map(jids,
+                    _.partial(
+                        _.compose(
+                            converse.wrappedChatBox.bind(converse), converse.chatboxes.getChatBox.bind(converse.chatboxes)
+                        ), _, true
+                    )
+                );
             }
         },
         'tokens': {
@@ -181,7 +187,8 @@
         },
         'plugins': {
             'add': function (name, plugin) {
-                converse.plugins[name] = plugin;
+                plugin.__name__ = name;
+                converse.pluggable.plugins[name] = plugin;
             },
             'remove': function (name) {
                 delete converse.plugins[name];

+ 1 - 1
src/converse-chatview.js

@@ -24,7 +24,7 @@
     };
 
 
-    converse_api.plugins.add('chatview', {
+    converse_api.plugins.add('converse-chatview', {
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's

+ 2 - 2
src/converse-controlbox.js

@@ -27,7 +27,7 @@
         moment = converse_api.env.moment;
 
 
-    converse_api.plugins.add('controlbox', {
+    converse_api.plugins.add('converse-controlbox', {
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's
@@ -668,7 +668,7 @@
 
                 initialize: function () {
                     this.render();
-					this.updateOnlineCount(); 
+                    this.updateOnlineCount();
                     converse.on('initialized', function () {
                         converse.roster.on("add", this.updateOnlineCount, this);
                         converse.roster.on('change', this.updateOnlineCount, this);

+ 34 - 126
src/converse-core.js

@@ -23,11 +23,12 @@
         "moment_with_locales",
         "strophe",
         "converse-templates",
+        "converse-pluggable",
         "strophe.disco",
         "backbone.browserStorage",
         "backbone.overview",
     ], factory);
-}(this, function ($, _, dummy, utils, moment, Strophe, templates) {
+}(this, function ($, _, dummy, utils, moment, Strophe, templates, pluggable) {
     /*
      * Cannot use this due to Safari bug.
      * See https://github.com/jcbrand/converse.js/issues/196
@@ -58,26 +59,31 @@
     var event_context = {};
 
     var converse = {
-        plugins: {},
-        initialized_plugins: [],
         templates: templates,
+
         emit: function (evt, data) {
             $(event_context).trigger(evt, data);
         },
+
         once: function (evt, handler) {
             $(event_context).one(evt, handler);
         },
+
         on: function (evt, handler) {
             if (_.contains(['ready', 'initialized'], evt)) {
                 converse.log('Warning: The "'+evt+'" event has been deprecated and will be removed, please use "connected".');
             }
             $(event_context).bind(evt, handler);
         },
+
         off: function (evt, handler) {
             $(event_context).unbind(evt, handler);
         }
     };
 
+    // Make converse pluggable
+    pluggable.enable(converse);
+
     // Module-level constants
     converse.STATUS_WEIGHTS = {
         'offline':      6,
@@ -126,6 +132,7 @@
 
     converse.initialize = function (settings, callback) {
         "use strict";
+        var init_deferred = new $.Deferred();
         var converse = this;
         var unloadevent;
         if ('onpagehide' in window) {
@@ -615,7 +622,7 @@
         };
 
 
-        this.onStatusInitialized = function (deferred) {
+        this.onStatusInitialized = function () {
             this.registerIntervalHandler();
             this.roster = new this.RosterContacts();
             this.roster.browserStorage = new Backbone.BrowserStorage[this.storage](
@@ -623,20 +630,14 @@
             this.chatboxes.onConnected();
             this.giveFeedback(__('Contacts'));
             if (typeof this.callback === 'function') {
-                // A callback method may be passed in via the
-                // converse.initialize method.
-                // XXX: Can we use $.Deferred instead of this callback?
-                if (this.connection.service === 'jasmine tests') {
-                    // XXX: Call back with the internal converse object. This
-                    // object should never be exposed to production systems.
-                    // 'jasmine tests' is an invalid http bind service value,
-                    // so we're sure that this is just for tests.
-                    this.callback(this);
-                } else  {
-                    this.callback();
-                }
+                // XXX: Deprecate in favor of init_deferred
+                this.callback();
+            }
+            if (converse.connection.service === 'jasmine tests') {
+                init_deferred.resolve(converse);
+            } else {
+                init_deferred.resolve();
             }
-            deferred.resolve();
             converse.emit('initialized');
         };
 
@@ -644,7 +645,6 @@
             // When reconnecting, there might be some open chat boxes. We don't
             // know whether these boxes are of the same account or not, so we
             // close them now.
-            var deferred = new $.Deferred();
             // XXX: ran into an issue where a returned PubSub BOSH response was
             // not received by the browser. The solution was to flush the
             // connection early on. I don't know what the underlying cause of
@@ -664,13 +664,11 @@
             this.domain = Strophe.getDomainFromJid(this.connection.jid);
             this.features = new this.Features();
             this.enableCarbons();
-            this.initStatus().done(_.bind(this.onStatusInitialized, this, deferred));
+            this.initStatus().done(_.bind(this.onStatusInitialized, this));
             converse.emit('connected');
             converse.emit('ready'); // BBB: Will be removed.
-            return deferred.promise();
         };
 
-
         this.RosterContact = Backbone.Model.extend({
 
             initialize: function (attributes, options) {
@@ -1770,117 +1768,27 @@
             return this;
         };
 
-        this.wrappedOverride = function (key, value, super_method) {
-            // We create a partially applied wrapper function, that
-            // makes sure to set the proper super method when the
-            // overriding method is called. This is done to enable
-            // chaining of plugin methods, all the way up to the
-            // original method.
-            this._super[key] = super_method;
-            return value.apply(this, _.rest(arguments, 3));
-        };
-
-        this._overrideAttribute = function (key, plugin) {
-            // See converse.plugins.override
-            var value = plugin.overrides[key];
-            if (typeof value === "function") {
-                var wrapped_function = _.partial(
-                    converse.wrappedOverride.bind(converse),
-                        key, value, converse[key].bind(converse)
-                );
-                converse[key] = wrapped_function;
-            } else {
-                converse[key] = value;
-            }
-        };
-
-        this._extendObject = function (obj, attributes) {
-            // See converse.plugins.extend
-            if (!obj.prototype._super) {
-                obj.prototype._super = {'converse': converse};
-            }
-            _.each(attributes, function (value, key) {
-                if (key === 'events') {
-                    obj.prototype[key] = _.extend(value, obj.prototype[key]);
-                } else if (typeof value === 'function') {
-                    // We create a partially applied wrapper function, that
-                    // makes sure to set the proper super method when the
-                    // overriding method is called. This is done to enable
-                    // chaining of plugin methods, all the way up to the
-                    // original method.
-                    var wrapped_function = _.partial(
-                        converse.wrappedOverride,
-                            key, value, obj.prototype[key]
-                    );
-                    obj.prototype[key] = wrapped_function;
-                } else {
-                    obj.prototype[key] = value;
-                }
-            });
-        };
-
-        this.initializePlugins = function () {
-            if (typeof converse._super === 'undefined') {
-                converse._super = { 'converse': converse };
-            }
-
-            var updateSettings = function (settings) {
-                /* Helper method which gets put on the plugin and allows it to
-                 * add more user-facing config settings to converse.js.
-                 */
-                _.extend(converse.default_settings, settings);
-                _.extend(converse, settings);
-                _.extend(converse, _.pick(converse.user_settings, Object.keys(settings)));
-            };
-
-            _.each(_.keys(this.plugins), function (name) {
-                var plugin = this.plugins[name];
-                plugin.updateSettings = updateSettings;
-
-                if (_.contains(this.initialized_plugins, name)) {
-                    // Don't initialize plugins twice, otherwise we get
-                    // infinite recursion in overridden methods.
-                    return;
-                }
-                plugin.converse = converse;
-                _.each(Object.keys(plugin.overrides || {}), function (key) {
-                    /* We automatically override all methods and Backbone views and
-                     * models that are in the "overrides" namespace.
-                     */
-                    var msg,
-                        override = plugin.overrides[key];
-                    if (typeof override === "object") {
-                        if (typeof converse[key] === 'undefined') {
-                            msg = "Error: Plugin tried to override "+key+" but it's not found.";
-                            if (converse.strict_plugin_dependencies) {
-                                throw msg;
-                            } else {
-                                converse.log(msg);
-                                return;
-                            }
-                        }
-                        this._extendObject(converse[key], override);
-                    } else {
-                        this._overrideAttribute(key, plugin);
-                    }
-                }.bind(this));
-
-                if (typeof plugin.initialize === "function") {
-                    plugin.initialize.bind(plugin)(this);
-                }
-                this.initialized_plugins.push(name);
-            }.bind(this));
-        };
-
         // Initialization
         // --------------
         // This is the end of the initialize method.
         if (settings.connection) {
             this.connection = settings.connection;
         }
-        this.initializePlugins();
-        this._initialize();
-        this.registerGlobalEventHandlers();
+        var updateSettings = function (settings) {
+            /* Helper method which gets put on the plugin and allows it to
+             * add more user-facing config settings to converse.js.
+             */
+            _.extend(converse.default_settings, settings);
+            _.extend(converse, settings);
+            _.extend(converse, _.pick(converse.user_settings, Object.keys(settings)));
+        };
+        converse.pluggable.initializePlugins({
+            'updateSettings': updateSettings,
+            'converse': converse
+        });
+        converse._initialize();
+        converse.registerGlobalEventHandlers();
+        return init_deferred.promise();
     };
     return converse;
 }));

+ 1 - 1
src/converse-dragresize.js

@@ -19,7 +19,7 @@
     var $ = converse_api.env.jQuery,
         _ = converse_api.env._;
 
-    converse_api.plugins.add('dragresize', {
+    converse_api.plugins.add('converse-dragresize', {
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's

+ 1 - 1
src/converse-headline.js

@@ -36,7 +36,7 @@
         return true;
     };
 
-    converse_api.plugins.add('headline', {
+    converse_api.plugins.add('converse-headline', {
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's

+ 1 - 1
src/converse-mam.js

@@ -32,7 +32,7 @@
     Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
 
 
-    converse_api.plugins.add('mam', {
+    converse_api.plugins.add('converse-mam', {
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's

+ 9 - 2
src/converse-minimize.js

@@ -23,7 +23,7 @@
         utils = converse_api.env.utils,
         __ = utils.__.bind(converse);
 
-    converse_api.plugins.add('minimize', {
+    converse_api.plugins.add('converse-minimize', {
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's
@@ -218,7 +218,14 @@
 
                 getShownChats: function () {
                     return this.filter(function (view) {
-                        return (!view.model.get('minimized') && view.$el.is(':visible'));
+                        // The controlbox can take a while to close,
+                        // so we need to check its state. That's why we checked
+                        // the 'closed' state.
+                        return (
+                            !view.model.get('minimized') &&
+                            !view.model.get('closed') &&
+                            view.$el.is(':visible')
+                        );
                     });
                 },
 

+ 43 - 31
src/converse-muc.js

@@ -14,8 +14,7 @@
             "converse-core",
             "converse-api",
             "typeahead",
-            "converse-chatview",
-            "converse-controlbox"
+            "converse-chatview"
     ], factory);
 }(this, function (converse, converse_api) {
     "use strict";
@@ -43,7 +42,16 @@
     Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
     Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
 
-    converse_api.plugins.add('muc', {
+    converse_api.plugins.add('converse-muc', {
+        /* Optional dependencies are other plugins which might be
+         * overridden or relied upon, if they exist, otherwise they're ignored.
+         *
+         * However, if the setting "strict_plugin_dependencies" is set to true,
+         * an error will be raised if the plugin is not found.
+         *
+         * NB: These plugins need to have already been loaded via require.js.
+         */
+        optional_dependencies: ["converse-controlbox"],
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's
@@ -66,9 +74,11 @@
             Features: {
                 addClientFeatures: function () {
                     this._super.addClientFeatures.apply(this, arguments);
-                    converse.connection.disco.addFeature('jabber:x:conference'); // Invites
-                    if (this.allow_muc) {
-                        this.connection.disco.addFeature(Strophe.NS.MUC);
+                    if (converse.allow_muc_invitations) {
+                        converse.connection.disco.addFeature('jabber:x:conference'); // Invites
+                    }
+                    if (converse.allow_muc) {
+                        converse.connection.disco.addFeature(Strophe.NS.MUC);
                     }
                 }
             },
@@ -147,6 +157,7 @@
             var converse = this.converse;
             // Configuration values for this plugin
             this.updateSettings({
+                allow_muc_invitations: true,
                 allow_muc: true,
                 auto_join_on_invite: false,  // Auto-join chatroom on invite
                 auto_join_rooms: [], // List of maps {'jid': 'room@example.org', 'nick': 'WizardKing69' },
@@ -252,16 +263,15 @@
                         // Bit of a hack, to make sure that the sidebar's state doesn't change
                         this.model.set({hidden_occupants: !this.model.get('hidden_occupants')});
                     }
-                    var $el = this.$('.icon-hide-users');
                     if (!this.model.get('hidden_occupants')) {
                         this.model.save({hidden_occupants: true});
-                        $el.removeClass('icon-hide-users').addClass('icon-show-users');
+                        this.$('.icon-hide-users').removeClass('icon-hide-users').addClass('icon-show-users');
                         this.$('.occupants').addClass('hidden');
                         this.$('.chat-area').addClass('full');
                         this.scrollDown();
                     } else {
                         this.model.save({hidden_occupants: false});
-                        $el.removeClass('icon-show-users').addClass('icon-hide-users');
+                        this.$('.icon-show-users').removeClass('icon-show-users').addClass('icon-hide-users');
                         this.$('.chat-area').removeClass('full');
                         this.$('div.occupants').removeClass('hidden');
                         this.scrollDown();
@@ -958,11 +968,15 @@
                 render: function () {
                     this.$el.html(
                         converse.templates.chatroom_sidebar({
+                            'allow_muc_invitations': converse.allow_muc_invitations,
                             'label_invitation': __('Invite'),
                             'label_occupants': __('Occupants')
                         })
                     );
-                    return this.initInviteWidget();
+                    if (converse.allow_muc_invitations) {
+                        return this.initInviteWidget();
+                    }
+                    return this;
                 },
 
                 onOccupantAdded: function (item) {
@@ -1354,15 +1368,17 @@
             };
             converse.on('chatBoxesFetched', autoJoinRooms);
 
-            var onConnected = function () {
-                converse.connection.addHandler(
-                    function (message) {
-                        converse.onDirectMUCInvitation(message);
-                        return true;
-                    }, 'jabber:x:conference', 'message');
-            };
-            converse.on('connected', onConnected);
-            converse.on('reconnected', onConnected);
+            if (converse.allow_muc_invitations) {
+                var onConnected = function () {
+                    converse.connection.addHandler(
+                        function (message) {
+                            converse.onDirectMUCInvitation(message);
+                            return true;
+                        }, 'jabber:x:conference', 'message');
+                };
+                converse.on('connected', onConnected);
+                converse.on('reconnected', onConnected);
+            }
             /* ------------------------------------------------------------ */
 
 
@@ -1397,18 +1413,14 @@
                         }
                         var _transform = function (jid) {
                             jid = jid.toLowerCase();
-                            var chatroom = converse.chatboxes.get(jid);
-                            if (!chatroom) {
-                                chatroom = converse.chatboxviews.showChat({
-                                    'id': jid,
-                                    'jid': jid,
-                                    'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
-                                    'nick': nick,
-                                    'type': 'chatroom',
-                                    'box_id': b64_sha1(jid)
-                                });
-                            }
-                            return converse.wrappedChatBox(converse.chatboxes.getChatBox(jid, true));
+                            return converse.wrappedChatBox(converse.chatboxviews.showChat({
+                                'id': jid,
+                                'jid': jid,
+                                'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
+                                'nick': nick,
+                                'type': 'chatroom',
+                                'box_id': b64_sha1(jid)
+                            }));
                         };
                         if (typeof jids === "undefined") {
                             throw new TypeError('rooms.open: You need to provide at least one JID');

+ 1 - 1
src/converse-notification.js

@@ -21,7 +21,7 @@
     var supports_html5_notification = "Notification" in window;
 
 
-    converse_api.plugins.add('notification', {
+    converse_api.plugins.add('converse-notification', {
 
         initialize: function () {
             /* The initialize function gets called as soon as the plugin is

+ 1 - 1
src/converse-otr.js

@@ -50,7 +50,7 @@
     OTR_CLASS_MAPPING[VERIFIED] = 'verified';
     OTR_CLASS_MAPPING[FINISHED] = 'finished';
 
-    converse_api.plugins.add('otr', {
+    converse_api.plugins.add('converse-otr', {
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's

+ 1 - 1
src/converse-ping.js

@@ -22,7 +22,7 @@
     // Other necessary globals
     var _ = converse_api.env._;
     
-    converse_api.plugins.add('ping', {
+    converse_api.plugins.add('converse-ping', {
 
         initialize: function () {
             /* The initialize function gets called as soon as the plugin is

+ 167 - 0
src/converse-pluggable.js

@@ -0,0 +1,167 @@
+/*
+ *     ____  __                        __    __         _
+ *    / __ \/ /_  __ ___   ___  ____ _/ /_  / /__      (_)____
+ *   / /_/ / / / / / __ \/ __ \/ __/ / __ \/ / _ \    / / ___/
+ *  / ____/ / /_/ / /_/ / /_/ / /_/ / /_/ / /  __/   / (__  )
+ * /_/   /_/\__,_/\__, /\__, /\__/_/_.___/_/\___(_)_/ /____/
+ *               /____//____/                    /___/
+ *
+ */
+(function (root, factory) {
+    define("converse-pluggable", ["jquery", "underscore"], factory);
+}(this, function ($, _) {
+    "use strict";
+
+    function Pluggable (plugged) {
+        this.plugged = plugged;
+        this.plugged._super = {};
+        this.plugins = {};
+        this.initialized_plugins = [];
+    }
+    _.extend(Pluggable.prototype, {
+        wrappedOverride: function (key, value, super_method) {
+            /* We create a partially applied wrapper function, that
+             * makes sure to set the proper super method when the
+             * overriding method is called. This is done to enable
+             * chaining of plugin methods, all the way up to the
+             * original method.
+             */
+            if (typeof super_method === "function") {
+                this._super[key] = super_method.bind(this);
+            }
+            return value.apply(this, _.rest(arguments, 3));
+        },
+
+        _overrideAttribute: function (key, plugin) {
+            /* Overrides an attribute on the original object (the thing being
+             * plugged into).
+             *
+             * If the attribute being overridden is a function, then the original
+             * function will still be available via the _super attribute.
+             *
+             * If the same function is being overridden multiple times, then
+             * the original function will be available at the end of a chain of
+             * functions, starting from the most recent override, all the way
+             * back to the original function, each being referenced by the
+             * previous' _super attribute.
+             *
+             * For example:
+             *
+             * plugin2.MyFunc._super.myFunc => * plugin1.MyFunc._super.myFunc => original.myFunc
+             */
+            var value = plugin.overrides[key];
+            if (typeof value === "function") {
+                var wrapped_function = _.partial(
+                    this.wrappedOverride, key, value, this.plugged[key]
+                );
+                this.plugged[key] = wrapped_function;
+            } else {
+                this.plugged[key] = value;
+            }
+        },
+
+        _extendObject: function (obj, attributes) {
+            if (!obj.prototype._super) {
+                // FIXME: make generic
+                obj.prototype._super = {'converse': this.plugged };
+            }
+            _.each(attributes, function (value, key) {
+                if (key === 'events') {
+                    obj.prototype[key] = _.extend(value, obj.prototype[key]);
+                } else if (typeof value === 'function') {
+                    // We create a partially applied wrapper function, that
+                    // makes sure to set the proper super method when the
+                    // overriding method is called. This is done to enable
+                    // chaining of plugin methods, all the way up to the
+                    // original method.
+                    var wrapped_function = _.partial(
+                        this.wrappedOverride, key, value, obj.prototype[key]
+                    );
+                    obj.prototype[key] = wrapped_function;
+                } else {
+                    obj.prototype[key] = value;
+                }
+            }.bind(this));
+        },
+
+        loadOptionalDependencies: function (plugin) {
+            _.each(plugin.optional_dependencies, function (name) {
+                var dep = this.plugins[name];
+                if (dep) {
+                    if (_.contains(dep.optional_dependencies, plugin.__name__)) {
+                        // FIXME: circular dependency checking is only one level deep.
+                        throw "Found a circular dependency between the plugins \""+
+                              plugin.__name__+"\" and \""+name+"\"";
+                    }
+                    this.initializePlugin(dep);
+                } else {
+                    this.throwUndefinedDependencyError(
+                        "Could not find optional dependency \""+name+"\" "+
+                        "for the plugin \""+plugin.__name__+"\". "+
+                        "If it's needed, make sure it's loaded by require.js");
+                }
+            }.bind(this));
+        },
+
+        throwUndefinedDependencyError: function (msg) {
+            if (this.plugged.strict_plugin_dependencies) {
+                throw msg;
+            } else {
+                console.log(msg);
+                return;
+            }
+        },
+
+        applyOverrides: function (plugin) {
+            _.each(Object.keys(plugin.overrides || {}), function (key) {
+                /* We automatically override all methods and Backbone views and
+                 * models that are in the "overrides" namespace.
+                 */
+                var override = plugin.overrides[key];
+                if (typeof override === "object") {
+                    if (typeof this.plugged[key] === 'undefined') {
+                        this.throwUndefinedDependencyError("Error: Plugin \""+plugin.__name__+"\" tried to override "+key+" but it's not found.");
+                    } else {
+                        this._extendObject(this.plugged[key], override);
+                    }
+                } else {
+                    this._overrideAttribute(key, plugin);
+                }
+            }.bind(this));
+        },
+
+        initializePlugin: function (plugin) {
+            if (_.contains(this.initialized_plugins, plugin.__name__)) {
+                // Don't initialize plugins twice, otherwise we get
+                // infinite recursion in overridden methods.
+                return;
+            }
+            _.extend(plugin, this.properties);
+            if (plugin.optional_dependencies) {
+                this.loadOptionalDependencies(plugin);
+            }
+            this.applyOverrides(plugin);
+            if (typeof plugin.initialize === "function") {
+                plugin.initialize.bind(plugin)(this);
+            }
+            this.initialized_plugins.push(plugin.__name__);
+        },
+
+        initializePlugins: function (properties) {
+            /* The properties variable is an object of attributes and methods
+             * which will be attached to the plugins.
+             */
+            if (!_.size(this.plugins)) {
+                return;
+            }
+            this.properties = properties;
+            _.each(_.values(this.plugins), this.initializePlugin.bind(this));
+        }
+    });
+    return {
+        'enable': function (object) {
+            /* Call this method to make an object pluggable */
+            return _.extend(object, {'pluggable': new Pluggable(object)});
+        }
+    };
+}));

+ 1 - 1
src/converse-register.js

@@ -40,7 +40,7 @@
     Strophe.Status.CONFLICT        = i + 3;
     Strophe.Status.NOTACCEPTABLE   = i + 5;
 
-    converse_api.plugins.add('register', {
+    converse_api.plugins.add('converse-register', {
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's

+ 1 - 1
src/converse-vcard.js

@@ -20,7 +20,7 @@
         moment = converse_api.env.moment;
 
 
-    converse_api.plugins.add('vcard', {
+    converse_api.plugins.add('converse-vcard', {
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's

+ 2 - 0
src/templates/chatroom_sidebar.html

@@ -1,7 +1,9 @@
 <!-- <div class="occupants"> -->
+{[ if (allow_muc_invitations) { ]}
 <form class="pure-form room-invite">
     <input class="invited-contact" placeholder="{{label_invitation}}" type="text"/>
 </form>
+{[ } ]}
 <p class="occupants-heading">{{label_occupants}}:</p>
 <ul class="occupant-list"></ul>
 <!-- </div> -->

+ 0 - 1
src/utils.js

@@ -149,7 +149,6 @@
             return str;
         },
 
-
         isOTRMessage: function (message) {
             var $body = $(message).children('body'),
                 text = ($body.length > 0 ? $body.text() : undefined);

+ 1 - 3
tests/main.js

@@ -56,7 +56,7 @@ require([
             jid: 'dummy@localhost',
             password: 'secret',
             debug: true 
-        }, function (converse) {
+        }).then(function (converse) {
             window.converse = converse;
             window.crypto = {
                 getRandomValues: function (buf) {
@@ -86,8 +86,6 @@ require([
                 "spec/register",
                 "spec/xmppstatus",
             ], function () {
-                // Make sure this callback is only called once.
-                delete converse.callback;
                 // Stub the trimChat method. It causes havoc when running with
                 // phantomJS.
                 converse.ChatBoxViews.prototype.trimChat = function () {};