浏览代码

Use composition instead of overrides

JC Brand 6 年之前
父节点
当前提交
1ef29bee4e

+ 135 - 147
docs/source/plugin_development.rst

@@ -189,14 +189,13 @@ The code for it would look something like this:
 
     // Commonly used utilities and variables can be found under the "env"
     // namespace of the "converse" global.
-    var Strophe = converse.env.Strophe,
-        $iq = converse.env.$iq,
-        $msg = converse.env.$msg,
-        $pres = converse.env.$pres,
-        $build = converse.env.$build,
-        b64_sha1 = converse.env.b64_sha1,
-        _ = converse.env._,
-        dayjs = converse.env.dayjs;
+    const Strophe = converse.env.Strophe,
+          $iq = converse.env.$iq,
+          $msg = converse.env.$msg,
+          $pres = converse.env.$pres,
+          $build = converse.env.$build,
+          _ = converse.env._,
+          dayjs = converse.env.dayjs;
 
 These dependencies are closured so that they don't pollute the global
 namespace, that's why you need to access them in such a way inside the module.
@@ -300,7 +299,7 @@ In this case, you should first listen for the ``connection`` event, and then do
 
     converse.plugins.add('myplugin', {
         initialize: function () {
-            var _converse = this._converse;
+            const _converse = this._converse;
 
             _converse.api.listen.on('connected', function () {
                 _converse.api.archive.query({'with': 'admin2@localhost'});
@@ -363,152 +362,141 @@ generated by `generator-conversejs <https://github.com/jcbrand/generator-convers
 
 .. code-block:: javascript
 
-    (function (root, factory) {
-        if (typeof define === 'function' && define.amd) {
-            // AMD. Register as a module called "myplugin"
-            define(["converse"], factory);
-        } else {
-            // Browser globals. If you're not using a module loader such as require.js,
-            // then this line below executes. Make sure that your plugin's <script> tag
-            // appears after the one from converse.js.
-            factory(converse);
-        }
-    }(this, function (converse) {
+    import converse from "@converse/headless/converse-core";
+
+    // Commonly used utilities and variables can be found under the "env"
+    // namespace of the "converse" global.
+    const Strophe = converse.env.Strophe,
+          $iq = converse.env.$iq,
+          $msg = converse.env.$msg,
+          $pres = converse.env.$pres,
+          $build = converse.env.$build,
+          _ = converse.env._,
+          dayjs = converse.env.dayjs;
+
+    // The following line registers your plugin.
+    converse.plugins.add("myplugin", {
+
+        /* Dependencies are other plugins which might be
+         * overridden or relied upon, and therefore need to be loaded before
+         * this plugin. They are "optional" because they might not be
+         * available, in which case any overrides applicable to them will be
+         * ignored.
+         *
+         * NB: These plugins need to have already been imported or loaded,
+         * either in your plugin or somewhere else.
+         *
+         * It's possible to make these dependencies "non-optional".
+         * If the setting "strict_plugin_dependencies" is set to true,
+         * an error will be raised if the plugin is not found.
+         */
+        'dependencies': [],
+
+        /* Converse.js's plugin mechanism will call the initialize
+         * method on any plugin (if it exists) as soon as the plugin has
+         * been loaded.
+         */
+        'initialize': function () {
+            /* Inside this method, you have access to the private
+             * `_converse` object.
+             */
+            const _converse = this._converse;
+            _converse.log("The \"myplugin\" plugin is being initialized");
+
+            /* From the `_converse` object you can get any configuration
+             * options that the user might have passed in via
+             * `converse.initialize`.
+             *
+             * You can also specify new configuration settings for this
+             * plugin, or override the default values of existing
+             * configuration settings. This is done like so:
+            */
+            _converse.api.settings.update({
+                'initialize_message': 'Initializing myplugin!'
+            });
+
+            /* The user can then pass in values for the configuration
+             * settings when `converse.initialize` gets called.
+             * For example:
+             *
+             *      converse.initialize({
+             *           "initialize_message": "My plugin has been initialized"
+             *      });
+             */
+            alert(this._converse.initialize_message);
 
-        // Commonly used utilities and variables can be found under the "env"
-        // namespace of the "converse" global.
-        var Strophe = converse.env.Strophe,
-            $iq = converse.env.$iq,
-            $msg = converse.env.$msg,
-            $pres = converse.env.$pres,
-            $build = converse.env.$build,
-            b64_sha1 = converse.env.b64_sha1,
-            _ = converse.env._,
-            dayjs = converse.env.dayjs;
-
-        // The following line registers your plugin.
-        converse.plugins.add("myplugin", {
-
-            /* Dependencies are other plugins which might be
-             * overridden or relied upon, and therefore need to be loaded before
-             * this plugin. They are "optional" because they might not be
-             * available, in which case any overrides applicable to them will be
-             * ignored.
+            /* Besides `_converse.api.settings.update`, there is also a
+             * `_converse.api.promises.add` method, which allows you to
+             * add new promises that your plugin is obligated to fulfill.
+             *
+             * This method takes a string or a list of strings which
+             * represent the promise names:
+             *
+             *      _converse.api.promises.add('myPromise');
+             *
+             * Your plugin should then, when appropriate, resolve the
+             * promise by calling `_converse.api.emit`, which will also
+             * emit an event with the same name as the promise.
+             * For example:
              *
-             * NB: These plugins need to have already been loaded via require.js.
+             *      _converse.api.trigger('operationCompleted');
              *
-             * It's possible to make these dependencies "non-optional".
-             * If the setting "strict_plugin_dependencies" is set to true,
-             * an error will be raised if the plugin is not found.
+             * Other plugins can then either listen for the event
+             * `operationCompleted` like so:
+             *
+             *      _converse.api.listen.on('operationCompleted', function { ... });
+             *
+             * or they can wait for the promise to be fulfilled like so:
+             *
+             *      _converse.api.waitUntil('operationCompleted', function { ... });
              */
-            'dependencies': [],
+        },
 
-            /* Converse.js's plugin mechanism will call the initialize
-             * method on any plugin (if it exists) as soon as the plugin has
-             * been loaded.
+        /* If you want to override some function or a Backbone model or
+         * view defined elsewhere in converse.js, then you do that under
+         * the "overrides" namespace.
+         */
+        'overrides': {
+            /* For example, the private *_converse* object has a
+             * method "onConnected". You can override that method as follows:
              */
-            'initialize': function () {
-                /* Inside this method, you have access to the private
-                 * `_converse` object.
-                 */
-                var _converse = this._converse;
-                _converse.log("The \"myplugin\" plugin is being initialized");
-
-                /* From the `_converse` object you can get any configuration
-                 * options that the user might have passed in via
-                 * `converse.initialize`.
-                 *
-                 * You can also specify new configuration settings for this
-                 * plugin, or override the default values of existing
-                 * configuration settings. This is done like so:
-                */
-                _converse.api.settings.update({
-                    'initialize_message': 'Initializing myplugin!'
-                });
-
-                /* The user can then pass in values for the configuration
-                 * settings when `converse.initialize` gets called.
-                 * For example:
-                 *
-                 *      converse.initialize({
-                 *           "initialize_message": "My plugin has been initialized"
-                 *      });
-                 */
-                alert(this._converse.initialize_message);
-
-                /* Besides `_converse.api.settings.update`, there is also a
-                 * `_converse.api.promises.add` method, which allows you to
-                 * add new promises that your plugin is obligated to fulfill.
-                 *
-                 * This method takes a string or a list of strings which
-                 * represent the promise names:
-                 *
-                 *      _converse.api.promises.add('myPromise');
-                 *
-                 * Your plugin should then, when appropriate, resolve the
-                 * promise by calling `_converse.api.emit`, which will also
-                 * emit an event with the same name as the promise.
-                 * For example:
-                 *
-                 *      _converse.api.trigger('operationCompleted');
-                 *
-                 * Other plugins can then either listen for the event
-                 * `operationCompleted` like so:
-                 *
-                 *      _converse.api.listen.on('operationCompleted', function { ... });
-                 *
-                 * or they can wait for the promise to be fulfilled like so:
-                 *
-                 *      _converse.api.waitUntil('operationCompleted', function { ... });
-                 */
+            'onConnected': function () {
+                // Overrides the onConnected method in converse.js
+
+                // Top-level functions in "overrides" are bound to the
+                // inner "_converse" object.
+                const _converse = this;
+
+                // Your custom code can come here ...
+
+                // You can access the original function being overridden
+                // via the __super__ attribute.
+                // Make sure to pass on the arguments supplied to this
+                // function and also to apply the proper "this" object.
+                _converse.__super__.onConnected.apply(this, arguments);
+
+                // Your custom code can come here ...
             },
 
-            /* If you want to override some function or a Backbone model or
-             * view defined elsewhere in converse.js, then you do that under
-             * the "overrides" namespace.
+            /* Override converse.js's XMPPStatus Backbone model so that we can override the
+             * function that sends out the presence stanza.
              */
-            'overrides': {
-                /* For example, the private *_converse* object has a
-                 * method "onConnected". You can override that method as follows:
-                 */
-                'onConnected': function () {
-                    // Overrides the onConnected method in converse.js
-
-                    // Top-level functions in "overrides" are bound to the
-                    // inner "_converse" object.
-                    var _converse = this;
-
-                    // Your custom code can come here ...
-
-                    // You can access the original function being overridden
-                    // via the __super__ attribute.
-                    // Make sure to pass on the arguments supplied to this
-                    // function and also to apply the proper "this" object.
-                    _converse.__super__.onConnected.apply(this, arguments);
-
-                    // Your custom code can come here ...
-                },
-
-                /* Override converse.js's XMPPStatus Backbone model so that we can override the
-                 * function that sends out the presence stanza.
-                 */
-                'XMPPStatus': {
-                    'sendPresence': function (type, status_message, jid) {
-                        // The "_converse" object is available via the __super__
-                        // attribute.
-                        var _converse = this.__super__._converse;
-
-                        // Custom code can come here ...
-
-                        // You can call the original overridden method, by
-                        // accessing it via the __super__ attribute.
-                        // When calling it, you need to apply the proper
-                        // context as reference by the "this" variable.
-                        this.__super__.sendPresence.apply(this, arguments);
-
-                        // Custom code can come here ...
-                    }
+            'XMPPStatus': {
+                'sendPresence': function (type, status_message, jid) {
+                    // The "_converse" object is available via the __super__
+                    // attribute.
+                    const _converse = this.__super__._converse;
+
+                    // Custom code can come here ...
+
+                    // You can call the original overridden method, by
+                    // accessing it via the __super__ attribute.
+                    // When calling it, you need to apply the proper
+                    // context as reference by the "this" variable.
+                    this.__super__.sendPresence.apply(this, arguments);
+
+                    // Custom code can come here ...
                 }
             }
-        });
-    }));
+        }
+    });

+ 62 - 66
src/converse-bookmark-views.js

@@ -42,20 +42,67 @@ converse.plugins.add('converse-bookmark-views', {
         // Overrides mentioned here will be picked up by converse.js's
         // plugin architecture they will replace existing methods on the
         // relevant objects or classes.
-        //
-        // New functions which don't exist yet can also be added.
-
         ChatRoomView: {
             events: {
                 'click .toggle-bookmark': 'toggleBookmark'
             },
+            async renderHeading () {
+                this.__super__.renderHeading.apply(this, arguments);
+                const { _converse } = this.__super__;
+                if (_converse.allow_bookmarks) {
+                    const supported = await _converse.checkBookmarksSupport();
+                    if (supported) {
+                        this.renderBookmarkToggle();
+                    }
+                }
+            }
+        }
+    },
 
-            initialize () {
-                this.__super__.initialize.apply(this, arguments);
-                this.model.on('change:bookmarked', this.onBookmarked, this);
-                this.setBookmarkState();
+    initialize () {
+        /* The initialize function gets called as soon as the plugin is
+         * loaded by converse.js's plugin machinery.
+         */
+        const { _converse } = this,
+              { __ } = _converse;
+
+        // Configuration values for this plugin
+        // ====================================
+        // Refer to docs/source/configuration.rst for explanations of these
+        // configuration settings.
+        _converse.api.settings.update({
+            hide_open_bookmarks: true,
+            muc_respect_autojoin: true
+        });
+
+
+        Object.assign(_converse, {
+
+            removeBookmarkViaEvent (ev) {
+                /* Remove a bookmark as determined by the passed in
+                 * event.
+                 */
+                ev.preventDefault();
+                const name = ev.target.getAttribute('data-bookmark-name');
+                const jid = ev.target.getAttribute('data-room-jid');
+                if (confirm(__("Are you sure you want to remove the bookmark \"%1$s\"?", name))) {
+                    _.invokeMap(_converse.bookmarks.where({'jid': jid}), Backbone.Model.prototype.destroy);
+                }
             },
 
+            addBookmarkViaEvent (ev) {
+                /* Add a bookmark as determined by the passed in
+                 * event.
+                 */
+                ev.preventDefault();
+                const jid = ev.target.getAttribute('data-room-jid');
+                const chatroom = _converse.api.rooms.open(jid, {'bring_to_foreground': true});
+                _converse.chatboxviews.get(jid).renderBookmarkForm();
+            },
+        });
+
+        const bookmarkableChatRoomView = {
+
             renderBookmarkToggle () {
                 if (this.el.querySelector('.chat-head .toggle-bookmark')) {
                     return;
@@ -80,21 +127,7 @@ converse.plugins.add('converse-bookmark-views', {
                 }
             },
 
-            async renderHeading () {
-                this.__super__.renderHeading.apply(this, arguments);
-                const { _converse } = this.__super__;
-                if (_converse.allow_bookmarks) {
-                    const supported = await _converse.checkBookmarksSupport();
-                    if (supported) {
-                        this.renderBookmarkToggle();
-                    }
-                }
-            },
-
             onBookmarked () {
-                const { _converse } = this.__super__,
-                      { __ } = _converse;
-
                 const icon = this.el.querySelector('.toggle-bookmark');
                 if (_.isNull(icon)) {
                     return;
@@ -111,7 +144,6 @@ converse.plugins.add('converse-bookmark-views', {
             setBookmarkState () {
                 /* Set whether the groupchat is bookmarked or not.
                  */
-                const { _converse } = this.__super__;
                 if (!_.isUndefined(_converse.bookmarks)) {
                     const models = _converse.bookmarks.where({'jid': this.model.get('jid')});
                     if (!models.length) {
@@ -125,7 +157,6 @@ converse.plugins.add('converse-bookmark-views', {
             renderBookmarkForm () {
                 this.hideChatRoomContents();
                 if (!this.bookmark_form) {
-                    const { _converse } = this.__super__;
                     this.bookmark_form = new _converse.MUCBookmarkForm({
                         'model': this.model,
                         'chatroomview': this
@@ -141,7 +172,6 @@ converse.plugins.add('converse-bookmark-views', {
                     ev.preventDefault();
                     ev.stopPropagation();
                 }
-                const { _converse } = this.__super__;
                 const models = _converse.bookmarks.where({'jid': this.model.get('jid')});
                 if (!models.length) {
                     this.renderBookmarkForm();
@@ -151,48 +181,7 @@ converse.plugins.add('converse-bookmark-views', {
                 }
             }
         }
-    },
-
-    initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by converse.js's plugin machinery.
-         */
-        const { _converse } = this,
-              { __ } = _converse;
-
-        // Configuration values for this plugin
-        // ====================================
-        // Refer to docs/source/configuration.rst for explanations of these
-        // configuration settings.
-        _converse.api.settings.update({
-            hide_open_bookmarks: true,
-            muc_respect_autojoin: true
-        });
-
-
-        Object.assign(_converse, {
-            removeBookmarkViaEvent (ev) {
-                /* Remove a bookmark as determined by the passed in
-                 * event.
-                 */
-                ev.preventDefault();
-                const name = ev.target.getAttribute('data-bookmark-name');
-                const jid = ev.target.getAttribute('data-room-jid');
-                if (confirm(__("Are you sure you want to remove the bookmark \"%1$s\"?", name))) {
-                    _.invokeMap(_converse.bookmarks.where({'jid': jid}), Backbone.Model.prototype.destroy);
-                }
-            },
-
-            addBookmarkViaEvent (ev) {
-                /* Add a bookmark as determined by the passed in
-                 * event.
-                 */
-                ev.preventDefault();
-                const jid = ev.target.getAttribute('data-room-jid');
-                const chatroom = _converse.api.rooms.open(jid, {'bring_to_foreground': true});
-                _converse.chatboxviews.get(jid).renderBookmarkForm();
-            },
-        });
+        Object.assign(_converse.ChatRoomView.prototype, bookmarkableChatRoomView);
 
 
         _converse.MUCBookmarkForm = Backbone.VDOMView.extend({
@@ -368,6 +357,7 @@ converse.plugins.add('converse-bookmark-views', {
             }
         });
 
+        /************************ BEGIN Event Handlers ************************/
         const initBookmarkViews = async function () {
             await _converse.api.waitUntil('roomsPanelRendered');
             _converse.bookmarksview = new _converse.BookmarksView({'model': _converse.bookmarks});
@@ -381,5 +371,11 @@ converse.plugins.add('converse-bookmark-views', {
         }
 
         _converse.api.listen.on('bookmarksInitialized', initBookmarkViews);
+
+        _converse.api.listen.on('chatRoomOpened', view => {
+            view.model.on('change:bookmarked', view.onBookmarked, view);
+            view.setBookmarkState();
+        });
+        /************************ END Event Handlers ************************/
     }
 });

+ 2 - 18
src/converse-controlbox.js

@@ -105,23 +105,6 @@ converse.plugins.add('converse-controlbox', {
                     view.close();
                 });
                 return this;
-            },
-
-            getChatBoxWidth (view) {
-                const { _converse } = this.__super__;
-                const controlbox = this.get('controlbox');
-                if (view.model.get('id') === 'controlbox') {
-                    /* We return the width of the controlbox or its toggle,
-                     * depending on which is visible.
-                     */
-                    if (!controlbox || !u.isVisible(controlbox.el)) {
-                        return u.getOuterWidth(_converse.controlboxtoggle.el, true);
-                    } else {
-                        return u.getOuterWidth(controlbox.el, true);
-                    }
-                } else {
-                    return this.__super__.getChatBoxWidth.apply(this, arguments);
-                }
             }
         },
 
@@ -232,11 +215,12 @@ converse.plugins.add('converse-controlbox', {
                  * Triggered when the _converse.ControlBoxView has been initialized and therefore
                  * exists. The controlbox contains the login and register forms when the user is
                  * logged out and a list of the user's contacts and group chats when logged in.
-                 * @event _converse#chatBoxInitialized
+                 * @event _converse#controlboxInitialized
                  * @type { _converse.ControlBoxView }
                  * @example _converse.api.listen.on('controlboxInitialized', view => { ... });
                  */
                 _converse.api.trigger('controlboxInitialized', this);
+                _converse.api.trigger('chatBoxInitialized', this);
             },
 
             render () {

+ 140 - 158
src/converse-dragresize.js

@@ -45,42 +45,6 @@ converse.plugins.add('converse-dragresize', {
         // Overrides mentioned here will be picked up by converse.js's
         // plugin architecture they will replace existing methods on the
         // relevant objects or classes.
-        //
-        // New functions which don't exist yet can also be added.
-
-        registerGlobalEventHandlers () {
-            const that = this;
-
-            document.addEventListener('mousemove', function (ev) {
-                if (!that.resizing || !that.allow_dragresize) { return true; }
-                ev.preventDefault();
-                that.resizing.chatbox.resizeChatBox(ev);
-            });
-
-            document.addEventListener('mouseup', function (ev) {
-                if (!that.resizing || !that.allow_dragresize) { return true; }
-                ev.preventDefault();
-                const height = that.applyDragResistance(
-                        that.resizing.chatbox.height,
-                        that.resizing.chatbox.model.get('default_height')
-                );
-                const width = that.applyDragResistance(
-                        that.resizing.chatbox.width,
-                        that.resizing.chatbox.model.get('default_width')
-                );
-                if (that.connection.connected) {
-                    that.resizing.chatbox.model.save({'height': height});
-                    that.resizing.chatbox.model.save({'width': width});
-                } else {
-                    that.resizing.chatbox.model.set({'height': height});
-                    that.resizing.chatbox.model.set({'width': width});
-                }
-                that.resizing = null;
-            });
-
-            return this.__super__.registerGlobalEventHandlers.apply(this, arguments);
-        },
-
         ChatBox: {
             initialize () {
                 const { _converse } = this.__super__;
@@ -102,9 +66,24 @@ converse.plugins.add('converse-dragresize', {
                 'mousedown .dragresize-topleft': 'onStartDiagonalResize'
             },
 
-            initialize () {
-                window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
-                this.__super__.initialize.apply(this, arguments);
+            render () {
+                const result = this.__super__.render.apply(this, arguments);
+                renderDragResizeHandles(this.__super__._converse, this);
+                this.setWidth();
+                return result;
+            },
+
+            _show () {
+                this.initDragResize().setDimensions();
+                this.__super__._show.apply(this, arguments);
+            }
+        },
+
+        HeadlinesBoxView: {
+            events: {
+                'mousedown .dragresize-top': 'onStartVerticalResize',
+                'mousedown .dragresize-left': 'onStartHorizontalResize',
+                'mousedown .dragresize-topleft': 'onStartDiagonalResize'
             },
 
             render () {
@@ -112,27 +91,70 @@ converse.plugins.add('converse-dragresize', {
                 renderDragResizeHandles(this.__super__._converse, this);
                 this.setWidth();
                 return result;
+            }
+        },
+
+        ControlBoxView: {
+            events: {
+                'mousedown .dragresize-top': 'onStartVerticalResize',
+                'mousedown .dragresize-left': 'onStartHorizontalResize',
+                'mousedown .dragresize-topleft': 'onStartDiagonalResize'
             },
 
-            setWidth () {
-                // If a custom width is applied (due to drag-resizing),
-                // then we need to set the width of the .chatbox element as well.
-                if (this.model.get('width')) {
-                    this.el.style.width = this.model.get('width');
-                }
+            render () {
+                const result = this.__super__.render.apply(this, arguments);
+                renderDragResizeHandles(this.__super__._converse, this);
+                this.setWidth();
+                return result;
             },
 
-            _show () {
+            renderLoginPanel () {
+                const result = this.__super__.renderLoginPanel.apply(this, arguments);
                 this.initDragResize().setDimensions();
-                this.__super__._show.apply(this, arguments);
+                return result;
+            },
+
+            renderControlBoxPane () {
+                const result = this.__super__.renderControlBoxPane.apply(this, arguments);
+                this.initDragResize().setDimensions();
+                return result;
+            }
+        },
+
+        ChatRoomView: {
+            events: {
+                'mousedown .dragresize-top': 'onStartVerticalResize',
+                'mousedown .dragresize-left': 'onStartHorizontalResize',
+                'mousedown .dragresize-topleft': 'onStartDiagonalResize'
             },
 
+            render () {
+                const result = this.__super__.render.apply(this, arguments);
+                renderDragResizeHandles(this.__super__._converse, this);
+                this.setWidth();
+                return result;
+            }
+        }
+    },
+
+    initialize () {
+        /* The initialize function gets called as soon as the plugin is
+         * loaded by converse.js's plugin machinery.
+         */
+        const { _converse } = this;
+
+        _converse.api.settings.update({
+            'allow_dragresize': true,
+        });
+
+
+        const dragResizable = {
+
             initDragResize () {
                 /* Determine and store the default box size.
                  * We need this information for the drag-resizing feature.
                  */
-                const { _converse } = this.__super__,
-                      flyout = this.el.querySelector('.box-flyout'),
+                const flyout = this.el.querySelector('.box-flyout'),
                       style = window.getComputedStyle(flyout);
 
                 if (_.isUndefined(this.model.get('height'))) {
@@ -157,6 +179,34 @@ converse.plugins.add('converse-dragresize', {
                 return this;
             },
 
+            resizeChatBox (ev) {
+                let diff;
+                if (_converse.resizing.direction.indexOf('top') === 0) {
+                    diff = ev.pageY - this.prev_pageY;
+                    if (diff) {
+                        this.height = ((this.height-diff) > (this.model.get('min_height') || 0)) ? (this.height-diff) : this.model.get('min_height');
+                        this.prev_pageY = ev.pageY;
+                        this.setChatBoxHeight(this.height);
+                    }
+                }
+                if (_.includes(_converse.resizing.direction, 'left')) {
+                    diff = this.prev_pageX - ev.pageX;
+                    if (diff) {
+                        this.width = ((this.width+diff) > (this.model.get('min_width') || 0)) ? (this.width+diff) : this.model.get('min_width');
+                        this.prev_pageX = ev.pageX;
+                        this.setChatBoxWidth(this.width);
+                    }
+                }
+            },
+
+            setWidth () {
+                // If a custom width is applied (due to drag-resizing),
+                // then we need to set the width of the .chatbox element as well.
+                if (this.model.get('width')) {
+                    this.el.style.width = this.model.get('width');
+                }
+            },
+
             setDimensions () {
                 // Make sure the chat box has the right height and width.
                 this.adjustToViewport();
@@ -165,7 +215,6 @@ converse.plugins.add('converse-dragresize', {
             },
 
             setChatBoxHeight (height) {
-                const { _converse } = this.__super__;
                 if (height) {
                     height = _converse.applyDragResistance(height, this.model.get('default_height'))+'px';
                 } else {
@@ -178,7 +227,6 @@ converse.plugins.add('converse-dragresize', {
             },
 
             setChatBoxWidth (width) {
-                const { _converse } = this.__super__;
                 if (width) {
                     width = _converse.applyDragResistance(width, this.model.get('default_width'))+'px';
                 } else {
@@ -208,7 +256,6 @@ converse.plugins.add('converse-dragresize', {
             },
 
             onStartVerticalResize (ev) {
-                const { _converse } = this.__super__;
                 if (!_converse.allow_dragresize) { return true; }
                 // Record element attributes for mouseMove().
                 const flyout = this.el.querySelector('.box-flyout'),
@@ -222,7 +269,6 @@ converse.plugins.add('converse-dragresize', {
             },
 
             onStartHorizontalResize (ev) {
-                const { _converse } = this.__super__;
                 if (!_converse.allow_dragresize) { return true; }
                 const flyout = this.el.querySelector('.box-flyout'),
                       style = window.getComputedStyle(flyout);
@@ -235,116 +281,13 @@ converse.plugins.add('converse-dragresize', {
             },
 
             onStartDiagonalResize (ev) {
-                const { _converse } = this.__super__;
                 this.onStartHorizontalResize(ev);
                 this.onStartVerticalResize(ev);
                 _converse.resizing.direction = 'topleft';
             },
+        };
+        Object.assign(_converse.ChatBoxView.prototype, dragResizable);
 
-            resizeChatBox (ev) {
-                let diff;
-                const { _converse } = this.__super__;
-                if (_converse.resizing.direction.indexOf('top') === 0) {
-                    diff = ev.pageY - this.prev_pageY;
-                    if (diff) {
-                        this.height = ((this.height-diff) > (this.model.get('min_height') || 0)) ? (this.height-diff) : this.model.get('min_height');
-                        this.prev_pageY = ev.pageY;
-                        this.setChatBoxHeight(this.height);
-                    }
-                }
-                if (_.includes(_converse.resizing.direction, 'left')) {
-                    diff = this.prev_pageX - ev.pageX;
-                    if (diff) {
-                        this.width = ((this.width+diff) > (this.model.get('min_width') || 0)) ? (this.width+diff) : this.model.get('min_width');
-                        this.prev_pageX = ev.pageX;
-                        this.setChatBoxWidth(this.width);
-                    }
-                }
-            }
-        },
-
-        HeadlinesBoxView: {
-            events: {
-                'mousedown .dragresize-top': 'onStartVerticalResize',
-                'mousedown .dragresize-left': 'onStartHorizontalResize',
-                'mousedown .dragresize-topleft': 'onStartDiagonalResize'
-            },
-
-            initialize () {
-                window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
-                return this.__super__.initialize.apply(this, arguments);
-            },
-
-            render () {
-                const result = this.__super__.render.apply(this, arguments);
-                renderDragResizeHandles(this.__super__._converse, this);
-                this.setWidth();
-                return result;
-            }
-        },
-
-        ControlBoxView: {
-            events: {
-                'mousedown .dragresize-top': 'onStartVerticalResize',
-                'mousedown .dragresize-left': 'onStartHorizontalResize',
-                'mousedown .dragresize-topleft': 'onStartDiagonalResize'
-            },
-
-            initialize () {
-                window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
-                this.__super__.initialize.apply(this, arguments);
-            },
-
-            render () {
-                const result = this.__super__.render.apply(this, arguments);
-                renderDragResizeHandles(this.__super__._converse, this);
-                this.setWidth();
-                return result;
-            },
-
-            renderLoginPanel () {
-                const result = this.__super__.renderLoginPanel.apply(this, arguments);
-                this.initDragResize().setDimensions();
-                return result;
-            },
-
-            renderControlBoxPane () {
-                const result = this.__super__.renderControlBoxPane.apply(this, arguments);
-                this.initDragResize().setDimensions();
-                return result;
-            }
-        },
-
-        ChatRoomView: {
-            events: {
-                'mousedown .dragresize-top': 'onStartVerticalResize',
-                'mousedown .dragresize-left': 'onStartHorizontalResize',
-                'mousedown .dragresize-topleft': 'onStartDiagonalResize'
-            },
-
-            initialize () {
-                window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
-                this.__super__.initialize.apply(this, arguments);
-            },
-
-            render () {
-                const result = this.__super__.render.apply(this, arguments);
-                renderDragResizeHandles(this.__super__._converse, this);
-                this.setWidth();
-                return result;
-            }
-        }
-    },
-
-    initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by converse.js's plugin machinery.
-         */
-        const { _converse } = this;
-
-        _converse.api.settings.update({
-            allow_dragresize: true,
-        });
 
         _converse.applyDragResistance = function (value, default_value) {
             /* This method applies some resistance around the
@@ -363,6 +306,45 @@ converse.plugins.add('converse-dragresize', {
             }
             return value;
         };
+
+
+        /************************ BEGIN Event Handlers ************************/
+        function registerGlobalEventHandlers () {
+
+            document.addEventListener('mousemove', function (ev) {
+                if (!_converse.resizing || !_converse.allow_dragresize) { return true; }
+                ev.preventDefault();
+                _converse.resizing.chatbox.resizeChatBox(ev);
+            });
+
+            document.addEventListener('mouseup', function (ev) {
+                if (!_converse.resizing || !_converse.allow_dragresize) { return true; }
+                ev.preventDefault();
+                const height = _converse.applyDragResistance(
+                        _converse.resizing.chatbox.height,
+                        _converse.resizing.chatbox.model.get('default_height')
+                );
+                const width = _converse.applyDragResistance(
+                        _converse.resizing.chatbox.width,
+                        _converse.resizing.chatbox.model.get('default_width')
+                );
+                if (_converse.connection.connected) {
+                    _converse.resizing.chatbox.model.save({'height': height});
+                    _converse.resizing.chatbox.model.save({'width': width});
+                } else {
+                    _converse.resizing.chatbox.model.set({'height': height});
+                    _converse.resizing.chatbox.model.set({'width': width});
+                }
+                _converse.resizing = null;
+            });
+        }
+        _converse.api.listen.on('registeredGlobalEventHandlers', registerGlobalEventHandlers);
+
+
+        _converse.api.listen.on('chatBoxInitialized', view => {
+            window.addEventListener('resize', _.debounce(() => view.setDimensions(), 100));
+        });
+        /************************ END Event Handlers ************************/
     }
 });
 

+ 109 - 88
src/converse-minimize.js

@@ -30,7 +30,7 @@ converse.plugins.add('converse-minimize', {
      *
      * NB: These plugins need to have already been loaded via require.js.
      */
-    dependencies: ["converse-chatview", "converse-controlbox", "converse-muc", "converse-muc-views", "converse-headline"],
+    dependencies: ["converse-chatview", "converse-controlbox", "converse-muc-views", "converse-headline", "converse-dragresize"],
 
     enabled (_converse) {
         return _converse.view_mode === 'overlayed';
@@ -57,20 +57,6 @@ converse.plugins.add('converse-minimize', {
                 });
             },
 
-            maximize () {
-                u.safeSave(this, {
-                    'minimized': false,
-                    'time_opened': (new Date()).getTime()
-                });
-            },
-
-            minimize () {
-                u.safeSave(this, {
-                    'minimized': true,
-                    'time_minimized': (new Date()).toISOString()
-                });
-            },
-
             maybeShow (force) {
                 if (!force && this.get('minimized')) {
                     // Must return the chatbox
@@ -122,65 +108,17 @@ converse.plugins.add('converse-minimize', {
                 if (!this.model.get('minimized')) {
                     return this.__super__.setChatBoxWidth.apply(this, arguments);
                 }
-            },
-
-            onMinimizedChanged (item) {
-                if (item.get('minimized')) {
-                    this.minimize();
-                } else {
-                    this.maximize();
-                }
-            },
-
-            maximize () {
-                // Restores a minimized chat box
-                const { _converse } = this.__super__;
-                this.insertIntoDOM();
-
-                if (!this.model.isScrolledUp()) {
-                    this.model.clearUnreadMsgCounter();
-                }
-                this.show();
-                /**
-                 * Triggered when a previously minimized chat gets maximized
-                 * @event _converse#chatBoxMaximized
-                 * @type { _converse.ChatBoxView }
-                 * @example _converse.api.listen.on('chatBoxMaximized', view => { ... });
-                 */
-                _converse.api.trigger('chatBoxMaximized', this);
-                return this;
-            },
-
-            minimize (ev) {
-                const { _converse } = this.__super__;
-                if (ev && ev.preventDefault) { ev.preventDefault(); }
-                // save the scroll position to restore it on maximize
-                if (this.model.collection && this.model.collection.browserStorage) {
-                    this.model.save({'scroll': this.content.scrollTop});
-                } else {
-                    this.model.set({'scroll': this.content.scrollTop});
-                }
-                this.setChatState(_converse.INACTIVE).model.minimize();
-                this.hide();
-                /**
-                 * Triggered when a previously maximized chat gets Minimized
-                 * @event _converse#chatBoxMinimized
-                 * @type { _converse.ChatBoxView }
-                 * @example _converse.api.listen.on('chatBoxMinimized', view => { ... });
-                 */
-                _converse.api.trigger('chatBoxMinimized', this);
-            },
+            }
         },
 
         ChatBoxHeading: {
-
             render () {
                 const { _converse } = this.__super__,
                     { __ } = _converse;
                 const result = this.__super__.render.apply(this, arguments);
-                const new_html = tpl_chatbox_minimize(
-                    {info_minimize: __('Minimize this chat box')}
-                );
+                const new_html = tpl_chatbox_minimize({
+                    'info_minimize': __('Minimize this chat box')
+                });
                 const el = this.el.querySelector('.toggle-chatbox-button');
                 if (el) {
                     el.outerHTML = new_html;
@@ -227,11 +165,109 @@ converse.plugins.add('converse-minimize', {
                 }
                 return div.innerHTML;
             }
-        },
+        }
+    },
+
+
+    initialize () {
+        /* The initialize function gets called as soon as the plugin is
+         * loaded by Converse.js's plugin machinery.
+         */
+        const { _converse } = this,
+              { __ } = _converse;
+
+        // Add new HTML templates.
+        _converse.templates.chatbox_minimize = tpl_chatbox_minimize;
+        _converse.templates.toggle_chats = tpl_toggle_chats;
+        _converse.templates.trimmed_chat = tpl_trimmed_chat;
+        _converse.templates.chats_panel = tpl_chats_panel;
+
+        _converse.api.settings.update({
+            'no_trimming': false, // Set to true for tests
+        });
 
-        ChatBoxViews: {
+
+        const minimizableChatBox = {
+            maximize () {
+                u.safeSave(this, {
+                    'minimized': false,
+                    'time_opened': (new Date()).getTime()
+                });
+            },
+
+            minimize () {
+                u.safeSave(this, {
+                    'minimized': true,
+                    'time_minimized': (new Date()).toISOString()
+                });
+            }
+        }
+        Object.assign(_converse.ChatBox.prototype, minimizableChatBox);
+
+
+        const minimizableChatBoxView = {
+            maximize () {
+                // Restores a minimized chat box
+                const { _converse } = this.__super__;
+                this.insertIntoDOM();
+
+                if (!this.model.isScrolledUp()) {
+                    this.model.clearUnreadMsgCounter();
+                }
+                this.show();
+                /**
+                 * Triggered when a previously minimized chat gets maximized
+                 * @event _converse#chatBoxMaximized
+                 * @type { _converse.ChatBoxView }
+                 * @example _converse.api.listen.on('chatBoxMaximized', view => { ... });
+                 */
+                _converse.api.trigger('chatBoxMaximized', this);
+                return this;
+            },
+
+            minimize (ev) {
+                const { _converse } = this.__super__;
+                if (ev && ev.preventDefault) { ev.preventDefault(); }
+                // save the scroll position to restore it on maximize
+                if (this.model.collection && this.model.collection.browserStorage) {
+                    this.model.save({'scroll': this.content.scrollTop});
+                } else {
+                    this.model.set({'scroll': this.content.scrollTop});
+                }
+                this.setChatState(_converse.INACTIVE).model.minimize();
+                this.hide();
+                /**
+                 * Triggered when a previously maximized chat gets Minimized
+                 * @event _converse#chatBoxMinimized
+                 * @type { _converse.ChatBoxView }
+                 * @example _converse.api.listen.on('chatBoxMinimized', view => { ... });
+                 */
+                _converse.api.trigger('chatBoxMinimized', this);
+            },
+
+            onMinimizedChanged (item) {
+                if (item.get('minimized')) {
+                    this.minimize();
+                } else {
+                    this.maximize();
+                }
+            }
+        }
+        Object.assign(_converse.ChatBoxView.prototype, minimizableChatBoxView);
+
+
+        const chatTrimmer = {
             getChatBoxWidth (view) {
-                if (!view.model.get('minimized') && u.isVisible(view.el)) {
+                if (view.model.get('id') === 'controlbox') {
+                    const controlbox = this.get('controlbox');
+                    // We return the width of the controlbox or its toggle,
+                    // depending on which is visible.
+                    if (!controlbox || !u.isVisible(controlbox.el)) {
+                        return u.getOuterWidth(_converse.controlboxtoggle.el, true);
+                    } else {
+                        return u.getOuterWidth(controlbox.el, true);
+                    }
+                } else if (!view.model.get('minimized') && u.isVisible(view.el)) {
                     return u.getOuterWidth(view.el, true);
                 }
                 return 0;
@@ -315,25 +351,8 @@ converse.plugins.add('converse-minimize', {
                 return model;
             }
         }
-    },
-
+        Object.assign(_converse.ChatBoxViews.prototype, chatTrimmer);
 
-    initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by Converse.js's plugin machinery.
-         */
-        const { _converse } = this,
-              { __ } = _converse;
-
-        // Add new HTML templates.
-        _converse.templates.chatbox_minimize = tpl_chatbox_minimize;
-        _converse.templates.toggle_chats = tpl_toggle_chats;
-        _converse.templates.trimmed_chat = tpl_trimmed_chat;
-        _converse.templates.chats_panel = tpl_chats_panel;
-
-        _converse.api.settings.update({
-            no_trimming: false, // Set to true for phantomjs tests (where browser apparently has no width)
-        });
 
         _converse.api.promises.add('minimizedChatsInitialized');
 
@@ -525,6 +544,7 @@ converse.plugins.add('converse-minimize', {
             }
         });
 
+        /************************ BEGIN Event Handlers ************************/
         Promise.all([
             _converse.api.waitUntil('connectionInitialized'),
             _converse.api.waitUntil('chatBoxViewsInitialized')
@@ -559,5 +579,6 @@ converse.plugins.add('converse-minimize', {
                 _converse.chatboxviews.trimChats(chatbox);
             }
         });
+        /************************ END Event Handlers ************************/
     }
 });

+ 31 - 28
src/converse-muc-views.js

@@ -62,41 +62,14 @@ converse.plugins.add('converse-muc-views', {
     dependencies: ["converse-autocomplete", "converse-modal", "converse-controlbox", "converse-chatview"],
 
     overrides: {
-
         ControlBoxView: {
-
-            renderRoomsPanel () {
-                const { _converse } = this.__super__;
-                if (this.roomspanel && u.isVisible(this.roomspanel.el)) {
-                    return;
-                }
-                this.roomspanel = new _converse.RoomsPanel({
-                    'model': new (_converse.RoomsPanelModel.extend({
-                        'id': `converse.roomspanel${_converse.bare_jid}`, // Required by web storage
-                        'browserStorage': new BrowserStorage[_converse.config.get('storage')](
-                            `converse.roomspanel${_converse.bare_jid}`)
-                    }))()
-                });
-                this.roomspanel.model.fetch();
-                this.el.querySelector('.controlbox-pane').insertAdjacentElement(
-                    'beforeEnd', this.roomspanel.render().el);
-
-                /**
-                 * Triggered once the section of the _converse.ControlBoxView
-                 * which shows gropuchats has been rendered.
-                 * @event _converse#roomsPanelRendered
-                 * @example _converse.api.listen.on('roomsPanelRendered', () => { ... });
-                 */
-                _converse.api.trigger('roomsPanelRendered');
-            },
-
             renderControlBoxPane () {
                 const { _converse } = this.__super__;
                 this.__super__.renderControlBoxPane.apply(this, arguments);
                 if (_converse.allow_muc) {
                     this.renderRoomsPanel();
                 }
-            },
+            }
         }
     },
 
@@ -123,6 +96,35 @@ converse.plugins.add('converse-muc-views', {
             }
         });
 
+
+        Object.assign(_converse.ControlBoxView.prototype, {
+
+            renderRoomsPanel () {
+                if (this.roomspanel && u.isVisible(this.roomspanel.el)) {
+                    return;
+                }
+                this.roomspanel = new _converse.RoomsPanel({
+                    'model': new (_converse.RoomsPanelModel.extend({
+                        'id': `converse.roomspanel${_converse.bare_jid}`, // Required by web storage
+                        'browserStorage': new BrowserStorage[_converse.config.get('storage')](
+                            `converse.roomspanel${_converse.bare_jid}`)
+                    }))()
+                });
+                this.roomspanel.model.fetch();
+                this.el.querySelector('.controlbox-pane').insertAdjacentElement(
+                    'beforeEnd', this.roomspanel.render().el);
+
+                /**
+                 * Triggered once the section of the _converse.ControlBoxView
+                 * which shows gropuchats has been rendered.
+                 * @event _converse#roomsPanelRendered
+                 * @example _converse.api.listen.on('roomsPanelRendered', () => { ... });
+                 */
+                _converse.api.trigger('roomsPanelRendered');
+            }
+        });
+
+
         function ___ (str) {
             /* This is part of a hack to get gettext to scan strings to be
             * translated. Strings we cannot send to the function above because
@@ -572,6 +574,7 @@ converse.plugins.add('converse-muc-views', {
                  * @example _converse.api.listen.on('chatRoomOpened', view => { ... });
                  */
                 _converse.api.trigger('chatRoomOpened', this);
+                _converse.api.trigger('chatBoxInitialized', this);
             },
 
             render () {

+ 2 - 2
src/converse-oauth.js

@@ -38,12 +38,12 @@ converse.plugins.add("converse-oauth", {
         /* For example, the private *_converse* object has a
          * method "onConnected". You can override that method as follows:
          */
-        'LoginPanel': {
+        LoginPanel: {
 
             insertOAuthProviders () {
                 const { _converse } = this.__super__;
                 if (_.isUndefined(this.oauth_providers_view)) {
-                    this.oauth_providers_view = 
+                    this.oauth_providers_view =
                         new _converse.OAuthProvidersView({'model': _converse.oauth_providers});
 
                     this.oauth_providers_view.render();

+ 118 - 123
src/converse-omemo.js

@@ -172,6 +172,90 @@ converse.plugins.add('converse-omemo', {
         },
 
         ChatBox: {
+            async getMessageAttributesFromStanza (stanza, original_stanza) {
+                const { _converse } = this.__super__;
+                const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop(),
+                      attrs = await this.__super__.getMessageAttributesFromStanza.apply(this, arguments);
+
+                if (!encrypted || !_converse.config.get('trusted')) {
+                    return attrs;
+                } else {
+                    return this.getEncryptionAttributesfromStanza(stanza, original_stanza, attrs);
+                }
+            },
+
+            async sendMessage (text, spoiler_hint) {
+                if (this.get('omemo_active') && text) {
+                    const { _converse } = this.__super__;
+                    const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint);
+                    attrs['is_encrypted'] = true;
+                    attrs['plaintext'] = attrs.message;
+                    try {
+                        const devices = await _converse.getBundlesAndBuildSessions(this);
+                        const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
+                        _converse.api.send(stanza);
+                    } catch (e) {
+                        this.handleMessageSendError(e);
+                        return false;
+                    }
+                    return true;
+                } else {
+                    return this.__super__.sendMessage.apply(this, arguments);
+                }
+            }
+        },
+
+        ChatBoxView:  {
+            events: {
+                'click .toggle-omemo': 'toggleOMEMO'
+            },
+
+            initialize () {
+                this.__super__.initialize.apply(this, arguments);
+                this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
+                this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
+            },
+
+            showMessage (message) {
+                // We don't show a message if it's only keying material
+                if (!message.get('is_only_key')) {
+                    return this.__super__.showMessage.apply(this, arguments);
+                }
+            }
+        },
+
+        ChatRoomView: {
+            events: {
+                'click .toggle-omemo': 'toggleOMEMO'
+            },
+
+            initialize () {
+                this.__super__.initialize.apply(this, arguments);
+                this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
+                this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
+            }
+        }
+    },
+
+    initialize () {
+        /* The initialize function gets called as soon as the plugin is
+         * loaded by Converse.js's plugin machinery.
+         */
+        const { _converse } = this,
+              { __ } = _converse;
+
+        _converse.api.promises.add(['OMEMOInitialized']);
+
+        _converse.NUM_PREKEYS = 100; // Set here so that tests can override
+
+
+        /**
+         * Mixin object that contains OMEMO-related methods for
+         * {@link _converse.ChatBox} or {@link _converse.ChatRoom} objects.
+         *
+         * @typedef {Object} OMEMOEnabledChatBox
+         */
+        const OMEMOEnabledChatBox = {
 
             async encryptMessage (plaintext) {
                 // The client MUST use fresh, randomly generated key/IV pairs
@@ -219,7 +303,6 @@ converse.plugins.add('converse-omemo', {
             },
 
             reportDecryptionError (e) {
-                const { _converse } = this.__super__;
                 if (_converse.debug) {
                     const { __ } = _converse;
                     this.messages.create({
@@ -231,8 +314,7 @@ converse.plugins.add('converse-omemo', {
             },
 
             async handleDecryptedWhisperMessage (attrs, key_and_tag) {
-                const { _converse } = this.__super__,
-                      encrypted = attrs.encrypted,
+                const encrypted = attrs.encrypted,
                       devicelist = _converse.devicelists.getDeviceList(this.get('jid'));
 
                 this.save('omemo_supported', true);
@@ -250,8 +332,7 @@ converse.plugins.add('converse-omemo', {
             },
 
             decrypt (attrs) {
-                const { _converse } = this.__super__,
-                      session_cipher = this.getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10));
+                const session_cipher = this.getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10));
 
                 // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
                 if (attrs.encrypted.prekey === true) {
@@ -284,8 +365,7 @@ converse.plugins.add('converse-omemo', {
             },
 
             getEncryptionAttributesfromStanza (stanza, original_stanza, attrs) {
-                const { _converse } = this.__super__,
-                      encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop(),
+                const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop(),
                       header = encrypted.querySelector('header'),
                       key = sizzle(`key[rid="${_converse.omemo_store.get('device_id')}"]`, encrypted).pop();
                 if (key) {
@@ -303,22 +383,8 @@ converse.plugins.add('converse-omemo', {
                 }
             },
 
-            async getMessageAttributesFromStanza (stanza, original_stanza) {
-                const { _converse } = this.__super__,
-                      encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop(),
-                      attrs = await this.__super__.getMessageAttributesFromStanza.apply(this, arguments);
-
-                if (!encrypted || !_converse.config.get('trusted')) {
-                    return attrs;
-                } else {
-                    return this.getEncryptionAttributesfromStanza(stanza, original_stanza, attrs);
-                }
-            },
-
-
             getSessionCipher (jid, id) {
-                const { _converse } = this.__super__,
-                        address = new libsignal.SignalProtocolAddress(jid, id);
+                const address = new libsignal.SignalProtocolAddress(jid, id);
                 this.session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
                 return this.session_cipher;
             },
@@ -330,8 +396,6 @@ converse.plugins.add('converse-omemo', {
             },
 
             handleMessageSendError (e) {
-                const { _converse } = this.__super__,
-                      { __ } = _converse;
                 if (e.name === 'IQError') {
                     this.save('omemo_supported', false);
 
@@ -355,46 +419,12 @@ converse.plugins.add('converse-omemo', {
                 } else {
                     throw e;
                 }
-            },
-
-            async sendMessage (text, spoiler_hint) {
-                if (this.get('omemo_active') && text) {
-                    const { _converse } = this.__super__;
-                    const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint);
-                    attrs['is_encrypted'] = true;
-                    attrs['plaintext'] = attrs.message;
-                    try {
-                        const devices = await _converse.getBundlesAndBuildSessions(this);
-                        const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
-                        _converse.api.send(stanza);
-                    } catch (e) {
-                        this.handleMessageSendError(e);
-                        return false;
-                    }
-                    return true;
-                } else {
-                    return this.__super__.sendMessage.apply(this, arguments);
-                }
             }
-        },
+        }
+        Object.assign(_converse.ChatBox.prototype, OMEMOEnabledChatBox);
 
-        ChatBoxView:  {
-            events: {
-                'click .toggle-omemo': 'toggleOMEMO'
-            },
 
-            initialize () {
-                this.__super__.initialize.apply(this, arguments);
-                this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
-                this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
-            },
-
-            showMessage (message) {
-                // We don't show a message if it's only keying material
-                if (!message.get('is_only_key')) {
-                    return this.__super__.showMessage.apply(this, arguments);
-                }
-            },
+        const OMEMOEnabledChatView = {
 
             onOMEMOSupportedDetermined () {
                 if (!this.model.get('omemo_supported') && this.model.get('omemo_active')) {
@@ -405,82 +435,47 @@ converse.plugins.add('converse-omemo', {
             },
 
             renderOMEMOToolbarButton () {
-                const { _converse } = this.__super__,
-                      { __ } = _converse,
-                      icon = this.el.querySelector('.toggle-omemo'),
-                      html = tpl_toolbar_omemo(Object.assign(this.model.toJSON(), {'__': __}));
+                if (this.model.get('type') !== _converse.CHATROOMS_TYPE ||
+                        this.model.features.get('membersonly') &&
+                        this.model.features.get('nonanonymous')) {
 
-                if (icon) {
-                    icon.outerHTML = html;
+                    const icon = this.el.querySelector('.toggle-omemo');
+                    const html = tpl_toolbar_omemo(Object.assign(this.model.toJSON(), {'__': __}));
+                    if (icon) {
+                        icon.outerHTML = html;
+                    } else {
+                        this.el.querySelector('.chat-toolbar').insertAdjacentHTML('beforeend', html);
+                    }
                 } else {
-                    this.el.querySelector('.chat-toolbar').insertAdjacentHTML('beforeend', html);
+                    const icon = this.el.querySelector('.toggle-omemo');
+                    if (icon) {
+                        icon.parentElement.removeChild(icon);
+                    }
                 }
             },
 
             toggleOMEMO (ev) {
-                const { _converse } = this.__super__, { __ } = _converse;
                 if (!this.model.get('omemo_supported')) {
-                    return _converse.api.alert.show(
-                        Strophe.LogLevel.ERROR,
-                        __('Error'),
-                        [__("Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.",
+                    let messages;
+                    if (this.model.get('type') === _converse.CHATROOMS_TYPE) {
+                        messages = [__(
+                            'Cannot use end-to-end encryption in this groupchat, '+
+                            'either the groupchat has some anonymity or not all participants support OMEMO.'
+                        )];
+                    } else {
+                        messages = [__(
+                            "Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.",
                             this.model.contact.getDisplayName()
-                           )]
-                    )
-                }
-                ev.preventDefault();
-                this.model.save({'omemo_active': !this.model.get('omemo_active')});
-            }
-        },
-
-        ChatRoomView: {
-            events: {
-                'click .toggle-omemo': 'toggleOMEMO'
-            },
-
-            initialize () {
-                this.__super__.initialize.apply(this, arguments);
-                this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
-                this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
-            },
-
-            toggleOMEMO (ev) {
-                const { _converse } = this.__super__, { __ } = _converse;
-                if (!this.model.get('omemo_supported')) {
-                    return _converse.api.alert.show(
-                        Strophe.LogLevel.ERROR,
-                        __('Error'),
-                        [__('Cannot use end-to-end encryption in this groupchat, '+
-                            'either the groupchat has some anonymity or not all participants support OMEMO.')]
-                    );
+                        )];
+                    }
+                    return _converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), messages);
                 }
                 ev.preventDefault();
                 this.model.save({'omemo_active': !this.model.get('omemo_active')});
-            },
-
-            renderOMEMOToolbarButton () {
-                if (this.model.features.get('membersonly') && this.model.features.get('nonanonymous')) {
-                    this.__super__.renderOMEMOToolbarButton.apply(arguments);
-                } else {
-                    const icon = this.el.querySelector('.toggle-omemo');
-                    if (icon) {
-                        icon.parentElement.removeChild(icon);
-                    }
-                }
             }
         }
-    },
+        Object.assign(_converse.ChatBoxView.prototype, OMEMOEnabledChatView);
 
-    initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by Converse.js's plugin machinery.
-         */
-        const { _converse } = this,
-              { __ } = _converse;
-
-        _converse.api.promises.add(['OMEMOInitialized']);
-
-        _converse.NUM_PREKEYS = 100; // Set here so that tests can override
 
         async function generateFingerprint (device) {
             if (_.get(device.get('bundle'), 'fingerprint')) {

+ 49 - 45
src/converse-register.js

@@ -45,17 +45,6 @@ converse.plugins.add('converse-register', {
         // New functions which don't exist yet can also be added.
 
         LoginPanel: {
-
-            insertRegisterLink () {
-                const { _converse } = this.__super__;
-                if (_.isUndefined(this.registerlinkview)) {
-                    this.registerlinkview = new _converse.RegisterLinkView({'model': this.model});
-                    this.registerlinkview.render();
-                    this.el.querySelector('.buttons').insertAdjacentElement('afterend', this.registerlinkview.el);
-                }
-                this.registerlinkview.render();
-            },
-
             render (cfg) {
                 const { _converse } = this.__super__;
                 this.__super__.render.apply(this, arguments);
@@ -67,11 +56,50 @@ converse.plugins.add('converse-register', {
         },
 
         ControlBoxView: {
+            renderLoginPanel () {
+                /* Also render a registration panel, when rendering the
+                 * login panel.
+                 */
+                this.__super__.renderLoginPanel.apply(this, arguments);
+                this.renderRegistrationPanel();
+                return this;
+            }
+        }
+    },
 
-            initialize () {
-                this.__super__.initialize.apply(this, arguments);
-                this.model.on('change:active-form', this.showLoginOrRegisterForm.bind(this))
-            },
+    initialize () {
+        /* The initialize function gets called as soon as the plugin is
+         * loaded by converse.js's plugin machinery.
+         */
+        const { _converse } = this,
+            { __ } = _converse;
+
+        _converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL';
+        _converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED';
+        _converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT';
+        _converse.CONNECTION_STATUS[Strophe.Status.NOTACCEPTABLE] = 'NOTACCEPTABLE';
+
+        _converse.api.settings.update({
+            'allow_registration': true,
+            'domain_placeholder': __(" e.g. conversejs.org"),  // Placeholder text shown in the domain input on the registration form
+            'providers_link': 'https://compliance.conversations.im/', // Link to XMPP providers shown on registration page
+            'registration_domain': ''
+        });
+
+
+        Object.assign(_converse.LoginPanel.prototype, {
+
+            insertRegisterLink () {
+                if (_.isUndefined(this.registerlinkview)) {
+                    this.registerlinkview = new _converse.RegisterLinkView({'model': this.model});
+                    this.registerlinkview.render();
+                    this.el.querySelector('.buttons').insertAdjacentElement('afterend', this.registerlinkview.el);
+                }
+                this.registerlinkview.render();
+            }
+        });
+
+        Object.assign(_converse.ControlBoxView.prototype, {
 
             showLoginOrRegisterForm () {
                 const { _converse } = this.__super__;
@@ -88,7 +116,6 @@ converse.plugins.add('converse-register', {
             },
 
             renderRegistrationPanel () {
-                const { _converse } = this.__super__;
                 if (_converse.allow_registration) {
                     this.registerpanel = new _converse.RegisterPanel({
                         'model': this.model
@@ -102,36 +129,7 @@ converse.plugins.add('converse-register', {
                     this.showLoginOrRegisterForm();
                 }
                 return this;
-            },
-
-            renderLoginPanel () {
-                /* Also render a registration panel, when rendering the
-                 * login panel.
-                 */
-                this.__super__.renderLoginPanel.apply(this, arguments);
-                this.renderRegistrationPanel();
-                return this;
             }
-        }
-    },
-
-    initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by converse.js's plugin machinery.
-         */
-        const { _converse } = this,
-            { __ } = _converse;
-
-        _converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL';
-        _converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED';
-        _converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT';
-        _converse.CONNECTION_STATUS[Strophe.Status.NOTACCEPTABLE] = 'NOTACCEPTABLE';
-
-        _converse.api.settings.update({
-            'allow_registration': true,
-            'domain_placeholder': __(" e.g. conversejs.org"),  // Placeholder text shown in the domain input on the registration form
-            'providers_link': 'https://compliance.conversations.im/', // Link to XMPP providers shown on registration page
-            'registration_domain': ''
         });
 
 
@@ -679,6 +677,12 @@ converse.plugins.add('converse-register', {
                 return false;
             }
         });
+
+        /************************ BEGIN Event Handlers ************************/
+        _converse.api.listen.on('controlboxInitialized', view => {
+            view.model.on('change:active-form', view.showLoginOrRegisterForm, view);
+        });
+        /************************ END Event Handlers ************************/
     }
 });
 

+ 10 - 61
src/converse-rosterview.js

@@ -29,27 +29,6 @@ converse.plugins.add('converse-rosterview', {
 
     dependencies: ["converse-roster", "converse-modal"],
 
-    overrides: {
-        // Overrides mentioned here will be picked up by converse.js's
-        // plugin architecture they will replace existing methods on the
-        // relevant objects or classes.
-        //
-        // New functions which don't exist yet can also be added.
-        afterReconnected () {
-            this.__super__.afterReconnected.apply(this, arguments);
-        },
-
-        RosterGroups: {
-            comparator () {
-                // RosterGroupsComparator only gets set later (once i18n is
-                // set up), so we need to wrap it in this nameless function.
-                const { _converse } = this.__super__;
-                return _converse.RosterGroupsComparator.apply(this, arguments);
-            }
-        }
-    },
-
-
     initialize () {
         /* The initialize function gets called as soon as the plugin is
          * loaded by converse.js's plugin machinery.
@@ -78,36 +57,6 @@ converse.plugins.add('converse-rosterview', {
             'away': __('This contact is away')
         };
         const LABEL_GROUPS = __('Groups');
-        const HEADER_CURRENT_CONTACTS =  __('My contacts');
-        const HEADER_PENDING_CONTACTS = __('Pending contacts');
-        const HEADER_REQUESTING_CONTACTS = __('Contact requests');
-        const HEADER_UNGROUPED = __('Ungrouped');
-        const HEADER_WEIGHTS = {};
-        HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 0;
-        HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS]    = 1;
-        HEADER_WEIGHTS[HEADER_UNGROUPED]           = 2;
-        HEADER_WEIGHTS[HEADER_PENDING_CONTACTS]    = 3;
-
-        _converse.RosterGroupsComparator = function (a, b) {
-            /* Groups are sorted alphabetically, ignoring case.
-             * However, Ungrouped, Requesting Contacts and Pending Contacts
-             * appear last and in that order.
-             */
-            a = a.get('name');
-            b = b.get('name');
-            const special_groups = Object.keys(HEADER_WEIGHTS);
-            const a_is_special = _.includes(special_groups, a);
-            const b_is_special = _.includes(special_groups, b);
-            if (!a_is_special && !b_is_special ) {
-                return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
-            } else if (a_is_special && b_is_special) {
-                return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : (HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0);
-            } else if (!a_is_special && b_is_special) {
-                return (b === HEADER_REQUESTING_CONTACTS) ? 1 : -1;
-            } else if (a_is_special && !b_is_special) {
-                return (a === HEADER_REQUESTING_CONTACTS) ? -1 : 1;
-            }
-        };
 
 
         _converse.AddContactModal = _converse.BootstrapModal.extend({
@@ -678,7 +627,7 @@ converse.plugins.add('converse-rosterview', {
                 let matches;
                 q = q.toLowerCase();
                 if (type === 'state') {
-                    if (this.model.get('name') === HEADER_REQUESTING_CONTACTS) {
+                    if (this.model.get('name') === _converse.HEADER_REQUESTING_CONTACTS) {
                         // When filtering by chat state, we still want to
                         // show requesting contacts, even though they don't
                         // have the state in question.
@@ -747,13 +696,13 @@ converse.plugins.add('converse-rosterview', {
             },
 
             onContactSubscriptionChange (contact) {
-                if ((this.model.get('name') === HEADER_PENDING_CONTACTS) && contact.get('subscription') !== 'from') {
+                if ((this.model.get('name') === _converse.HEADER_PENDING_CONTACTS) && contact.get('subscription') !== 'from') {
                     this.removeContact(contact);
                 }
             },
 
             onContactRequestChange (contact) {
-                if ((this.model.get('name') === HEADER_REQUESTING_CONTACTS) && !contact.get('requesting')) {
+                if ((this.model.get('name') === _converse.HEADER_REQUESTING_CONTACTS) && !contact.get('requesting')) {
                     this.removeContact(contact);
                 }
             },
@@ -926,16 +875,16 @@ converse.plugins.add('converse-rosterview', {
                 this.update();
                 if (_.has(contact.changed, 'subscription')) {
                     if (contact.changed.subscription === 'from') {
-                        this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
+                        this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS);
                     } else if (_.includes(['both', 'to'], contact.get('subscription'))) {
                         this.addExistingContact(contact);
                     }
                 }
                 if (_.has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') {
-                    this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
+                    this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS);
                 }
                 if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
-                    this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
+                    this.addContactToGroup(contact, _converse.HEADER_REQUESTING_CONTACTS);
                 }
                 this.updateFilter();
             },
@@ -961,10 +910,10 @@ converse.plugins.add('converse-rosterview', {
                 if (_converse.roster_groups) {
                     groups = contact.get('groups');
                     if (groups.length === 0) {
-                        groups = [HEADER_UNGROUPED];
+                        groups = [_converse.HEADER_UNGROUPED];
                     }
                 } else {
-                    groups = [HEADER_CURRENT_CONTACTS];
+                    groups = [_converse.HEADER_CURRENT_CONTACTS];
                 }
                 _.each(groups, _.bind(this.addContactToGroup, this, contact, _, options));
             },
@@ -982,9 +931,9 @@ converse.plugins.add('converse-rosterview', {
                         return;
                     }
                     if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
-                        this.addContactToGroup(contact, HEADER_PENDING_CONTACTS, options);
+                        this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS, options);
                     } else if (contact.get('requesting') === true) {
-                        this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS, options);
+                        this.addContactToGroup(contact, _converse.HEADER_REQUESTING_CONTACTS, options);
                     }
                 }
                 return this;

+ 1 - 1
src/converse-singleton.js

@@ -82,7 +82,7 @@ converse.plugins.add('converse-singleton', {
                 const { _converse } = this.__super__;
                 if (_converse.isUniView()) {
                     return false;
-                } else { 
+                } else {
                     return this.__super__.shouldShowOnTextMessage.apply(this, arguments);
                 }
             },

+ 1 - 3
src/headless/converse-bookmarks.js

@@ -40,13 +40,11 @@ converse.plugins.add('converse-bookmarks', {
         // New functions which don't exist yet can also be added.
 
         ChatRoom: {
-
             getAndPersistNickname(nick) {
                 const { _converse } = this.__super__;
                 nick = nick || _converse.getNicknameFromBookmark(this.get('jid'));
                 return this.__super__.getAndPersistNickname.call(this, nick);
-            },
-
+            }
         }
     },
 

+ 1 - 1
src/headless/converse-core.js

@@ -251,7 +251,7 @@ _converse.log = function (message, level, style='') {
         message = message.outerHTML;
     }
     const prefix = style ? '%c' : '';
-    const logger = _.assign({
+    const logger = Object.assign({
             'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop,
             'error': _.get(console, 'log') ? console.log.bind(console) : _.noop,
             'info': _.get(console, 'log') ? console.log.bind(console) : _.noop,

+ 42 - 43
src/headless/converse-mam.js

@@ -29,9 +29,40 @@ converse.plugins.add('converse-mam', {
         // Overrides mentioned here will be picked up by converse.js's
         // plugin architecture they will replace existing methods on the
         // relevant objects or classes.
-        //
-        // New functions which don't exist yet can also be added.
         ChatBox: {
+            async getDuplicateMessage (stanza) {
+                const message = await this.__super__.getDuplicateMessage.apply(this, arguments);
+                if (!message) {
+                    return this.findDuplicateFromArchiveID(stanza);
+                }
+                return message;
+            },
+
+            getUpdatedMessageAttributes (message, stanza) {
+                const attrs = this.__super__.getUpdatedMessageAttributes.apply(this, arguments);
+                if (message && !message.get('is_archived')) {
+                    return Object.assign(attrs, {
+                        'is_archived': this.isArchived(stanza)
+                    }, this.getStanzaIDs(stanza))
+                }
+                return attrs;
+            }
+        }
+    },
+
+    initialize () {
+        /* The initialize function gets called as soon as the plugin is
+         * loaded by Converse.js's plugin machinery.
+         */
+        const { _converse } = this;
+
+        _converse.api.settings.update({
+            archived_messages_page_size: '50',
+            message_archiving: undefined, // Supported values are 'always', 'never', 'roster' (https://xmpp.org/extensions/xep-0313.html#prefs)
+            message_archiving_timeout: 20000, // Time (in milliseconds) to wait before aborting MAM request
+        });
+
+        const MAMEnabledChat = {
 
             fetchNewestMessages () {
                 /* Fetches messages that might have been archived *after*
@@ -40,7 +71,6 @@ converse.plugins.add('converse-mam', {
                 if (this.disable_mam) {
                     return;
                 }
-                const { _converse } = this.__super__;
                 const most_recent_msg = u.getMostRecentMessage(this);
 
                 if (_.isNil(most_recent_msg)) {
@@ -59,7 +89,6 @@ converse.plugins.add('converse-mam', {
                 if (this.disable_mam) {
                     return;
                 }
-                const { _converse } = this.__super__;
                 const is_groupchat = this.get('type') === CHATROOMS_TYPE;
                 const mam_jid = is_groupchat ? this.get('jid') : _converse.bare_jid;
                 if (!(await _converse.api.disco.supports(Strophe.NS.MAM, mam_jid))) {
@@ -92,7 +121,6 @@ converse.plugins.add('converse-mam', {
             },
 
             async findDuplicateFromArchiveID (stanza) {
-                const { _converse } = this.__super__;
                 const result = sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop();
                 if (!result) {
                     return null;
@@ -107,32 +135,11 @@ converse.plugins.add('converse-mam', {
                 return this.messages.findWhere(query);
             },
 
-            async getDuplicateMessage (stanza) {
-                const message = await this.__super__.getDuplicateMessage.apply(this, arguments);
-                if (!message) {
-                    return this.findDuplicateFromArchiveID(stanza);
-                }
-                return message;
-            },
-
-            getUpdatedMessageAttributes (message, stanza) {
-                const attrs = this.__super__.getUpdatedMessageAttributes.apply(this, arguments);
-                if (message && !message.get('is_archived')) {
-                    return Object.assign(attrs, {
-                        'is_archived': this.isArchived(stanza)
-                    }, this.getStanzaIDs(stanza))
-                }
-                return attrs;
-            }
-        },
+        }
+        Object.assign(_converse.ChatBox.prototype, MAMEnabledChat);
 
-        ChatRoom: {
-            initialize () {
-                this.__super__.initialize.apply(this, arguments);
-                this.on('change:mam_enabled', this.fetchArchivedMessagesIfNecessary, this);
-                this.on('change:connection_status', this.fetchArchivedMessagesIfNecessary, this);
-            },
 
+        Object.assign(_converse.ChatRoom.prototype, {
             fetchArchivedMessagesIfNecessary () {
                 if (this.get('connection_status') !== converse.ROOMSTATUS.ENTERED ||
                         !this.get('mam_enabled') ||
@@ -142,22 +149,9 @@ converse.plugins.add('converse-mam', {
                 this.fetchArchivedMessages();
                 this.save({'mam_initialized': true});
             }
-        },
-
-    },
-
-    initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by Converse.js's plugin machinery.
-         */
-        const { _converse } = this;
-
-        _converse.api.settings.update({
-            archived_messages_page_size: '50',
-            message_archiving: undefined, // Supported values are 'always', 'never', 'roster' (https://xmpp.org/extensions/xep-0313.html#prefs)
-            message_archiving_timeout: 20000, // Time (in milliseconds) to wait before aborting MAM request
         });
 
+
         _converse.onMAMError = function (iq) {
             if (iq.querySelectorAll('feature-not-implemented').length) {
                 _converse.log(
@@ -220,6 +214,11 @@ converse.plugins.add('converse-mam', {
         _converse.api.listen.on('afterMessagesFetched', chat => chat.fetchNewestMessages());
         _converse.api.listen.on('chatReconnected', chat => chat.fetchNewestMessages());
         _converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.MAM));
+
+        _converse.api.listen.on('chatRoomOpened', (room) => {
+            room.on('change:mam_enabled', room.fetchArchivedMessagesIfNecessary, room);
+            room.on('change:connection_status', room.fetchArchivedMessagesIfNecessary, room);
+        });
         /************************ END Event Handlers **************************/
 
 

+ 32 - 0
src/headless/converse-roster.js

@@ -36,6 +36,17 @@ converse.plugins.add('converse-roster', {
             'rosterInitialized',
         ]);
 
+        _converse.HEADER_CURRENT_CONTACTS =  __('My contacts');
+        _converse.HEADER_PENDING_CONTACTS = __('Pending contacts');
+        _converse.HEADER_REQUESTING_CONTACTS = __('Contact requests');
+        _converse.HEADER_UNGROUPED = __('Ungrouped');
+
+        const HEADER_WEIGHTS = {};
+        HEADER_WEIGHTS[_converse.HEADER_REQUESTING_CONTACTS] = 0;
+        HEADER_WEIGHTS[_converse.HEADER_CURRENT_CONTACTS]    = 1;
+        HEADER_WEIGHTS[_converse.HEADER_UNGROUPED]           = 2;
+        HEADER_WEIGHTS[_converse.HEADER_PENDING_CONTACTS]    = 3;
+
 
         _converse.registerPresenceHandler = function () {
             _converse.unregisterPresenceHandler();
@@ -381,6 +392,10 @@ converse.plugins.add('converse-roster', {
             model: _converse.RosterContact,
 
             comparator (contact1, contact2) {
+                /* Groups are sorted alphabetically, ignoring case.
+                 * However, Ungrouped, Requesting Contacts and Pending Contacts
+                 * appear last and in that order.
+                 */
                 const status1 = contact1.presence.get('show') || 'offline';
                 const status2 = contact2.presence.get('show') || 'offline';
                 if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) {
@@ -854,6 +869,23 @@ converse.plugins.add('converse-roster', {
         _converse.RosterGroups = Backbone.Collection.extend({
             model: _converse.RosterGroup,
 
+            comparator (a, b) {
+                a = a.get('name');
+                b = b.get('name');
+                const special_groups = Object.keys(HEADER_WEIGHTS);
+                const a_is_special = _.includes(special_groups, a);
+                const b_is_special = _.includes(special_groups, b);
+                if (!a_is_special && !b_is_special ) {
+                    return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
+                } else if (a_is_special && b_is_special) {
+                    return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : (HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0);
+                } else if (!a_is_special && b_is_special) {
+                    return (b === _converse.HEADER_REQUESTING_CONTACTS) ? 1 : -1;
+                } else if (a_is_special && !b_is_special) {
+                    return (a === _converse.HEADER_REQUESTING_CONTACTS) ? -1 : 1;
+                }
+            },
+
             fetchRosterGroups () {
                 /* Fetches all the roster groups from sessionStorage.
                 *