瀏覽代碼

Get the webpack dev server to work again

This required triggering the `converse-loaded` event in the entry.js
file, which means it won't be triggered for `@converse/headless` when
used in isolation.

Not ideal, but probably ok because consumers of `@converse/headless`
should probably import it into their own project in any case.
JC Brand 4 年之前
父節點
當前提交
dd609c1cec
共有 10 個文件被更改,包括 1026 次插入998 次删除
  1. 4 22
      docs/source/dependencies.rst
  2. 8 31
      docs/source/plugin_development.rst
  3. 377 306
      package-lock.json
  4. 3 3
      package.json
  5. 587 594
      spec/mock.js
  6. 11 0
      src/entry.js
  7. 0 10
      src/headless/core.js
  8. 29 28
      webpack.html
  9. 1 1
      webpack.prod.js
  10. 6 3
      webpack.serve.js

+ 4 - 22
docs/source/dependencies.rst

@@ -2,9 +2,9 @@
 
     <div id="banner"><a href="https://github.com/jcbrand/converse.js/blob/master/docs/source/theming.rst">Edit me on GitHub</a></div>
 
-============
-Dependencies
-============
+=============================
+Starting up a dev environment
+=============================
 
 Installing the 3rd party dependencies
 =====================================
@@ -64,30 +64,12 @@ the list under the ``devDependencies`` in `package.json <https://github.com/jcbr
     where you can log in and be taken directly to the chatroom.
 
 
-Brief description of Converse's dependencies
-===============================================
-
-Converse relies on the following dependencies:
-
-* `DayJS <https://github.com/iamkun/dayjs>`_ provides a better API for handling dates and times.
-* `Strophe.js <http://strophe.im/>`_ maintains the XMPP session, is used to
-  build XMPP stanzas, to send them, and to register handlers for received stanzas.
-* `lodash <https://lodash.com/>`_ provides very useful utility functions.
-* `Skeletor <https://github.com/skeletorjs/skeletor/>`_, a `Backbone <http://backbonejs.org/>`_ fork
-  which is used to model the data as Models and Collections and to create Views that render the UI.
-* `pluggable.js <https://github.com/jcbrand/pluggable.js>`_ provides the plugin
-  architecture for Converse. It registers and initializes plugins and
-  allows existing attributes, functions and objects on Converse to be
-  overridden inside plugins.
-
 .. _`dependency-libsignal`:
 
 Libsignal
 ---------
 
-If you want OMEMO encryption, you need to load `libsignal
-<https://github.com/signalapp/libsignal-protocol-javascript>`_ separately in
-your page.
+If you want OMEMO encryption, you need to load `libsignal <https://github.com/signalapp/libsignal-protocol-javascript>`_ separately in your page.
 
 For example::
 

+ 8 - 31
docs/source/plugin_development.rst

@@ -167,7 +167,7 @@ Accessing 3rd party libraries
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Immediately inside the module shown above you can access 3rd party libraries (such
-dayjs and lodash) via the ``converse.env`` map.
+dayjs) via the ``converse.env`` map.
 
 The code for it could look something like this:
 
@@ -175,7 +175,7 @@ The code for it could look something like this:
 
     // Commonly used utilities and variables can be found under the "env"
     // namespace of the "converse" global.
-    const { Promise, Strophe, dayjs, sizzle, _, $build, $iq, $msg, $pres } = converse.env;
+    const { Promise, Strophe, dayjs, sizzle, $build, $iq, $msg, $pres } = converse.env;
 
 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.
@@ -263,7 +263,8 @@ For example:
 Overriding a template
 ~~~~~~~~~~~~~~~~~~~~~
 
-Converse uses various templates, loaded with lodash, to generate its HTML.
+Converse uses `lit-html <https://lit-html.polymer-project.org/guide>`_
+templates.
 
 It's not possible to override a template with the plugin's ``overrides``
 feature, instead you should configure a new path to your own template via your
@@ -278,26 +279,6 @@ For example, in your webpack config file, you could add the following to the
 
 .. code-block:: javascript
 
-    module: {
-        {
-            test: /templates\/.*\.(html|svg)$/,
-            use: [{
-                loader: 'lodash-template-webpack-loader',
-                options: {
-                    escape: /\{\{\{([\s\S]+?)\}\}\}/g,
-                    evaluate: /\{\[([\s\S]+?)\]\}/g,
-                    interpolate: /\{\{([\s\S]+?)\}\}/g,
-                    // By default, template places the values from your data in the
-                    // local scope via the with statement. However, you can specify
-                    // a single variable name with the variable setting. This can
-                    // significantly improve the speed at which a template is able
-                    // to render.
-                    variable: 'o',
-                    prependFilenameComment: __dirname
-                }
-            }]
-        }
-    },
     resolve: {
         extensions: ['.js'],
         modules: [
@@ -305,17 +286,13 @@ For example, in your webpack config file, you could add the following to the
             path.join(__dirname, 'node_modules/converse.js/src')
         ],
         alias: {
-            'templates/profile_view.html$': path.resolve(__dirname, 'templates/profile_view.html')
+            'plugins/profile/templates/profile.js$': path.resolve(__dirname, 'templates/custom-profile.js')
         }
     }
 
-
-You'll need to install ``lodash-template-webpack-loader``.
-
-Currently Converse uses a fork of `lodash-template-webpack-loader <https://github.com/jcbrand/lodash-template-webpack-loader>`_.
-
-To install it, you can add ``"lodash-template-webpack-loader": "jcbrand/lodash-template-webpack-loader"``
-to your package.json's ``devDependencies``.
+This will override the template that gets imported at the path ``plugins/profile/templates/profile.js``
+with your own template at the path ``templates/custom-profile.js`` (relative to
+your webpack config file).
 
 
 .. _`dependencies`:

文件差異過大導致無法顯示
+ 377 - 306
package-lock.json


+ 3 - 3
package.json

@@ -83,7 +83,7 @@
     "exports-loader": "^0.7.0",
     "fast-text-encoding": "^1.0.3",
     "file-loader": "^6.0.0",
-    "html-webpack-plugin": "^4.3.0",
+    "html-webpack-plugin": "^5.3.1",
     "http-server": "^0.12.3",
     "imports-loader": "^0.8.0",
     "install": "^0.13.0",
@@ -110,8 +110,8 @@
     "sinon": "^9.2.4",
     "style-loader": "^0.23.1",
     "webpack": "^5.36.1",
-    "webpack-cli": "^4.5.0",
-    "webpack-dev-server": "^3.11.2",
+    "webpack-cli": "^4.6.0",
+    "webpack-dev-server": "^4.0.0-beta.2",
     "webpack-merge": "^5.7.3"
   },
   "dependencies": {

+ 587 - 594
spec/mock.js

@@ -1,10 +1,10 @@
-/*global converse */
-
 const mock = {};
 window.mock = mock;
-let _converse, initConverse;
+let _converse;
 
-const converseLoaded = new Promise(resolve => window.addEventListener('converse-loaded', resolve));
+const converse = window.converse;
+converse.load();
+const { u, sizzle, Strophe, dayjs, $iq, $msg, $pres } = converse.env;
 
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000;
 
@@ -25,7 +25,6 @@ mock.initConverse = function (promise_names=[], settings=null, func) {
         }
         document.title = "Converse Tests";
 
-        await converseLoaded;
         await initConverse(settings);
         await Promise.all((promise_names || []).map(_converse.api.waitUntil));
         try {
@@ -38,644 +37,638 @@ mock.initConverse = function (promise_names=[], settings=null, func) {
     }
 };
 
-window.addEventListener('converse-loaded', () => {
-    const { u, sizzle, Strophe, dayjs, $iq, $msg, $pres } = converse.env;
-
-    mock.waitUntilDiscoConfirmed = async function (_converse, entity_jid, identities, features=[], items=[], type='info') {
-        const sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`;
-        const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop(), 300);
-        const stanza = $iq({
-            'type': 'result',
-            'from': entity_jid,
-            'to': 'romeo@montague.lit/orchard',
-            'id': iq.getAttribute('id'),
-        }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#'+type});
-
-        identities?.forEach(identity => stanza.c('identity', {'category': identity.category, 'type': identity.type}).up());
-        features?.forEach(feature => stanza.c('feature', {'var': feature}).up());
-        items?.forEach(item => stanza.c('item', {'jid': item}).up());
-        _converse.connection._dataRecv(mock.createRequest(stanza));
-    }
-
-    mock.createRequest = function (iq) {
-        iq = typeof iq.tree == "function" ? iq.tree() : iq;
-        var req = new Strophe.Request(iq, function() {});
-        req.getResponse = function () {
-            var env = new Strophe.Builder('env', {type: 'mock'}).tree();
-            env.appendChild(iq);
-            return env;
-        };
-        return req;
+mock.waitUntilDiscoConfirmed = async function (_converse, entity_jid, identities, features=[], items=[], type='info') {
+    const sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`;
+    const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop(), 300);
+    const stanza = $iq({
+        'type': 'result',
+        'from': entity_jid,
+        'to': 'romeo@montague.lit/orchard',
+        'id': iq.getAttribute('id'),
+    }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#'+type});
+
+    identities?.forEach(identity => stanza.c('identity', {'category': identity.category, 'type': identity.type}).up());
+    features?.forEach(feature => stanza.c('feature', {'var': feature}).up());
+    items?.forEach(item => stanza.c('item', {'jid': item}).up());
+    _converse.connection._dataRecv(mock.createRequest(stanza));
+}
+
+mock.createRequest = function (iq) {
+    iq = typeof iq.tree == "function" ? iq.tree() : iq;
+    var req = new Strophe.Request(iq, function() {});
+    req.getResponse = function () {
+        var env = new Strophe.Builder('env', {type: 'mock'}).tree();
+        env.appendChild(iq);
+        return env;
     };
+    return req;
+};
 
-    mock.closeAllChatBoxes = function (_converse) {
-        return Promise.all(_converse.chatboxviews.map(view => view.close()));
-    };
+mock.closeAllChatBoxes = function (_converse) {
+    return Promise.all(_converse.chatboxviews.map(view => view.close()));
+};
 
-    mock.toggleControlBox = function () {
-        const toggle = document.querySelector(".toggle-controlbox");
-        if (!u.isVisible(document.querySelector("#controlbox"))) {
-            if (!u.isVisible(toggle)) {
-                u.removeClass('hidden', toggle);
-            }
-            toggle.click();
+mock.toggleControlBox = function () {
+    const toggle = document.querySelector(".toggle-controlbox");
+    if (!u.isVisible(document.querySelector("#controlbox"))) {
+        if (!u.isVisible(toggle)) {
+            u.removeClass('hidden', toggle);
         }
+        toggle.click();
     }
+}
 
-    mock.openControlBox = async function (_converse) {
-        const model = await _converse.api.controlbox.open();
-        await u.waitUntil(() => model.get('connected'));
-        mock.toggleControlBox();
-        return this;
-    };
+mock.openControlBox = async function (_converse) {
+    const model = await _converse.api.controlbox.open();
+    await u.waitUntil(() => model.get('connected'));
+    mock.toggleControlBox();
+    return this;
+};
 
-    mock.closeControlBox = function () {
-        const controlbox = document.querySelector("#controlbox");
-        if (u.isVisible(controlbox)) {
-            const button = controlbox.querySelector(".close-chatbox-button");
-            (button !== null) && button.click();
-        }
-        return this;
-    };
+mock.closeControlBox = function () {
+    const controlbox = document.querySelector("#controlbox");
+    if (u.isVisible(controlbox)) {
+        const button = controlbox.querySelector(".close-chatbox-button");
+        (button !== null) && button.click();
+    }
+    return this;
+};
 
-    mock.waitUntilBookmarksReturned = async function (_converse, bookmarks=[]) {
-        await mock.waitUntilDiscoConfirmed(
-            _converse, _converse.bare_jid,
-            [{'category': 'pubsub', 'type': 'pep'}],
-            ['http://jabber.org/protocol/pubsub#publish-options']
-        );
-        const IQ_stanzas = _converse.connection.IQ_stanzas;
-        const sent_stanza = await u.waitUntil(
-            () => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop()
-        );
-        const stanza = $iq({
-            'to': _converse.connection.jid,
-            'type':'result',
-            'id':sent_stanza.getAttribute('id')
-        }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
-            .c('items', {'node': 'storage:bookmarks'})
-                .c('item', {'id': 'current'})
-                    .c('storage', {'xmlns': 'storage:bookmarks'});
-        bookmarks.forEach(bookmark => {
-            stanza.c('conference', {
-                'name': bookmark.name,
-                'autojoin': bookmark.autojoin,
-                'jid': bookmark.jid
-            }).c('nick').t(bookmark.nick).up().up()
-        });
-        _converse.connection._dataRecv(mock.createRequest(stanza));
-        await _converse.api.waitUntil('bookmarksInitialized');
-    };
+mock.waitUntilBookmarksReturned = async function (_converse, bookmarks=[]) {
+    await mock.waitUntilDiscoConfirmed(
+        _converse, _converse.bare_jid,
+        [{'category': 'pubsub', 'type': 'pep'}],
+        ['http://jabber.org/protocol/pubsub#publish-options']
+    );
+    const IQ_stanzas = _converse.connection.IQ_stanzas;
+    const sent_stanza = await u.waitUntil(
+        () => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop()
+    );
+    const stanza = $iq({
+        'to': _converse.connection.jid,
+        'type':'result',
+        'id':sent_stanza.getAttribute('id')
+    }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
+        .c('items', {'node': 'storage:bookmarks'})
+            .c('item', {'id': 'current'})
+                .c('storage', {'xmlns': 'storage:bookmarks'});
+    bookmarks.forEach(bookmark => {
+        stanza.c('conference', {
+            'name': bookmark.name,
+            'autojoin': bookmark.autojoin,
+            'jid': bookmark.jid
+        }).c('nick').t(bookmark.nick).up().up()
+    });
+    _converse.connection._dataRecv(mock.createRequest(stanza));
+    await _converse.api.waitUntil('bookmarksInitialized');
+};
 
-    mock.openChatBoxes = function (converse, amount) {
-        for (let i=0; i<amount; i++) {
-            const jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            converse.roster.get(jid).openChat();
-        }
-    };
+mock.openChatBoxes = function (converse, amount) {
+    for (let i=0; i<amount; i++) {
+        const jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        converse.roster.get(jid).openChat();
+    }
+};
 
-    mock.openChatBoxFor = async function (_converse, jid) {
-        await _converse.api.waitUntil('rosterContactsFetched');
-        _converse.roster.get(jid).openChat();
-        return u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
-    };
+mock.openChatBoxFor = async function (_converse, jid) {
+    await _converse.api.waitUntil('rosterContactsFetched');
+    _converse.roster.get(jid).openChat();
+    return u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
+};
 
-    mock.openChatRoomViaModal = async function (_converse, jid, nick='') {
-        // Opens a new chatroom
-        const model = await _converse.api.controlbox.open('controlbox');
-        await u.waitUntil(() => model.get('connected'));
-        await mock.openControlBox(_converse);
-        document.querySelector('converse-rooms-list .show-add-muc-modal').click();
-        mock.closeControlBox(_converse);
-        const modal = _converse.api.modal.get('add-chatroom-modal');
-        await u.waitUntil(() => u.isVisible(modal.el), 1500)
-        modal.el.querySelector('input[name="chatroom"]').value = jid;
-        if (nick) {
-            modal.el.querySelector('input[name="nickname"]').value = nick;
-        }
-        modal.el.querySelector('form input[type="submit"]').click();
-        await u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
-        return _converse.chatboxviews.get(jid);
-    };
+mock.openChatRoomViaModal = async function (_converse, jid, nick='') {
+    // Opens a new chatroom
+    const model = await _converse.api.controlbox.open('controlbox');
+    await u.waitUntil(() => model.get('connected'));
+    await mock.openControlBox(_converse);
+    document.querySelector('converse-rooms-list .show-add-muc-modal').click();
+    mock.closeControlBox(_converse);
+    const modal = _converse.api.modal.get('add-chatroom-modal');
+    await u.waitUntil(() => u.isVisible(modal.el), 1500)
+    modal.el.querySelector('input[name="chatroom"]').value = jid;
+    if (nick) {
+        modal.el.querySelector('input[name="nickname"]').value = nick;
+    }
+    modal.el.querySelector('form input[type="submit"]').click();
+    await u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
+    return _converse.chatboxviews.get(jid);
+};
 
-    mock.openChatRoom = function (_converse, room, server) {
-        return _converse.api.rooms.open(`${room}@${server}`);
-    };
+mock.openChatRoom = function (_converse, room, server) {
+    return _converse.api.rooms.open(`${room}@${server}`);
+};
 
-    mock.getRoomFeatures = async function (_converse, muc_jid, features=[]) {
-        const room = Strophe.getNodeFromJid(muc_jid);
-        muc_jid = muc_jid.toLowerCase();
-        const stanzas = _converse.connection.IQ_stanzas;
-        const stanza = await u.waitUntil(() => stanzas.filter(
-            iq => iq.querySelector(
-                `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
-            )).pop()
-        );
-        const features_stanza = $iq({
-            'from': muc_jid,
-            'id': stanza.getAttribute('id'),
-            'to': 'romeo@montague.lit/desktop',
-            'type': 'result'
-        }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
-            .c('identity', {
-                'category': 'conference',
-                'name': room[0].toUpperCase() + room.slice(1),
-                'type': 'text'
-            }).up();
-
-        features = features.length ? features : mock.default_muc_features;
-        features.forEach(f => features_stanza.c('feature', {'var': f}).up());
-        features_stanza.c('x', { 'xmlns':'jabber:x:data', 'type':'result'})
-            .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
-                .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up()
-            .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'})
-                .c('value').t('This is the description').up().up()
-            .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'})
-                .c('value').t(0);
-        _converse.connection._dataRecv(mock.createRequest(features_stanza));
-    };
+mock.getRoomFeatures = async function (_converse, muc_jid, features=[]) {
+    const room = Strophe.getNodeFromJid(muc_jid);
+    muc_jid = muc_jid.toLowerCase();
+    const stanzas = _converse.connection.IQ_stanzas;
+    const stanza = await u.waitUntil(() => stanzas.filter(
+        iq => iq.querySelector(
+            `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+        )).pop()
+    );
+    const features_stanza = $iq({
+        'from': muc_jid,
+        'id': stanza.getAttribute('id'),
+        'to': 'romeo@montague.lit/desktop',
+        'type': 'result'
+    }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
+        .c('identity', {
+            'category': 'conference',
+            'name': room[0].toUpperCase() + room.slice(1),
+            'type': 'text'
+        }).up();
+
+    features = features.length ? features : mock.default_muc_features;
+    features.forEach(f => features_stanza.c('feature', {'var': f}).up());
+    features_stanza.c('x', { 'xmlns':'jabber:x:data', 'type':'result'})
+        .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
+            .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up()
+        .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'})
+            .c('value').t('This is the description').up().up()
+        .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'})
+            .c('value').t(0);
+    _converse.connection._dataRecv(mock.createRequest(features_stanza));
+};
 
 
-    mock.waitForReservedNick = async function (_converse, muc_jid, nick) {
-        const stanzas = _converse.connection.IQ_stanzas;
-        const selector = `iq[to="${muc_jid.toLowerCase()}"] query[node="x-roomuser-item"]`;
-        const iq = await u.waitUntil(() => stanzas.filter(s => sizzle(selector, s).length).pop());
-
-        // We remove the stanza, otherwise we might get stale stanzas returned in our filter above.
-        stanzas.splice(stanzas.indexOf(iq), 1)
-
-        // The XMPP server returns the reserved nick for this user.
-        const IQ_id = iq.getAttribute('id');
-        const stanza = $iq({
-            'type': 'result',
-            'id': IQ_id,
-            'from': muc_jid,
-            'to': _converse.connection.jid
-        }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info', 'node': 'x-roomuser-item'});
-        if (nick) {
-            stanza.c('identity', {'category': 'conference', 'name': nick, 'type': 'text'});
-        }
-        _converse.connection._dataRecv(mock.createRequest(stanza));
-        if (nick) {
-            return u.waitUntil(() => nick);
-        }
-    };
+mock.waitForReservedNick = async function (_converse, muc_jid, nick) {
+    const stanzas = _converse.connection.IQ_stanzas;
+    const selector = `iq[to="${muc_jid.toLowerCase()}"] query[node="x-roomuser-item"]`;
+    const iq = await u.waitUntil(() => stanzas.filter(s => sizzle(selector, s).length).pop());
+
+    // We remove the stanza, otherwise we might get stale stanzas returned in our filter above.
+    stanzas.splice(stanzas.indexOf(iq), 1)
+
+    // The XMPP server returns the reserved nick for this user.
+    const IQ_id = iq.getAttribute('id');
+    const stanza = $iq({
+        'type': 'result',
+        'id': IQ_id,
+        'from': muc_jid,
+        'to': _converse.connection.jid
+    }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info', 'node': 'x-roomuser-item'});
+    if (nick) {
+        stanza.c('identity', {'category': 'conference', 'name': nick, 'type': 'text'});
+    }
+    _converse.connection._dataRecv(mock.createRequest(stanza));
+    if (nick) {
+        return u.waitUntil(() => nick);
+    }
+};
 
 
-    mock.returnMemberLists = async function (_converse, muc_jid, members=[], affiliations=['member', 'owner', 'admin']) {
-        if (affiliations.length === 0) {
-            return;
-        }
-        const stanzas = _converse.connection.IQ_stanzas;
-
-        if (affiliations.includes('member')) {
-            const member_IQ = await u.waitUntil(() =>
-                stanzas.filter(s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="member"]`, s).length
-            ).pop());
-            const member_list_stanza = $iq({
-                    'from': 'coven@chat.shakespeare.lit',
-                    'id': member_IQ.getAttribute('id'),
-                    'to': 'romeo@montague.lit/orchard',
-                    'type': 'result'
-                }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN});
-            members.filter(m => m.affiliation === 'member').forEach(m => {
-                member_list_stanza.c('item', {
-                    'affiliation': m.affiliation,
-                    'jid': m.jid,
-                    'nick': m.nick
-                });
+mock.returnMemberLists = async function (_converse, muc_jid, members=[], affiliations=['member', 'owner', 'admin']) {
+    if (affiliations.length === 0) {
+        return;
+    }
+    const stanzas = _converse.connection.IQ_stanzas;
+
+    if (affiliations.includes('member')) {
+        const member_IQ = await u.waitUntil(() =>
+            stanzas.filter(s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="member"]`, s).length
+        ).pop());
+        const member_list_stanza = $iq({
+                'from': 'coven@chat.shakespeare.lit',
+                'id': member_IQ.getAttribute('id'),
+                'to': 'romeo@montague.lit/orchard',
+                'type': 'result'
+            }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN});
+        members.filter(m => m.affiliation === 'member').forEach(m => {
+            member_list_stanza.c('item', {
+                'affiliation': m.affiliation,
+                'jid': m.jid,
+                'nick': m.nick
             });
-            _converse.connection._dataRecv(mock.createRequest(member_list_stanza));
-        }
+        });
+        _converse.connection._dataRecv(mock.createRequest(member_list_stanza));
+    }
 
-        if (affiliations.includes('admin')) {
-            const admin_IQ = await u.waitUntil(() => stanzas.filter(
-                s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="admin"]`, s).length
-            ).pop());
-            const admin_list_stanza = $iq({
-                    'from': 'coven@chat.shakespeare.lit',
-                    'id': admin_IQ.getAttribute('id'),
-                    'to': 'romeo@montague.lit/orchard',
-                    'type': 'result'
-                }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN});
-            members.filter(m => m.affiliation === 'admin').forEach(m => {
-                admin_list_stanza.c('item', {
-                    'affiliation': m.affiliation,
-                    'jid': m.jid,
-                    'nick': m.nick
-                });
+    if (affiliations.includes('admin')) {
+        const admin_IQ = await u.waitUntil(() => stanzas.filter(
+            s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="admin"]`, s).length
+        ).pop());
+        const admin_list_stanza = $iq({
+                'from': 'coven@chat.shakespeare.lit',
+                'id': admin_IQ.getAttribute('id'),
+                'to': 'romeo@montague.lit/orchard',
+                'type': 'result'
+            }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN});
+        members.filter(m => m.affiliation === 'admin').forEach(m => {
+            admin_list_stanza.c('item', {
+                'affiliation': m.affiliation,
+                'jid': m.jid,
+                'nick': m.nick
             });
-            _converse.connection._dataRecv(mock.createRequest(admin_list_stanza));
-        }
+        });
+        _converse.connection._dataRecv(mock.createRequest(admin_list_stanza));
+    }
 
-        if (affiliations.includes('owner')) {
-            const owner_IQ = await u.waitUntil(() => stanzas.filter(
-                s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="owner"]`, s).length
-            ).pop());
-            const owner_list_stanza = $iq({
-                    'from': 'coven@chat.shakespeare.lit',
-                    'id': owner_IQ.getAttribute('id'),
-                    'to': 'romeo@montague.lit/orchard',
-                    'type': 'result'
-                }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN});
-            members.filter(m => m.affiliation === 'owner').forEach(m => {
-                owner_list_stanza.c('item', {
-                    'affiliation': m.affiliation,
-                    'jid': m.jid,
-                    'nick': m.nick
-                });
+    if (affiliations.includes('owner')) {
+        const owner_IQ = await u.waitUntil(() => stanzas.filter(
+            s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="owner"]`, s).length
+        ).pop());
+        const owner_list_stanza = $iq({
+                'from': 'coven@chat.shakespeare.lit',
+                'id': owner_IQ.getAttribute('id'),
+                'to': 'romeo@montague.lit/orchard',
+                'type': 'result'
+            }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN});
+        members.filter(m => m.affiliation === 'owner').forEach(m => {
+            owner_list_stanza.c('item', {
+                'affiliation': m.affiliation,
+                'jid': m.jid,
+                'nick': m.nick
             });
-            _converse.connection._dataRecv(mock.createRequest(owner_list_stanza));
-        }
-        return new Promise(resolve => _converse.api.listen.on('membersFetched', resolve));
-    };
+        });
+        _converse.connection._dataRecv(mock.createRequest(owner_list_stanza));
+    }
+    return new Promise(resolve => _converse.api.listen.on('membersFetched', resolve));
+};
 
-    mock.receiveOwnMUCPresence = async function (_converse, muc_jid, nick) {
-        const sent_stanzas = _converse.connection.sent_stanzas;
-        await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop());
-        const presence = $pres({
-                to: _converse.connection.jid,
-                from: `${muc_jid}/${nick}`,
-                id: u.getUniqueId()
-        }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
-            .c('item').attrs({
-                affiliation: 'owner',
-                jid: _converse.bare_jid,
-                role: 'moderator'
-            }).up()
-            .c('status').attrs({code:'110'});
-        _converse.connection._dataRecv(mock.createRequest(presence));
-    };
+mock.receiveOwnMUCPresence = async function (_converse, muc_jid, nick) {
+    const sent_stanzas = _converse.connection.sent_stanzas;
+    await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop());
+    const presence = $pres({
+            to: _converse.connection.jid,
+            from: `${muc_jid}/${nick}`,
+            id: u.getUniqueId()
+    }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
+        .c('item').attrs({
+            affiliation: 'owner',
+            jid: _converse.bare_jid,
+            role: 'moderator'
+        }).up()
+        .c('status').attrs({code:'110'});
+    _converse.connection._dataRecv(mock.createRequest(presence));
+};
 
 
-    mock.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[], members=[], force_open=true, settings={}) {
-        const { api } = _converse;
-        muc_jid = muc_jid.toLowerCase();
-        const room_creation_promise = api.rooms.open(muc_jid, settings, force_open);
-        await mock.getRoomFeatures(_converse, muc_jid, features);
-        await mock.waitForReservedNick(_converse, muc_jid, nick);
-        // The user has just entered the room (because join was called)
-        // and receives their own presence from the server.
-        // See example 24: https://xmpp.org/extensions/xep-0045.html#enter-pres
-        await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
-
-        await room_creation_promise;
-        const model = _converse.chatboxes.get(muc_jid);
-        await u.waitUntil(() => (model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
-
-        const affs = _converse.muc_fetch_members;
-        const all_affiliations = Array.isArray(affs) ? affs :  (affs ? ['member', 'admin', 'owner'] : []);
-        await mock.returnMemberLists(_converse, muc_jid, members, all_affiliations);
-        return model.messages.fetched;
-    };
+mock.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[], members=[], force_open=true, settings={}) {
+    const { api } = _converse;
+    muc_jid = muc_jid.toLowerCase();
+    const room_creation_promise = api.rooms.open(muc_jid, settings, force_open);
+    await mock.getRoomFeatures(_converse, muc_jid, features);
+    await mock.waitForReservedNick(_converse, muc_jid, nick);
+    // The user has just entered the room (because join was called)
+    // and receives their own presence from the server.
+    // See example 24: https://xmpp.org/extensions/xep-0045.html#enter-pres
+    await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
+
+    await room_creation_promise;
+    const model = _converse.chatboxes.get(muc_jid);
+    await u.waitUntil(() => (model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
+
+    const affs = _converse.muc_fetch_members;
+    const all_affiliations = Array.isArray(affs) ? affs :  (affs ? ['member', 'admin', 'owner'] : []);
+    await mock.returnMemberLists(_converse, muc_jid, members, all_affiliations);
+    return model.messages.fetched;
+};
 
-    mock.createContact = async function (_converse, name, ask, requesting, subscription) {
-        const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
-        if (_converse.roster.get(jid)) {
-            return Promise.resolve();
-        }
-        const contact = await new Promise((success, error) => {
-            _converse.roster.create({
-                'ask': ask,
-                'fullname': name,
-                'jid': jid,
-                'requesting': requesting,
-                'subscription': subscription
-            }, {success, error});
-        });
-        return contact;
-    };
+mock.createContact = async function (_converse, name, ask, requesting, subscription) {
+    const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
+    if (_converse.roster.get(jid)) {
+        return Promise.resolve();
+    }
+    const contact = await new Promise((success, error) => {
+        _converse.roster.create({
+            'ask': ask,
+            'fullname': name,
+            'jid': jid,
+            'requesting': requesting,
+            'subscription': subscription
+        }, {success, error});
+    });
+    return contact;
+};
 
-    mock.createContacts = async function (_converse, type, length) {
-        /* Create current (as opposed to requesting or pending) contacts
-            * for the user's roster.
-            *
-            * These contacts are not grouped. See below.
-            */
-        await _converse.api.waitUntil('rosterContactsFetched');
-        let names, subscription, requesting, ask;
-        if (type === 'requesting') {
-            names = mock.req_names;
-            subscription = 'none';
-            requesting = true;
-            ask = null;
-        } else if (type === 'pending') {
-            names = mock.pend_names;
-            subscription = 'none';
-            requesting = false;
-            ask = 'subscribe';
-        } else if (type === 'current') {
-            names = mock.cur_names;
-            subscription = 'both';
-            requesting = false;
-            ask = null;
-        } else if (type === 'all') {
-            await this.createContacts(_converse, 'current');
-            await this.createContacts(_converse, 'requesting')
-            await this.createContacts(_converse, 'pending');
-            return this;
-        } else {
-            throw Error("Need to specify the type of contact to create");
-        }
-        const promises = names.slice(0, length).map(n => this.createContact(_converse, n, ask, requesting, subscription));
-        await Promise.all(promises);
-    };
+mock.createContacts = async function (_converse, type, length) {
+    /* Create current (as opposed to requesting or pending) contacts
+        * for the user's roster.
+        *
+        * These contacts are not grouped. See below.
+        */
+    await _converse.api.waitUntil('rosterContactsFetched');
+    let names, subscription, requesting, ask;
+    if (type === 'requesting') {
+        names = mock.req_names;
+        subscription = 'none';
+        requesting = true;
+        ask = null;
+    } else if (type === 'pending') {
+        names = mock.pend_names;
+        subscription = 'none';
+        requesting = false;
+        ask = 'subscribe';
+    } else if (type === 'current') {
+        names = mock.cur_names;
+        subscription = 'both';
+        requesting = false;
+        ask = null;
+    } else if (type === 'all') {
+        await this.createContacts(_converse, 'current');
+        await this.createContacts(_converse, 'requesting')
+        await this.createContacts(_converse, 'pending');
+        return this;
+    } else {
+        throw Error("Need to specify the type of contact to create");
+    }
+    const promises = names.slice(0, length).map(n => this.createContact(_converse, n, ask, requesting, subscription));
+    await Promise.all(promises);
+};
 
-    mock.waitForRoster = async function (_converse, type='current', length=-1, include_nick=true, grouped=true) {
-        const s = `iq[type="get"] query[xmlns="${Strophe.NS.ROSTER}"]`;
-        const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(iq => sizzle(s, iq).length).pop());
+mock.waitForRoster = async function (_converse, type='current', length=-1, include_nick=true, grouped=true) {
+    const s = `iq[type="get"] query[xmlns="${Strophe.NS.ROSTER}"]`;
+    const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(iq => sizzle(s, iq).length).pop());
 
-        const result = $iq({
-            'to': _converse.connection.jid,
-            'type': 'result',
-            'id': iq.getAttribute('id')
-        }).c('query', {
-            'xmlns': 'jabber:iq:roster'
-        });
-        if (type === 'pending' || type === 'all') {
-            const pend_names = (length > -1) ? mock.pend_names.slice(0, length) : mock.pend_names;
-            pend_names.map(name =>
-                result.c('item', {
-                    jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
-                    name: include_nick ? name : undefined,
-                    subscription: 'none',
-                    ask: 'subscribe'
-                }).up()
-            );
-        }
-        if (type === 'current' || type === 'all') {
-            const cur_names = Object.keys(mock.current_contacts_map);
-            const names = (length > -1) ? cur_names.slice(0, length) : cur_names;
-            names.forEach(name => {
-                result.c('item', {
-                    jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
-                    name: include_nick ? name : undefined,
-                    subscription: 'both',
-                    ask: null
-                });
-                if (grouped) {
-                    mock.current_contacts_map[name].forEach(g => result.c('group').t(g).up());
-                }
-                result.up();
+    const result = $iq({
+        'to': _converse.connection.jid,
+        'type': 'result',
+        'id': iq.getAttribute('id')
+    }).c('query', {
+        'xmlns': 'jabber:iq:roster'
+    });
+    if (type === 'pending' || type === 'all') {
+        const pend_names = (length > -1) ? mock.pend_names.slice(0, length) : mock.pend_names;
+        pend_names.map(name =>
+            result.c('item', {
+                jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
+                name: include_nick ? name : undefined,
+                subscription: 'none',
+                ask: 'subscribe'
+            }).up()
+        );
+    }
+    if (type === 'current' || type === 'all') {
+        const cur_names = Object.keys(mock.current_contacts_map);
+        const names = (length > -1) ? cur_names.slice(0, length) : cur_names;
+        names.forEach(name => {
+            result.c('item', {
+                jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
+                name: include_nick ? name : undefined,
+                subscription: 'both',
+                ask: null
             });
-        }
-        _converse.connection._dataRecv(mock.createRequest(result));
-        await _converse.api.waitUntil('rosterContactsFetched');
-    };
-
-    mock.createChatMessage = function (_converse, sender_jid, message) {
-        return $msg({
-                    from: sender_jid,
-                    to: _converse.connection.jid,
-                    type: 'chat',
-                    id: (new Date()).getTime()
-                })
-                .c('body').t(message).up()
-                .c('markable', {'xmlns': Strophe.NS.MARKERS}).up()
-                .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+            if (grouped) {
+                mock.current_contacts_map[name].forEach(g => result.c('group').t(g).up());
+            }
+            result.up();
+        });
     }
+    _converse.connection._dataRecv(mock.createRequest(result));
+    await _converse.api.waitUntil('rosterContactsFetched');
+};
 
-    mock.sendMessage = async function (view, message) {
-        const promise = new Promise(resolve => view.model.messages.once('rendered', resolve));
-        const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
-        textarea.value = message;
-        const bottom_panel = view.querySelector('converse-chat-bottom-panel') || view.querySelector('converse-muc-bottom-panel');
-        bottom_panel.onKeyDown({
-            target: view.querySelector('textarea.chat-textarea'),
-            preventDefault: () => {},
-            keyCode: 13
-        });
-        return promise;
-    };
+mock.createChatMessage = function (_converse, sender_jid, message) {
+    return $msg({
+                from: sender_jid,
+                to: _converse.connection.jid,
+                type: 'chat',
+                id: (new Date()).getTime()
+            })
+            .c('body').t(message).up()
+            .c('markable', {'xmlns': Strophe.NS.MARKERS}).up()
+            .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+}
+
+mock.sendMessage = async function (view, message) {
+    const promise = new Promise(resolve => view.model.messages.once('rendered', resolve));
+    const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
+    textarea.value = message;
+    const bottom_panel = view.querySelector('converse-chat-bottom-panel') || view.querySelector('converse-muc-bottom-panel');
+    bottom_panel.onKeyDown({
+        target: view.querySelector('textarea.chat-textarea'),
+        preventDefault: () => {},
+        keyCode: 13
+    });
+    return promise;
+};
 
 
-    window.libsignal = {
-        'SignalProtocolAddress': function (name, device_id) {
-            this.name = name;
-            this.deviceId = device_id;
-        },
-        'SessionCipher': function (storage, remote_address) {
-            this.remoteAddress = remote_address;
-            this.storage = storage;
-            this.encrypt = () => Promise.resolve({
-                'type': 1,
-                'body': 'c1ph3R73X7',
-                'registrationId': '1337'
+window.libsignal = {
+    'SignalProtocolAddress': function (name, device_id) {
+        this.name = name;
+        this.deviceId = device_id;
+    },
+    'SessionCipher': function (storage, remote_address) {
+        this.remoteAddress = remote_address;
+        this.storage = storage;
+        this.encrypt = () => Promise.resolve({
+            'type': 1,
+            'body': 'c1ph3R73X7',
+            'registrationId': '1337'
+        });
+        this.decryptPreKeyWhisperMessage = (key_and_tag) => {
+            return Promise.resolve(key_and_tag);
+        };
+        this.decryptWhisperMessage = (key_and_tag) => {
+            return Promise.resolve(key_and_tag);
+        }
+    },
+    'SessionBuilder': function (storage, remote_address) { // eslint-disable-line no-unused-vars
+        this.processPreKey = function () {
+            return Promise.resolve();
+        }
+    },
+    'KeyHelper': {
+        'generateIdentityKeyPair': function () {
+            return Promise.resolve({
+                'pubKey': new TextEncoder('utf-8').encode('1234'),
+                'privKey': new TextEncoder('utf-8').encode('4321')
             });
-            this.decryptPreKeyWhisperMessage = (key_and_tag) => {
-                return Promise.resolve(key_and_tag);
-            };
-            this.decryptWhisperMessage = (key_and_tag) => {
-                return Promise.resolve(key_and_tag);
-            }
         },
-        'SessionBuilder': function (storage, remote_address) { // eslint-disable-line no-unused-vars
-            this.processPreKey = function () {
-                return Promise.resolve();
-            }
+        'generateRegistrationId': function () {
+            return '123456789';
         },
-        'KeyHelper': {
-            'generateIdentityKeyPair': function () {
-                return Promise.resolve({
+        'generatePreKey': function (keyid) {
+            return Promise.resolve({
+                'keyId': keyid,
+                'keyPair': {
                     'pubKey': new TextEncoder('utf-8').encode('1234'),
                     'privKey': new TextEncoder('utf-8').encode('4321')
-                });
-            },
-            'generateRegistrationId': function () {
-                return '123456789';
-            },
-            'generatePreKey': function (keyid) {
-                return Promise.resolve({
-                    'keyId': keyid,
-                    'keyPair': {
-                        'pubKey': new TextEncoder('utf-8').encode('1234'),
-                        'privKey': new TextEncoder('utf-8').encode('4321')
-                    }
-                });
-            },
-            'generateSignedPreKey': function (identity_keypair, keyid) {
-                return Promise.resolve({
-                    'signature': new TextEncoder('utf-8').encode('11112222333344445555'),
-                    'keyId': keyid,
-                    'keyPair': {
-                        'pubKey': new TextEncoder('utf-8').encode('1234'),
-                        'privKey': new TextEncoder('utf-8').encode('4321')
-                    }
-                });
-            }
+                }
+            });
+        },
+        'generateSignedPreKey': function (identity_keypair, keyid) {
+            return Promise.resolve({
+                'signature': new TextEncoder('utf-8').encode('11112222333344445555'),
+                'keyId': keyid,
+                'keyPair': {
+                    'pubKey': new TextEncoder('utf-8').encode('1234'),
+                    'privKey': new TextEncoder('utf-8').encode('4321')
+                }
+            });
         }
-    };
+    }
+};
 
-    mock.default_muc_features = [
-        'http://jabber.org/protocol/muc',
-        'jabber:iq:register',
-        Strophe.NS.SID,
-        Strophe.NS.MAM,
-        'muc_passwordprotected',
-        'muc_hidden',
-        'muc_temporary',
-        'muc_open',
-        'muc_unmoderated',
-        'muc_anonymous'
-    ];
-
-    mock.view_mode = 'overlayed';
-
-    // Names from http://www.fakenamegenerator.com/
-    mock.req_names = [
-        'Escalus, prince of Verona', 'The Nurse', 'Paris'
-    ];
-    mock.pend_names = [
-        'Lord Capulet', 'Guard', 'Servant'
-    ];
-    mock.current_contacts_map = {
-        'Mercutio': ['Colleagues', 'friends & acquaintences'],
-        'Juliet Capulet': ['friends & acquaintences'],
-        'Lady Montague': ['Colleagues', 'Family'],
-        'Lord Montague': ['Family'],
-        'Friar Laurence': ['friends & acquaintences'],
-        'Tybalt': ['friends & acquaintences'],
-        'Lady Capulet': ['ænemies'],
-        'Benviolo': ['friends & acquaintences'],
-        'Balthasar': ['Colleagues'],
-        'Peter': ['Colleagues'],
-        'Abram': ['Colleagues'],
-        'Sampson': ['Colleagues'],
-        'Gregory': ['friends & acquaintences'],
-        'Potpan': [],
-        'Friar John': []
-    };
+mock.default_muc_features = [
+    'http://jabber.org/protocol/muc',
+    'jabber:iq:register',
+    Strophe.NS.SID,
+    Strophe.NS.MAM,
+    'muc_passwordprotected',
+    'muc_hidden',
+    'muc_temporary',
+    'muc_open',
+    'muc_unmoderated',
+    'muc_anonymous'
+];
+
+mock.view_mode = 'overlayed';
+
+// Names from http://www.fakenamegenerator.com/
+mock.req_names = [
+    'Escalus, prince of Verona', 'The Nurse', 'Paris'
+];
+mock.pend_names = [
+    'Lord Capulet', 'Guard', 'Servant'
+];
+mock.current_contacts_map = {
+    'Mercutio': ['Colleagues', 'friends & acquaintences'],
+    'Juliet Capulet': ['friends & acquaintences'],
+    'Lady Montague': ['Colleagues', 'Family'],
+    'Lord Montague': ['Family'],
+    'Friar Laurence': ['friends & acquaintences'],
+    'Tybalt': ['friends & acquaintences'],
+    'Lady Capulet': ['ænemies'],
+    'Benviolo': ['friends & acquaintences'],
+    'Balthasar': ['Colleagues'],
+    'Peter': ['Colleagues'],
+    'Abram': ['Colleagues'],
+    'Sampson': ['Colleagues'],
+    'Gregory': ['friends & acquaintences'],
+    'Potpan': [],
+    'Friar John': []
+};
 
-    const map = mock.current_contacts_map;
-    const groups_map = {};
-    Object.keys(map).forEach(k => {
-        const groups = map[k].length ? map[k] : ["Ungrouped"];
-        Object.values(groups).forEach(g => {
-            groups_map[g] = groups_map[g] ? [...groups_map[g], k] : [k]
-        });
+const map = mock.current_contacts_map;
+const groups_map = {};
+Object.keys(map).forEach(k => {
+    const groups = map[k].length ? map[k] : ["Ungrouped"];
+    Object.values(groups).forEach(g => {
+        groups_map[g] = groups_map[g] ? [...groups_map[g], k] : [k]
     });
-    mock.groups_map = groups_map;
+});
+mock.groups_map = groups_map;
 
-    mock.cur_names = Object.keys(mock.current_contacts_map);
-    mock.num_contacts = mock.req_names.length + mock.pend_names.length + mock.cur_names.length;
+mock.cur_names = Object.keys(mock.current_contacts_map);
+mock.num_contacts = mock.req_names.length + mock.pend_names.length + mock.cur_names.length;
 
-    mock.groups = {
-        'colleagues': 3,
-        'friends & acquaintences': 3,
-        'Family': 4,
-        'ænemies': 3,
-        'Ungrouped': 2
-    };
+mock.groups = {
+    'colleagues': 3,
+    'friends & acquaintences': 3,
+    'Family': 4,
+    'ænemies': 3,
+    'Ungrouped': 2
+};
 
-    mock.chatroom_names = [
-        'Dyon van de Wege',
-        'Thomas Kalb',
-        'Dirk Theissen',
-        'Felix Hofmann',
-        'Ka Lek',
-        'Anne Ebersbacher'
-    ];
-    // TODO: need to also test other roles and affiliations
-    mock.chatroom_roles = {
-        'Anne Ebersbacher': { affiliation: "owner", role: "moderator" },
-        'Dirk Theissen': { affiliation: "admin", role: "moderator" },
-        'Dyon van de Wege': { affiliation: "member", role: "occupant" },
-        'Felix Hofmann': { affiliation: "member", role: "occupant" },
-        'Ka Lek': { affiliation: "member", role: "occupant" },
-        'Thomas Kalb': { affiliation: "member", role: "occupant" }
-    };
+mock.chatroom_names = [
+    'Dyon van de Wege',
+    'Thomas Kalb',
+    'Dirk Theissen',
+    'Felix Hofmann',
+    'Ka Lek',
+    'Anne Ebersbacher'
+];
+// TODO: need to also test other roles and affiliations
+mock.chatroom_roles = {
+    'Anne Ebersbacher': { affiliation: "owner", role: "moderator" },
+    'Dirk Theissen': { affiliation: "admin", role: "moderator" },
+    'Dyon van de Wege': { affiliation: "member", role: "occupant" },
+    'Felix Hofmann': { affiliation: "member", role: "occupant" },
+    'Ka Lek': { affiliation: "member", role: "occupant" },
+    'Thomas Kalb': { affiliation: "member", role: "occupant" }
+};
 
-    mock.event = {
-        'preventDefault': function () {}
-    };
+mock.event = {
+    'preventDefault': function () {}
+};
 
-    function clearIndexedDB () {
-        const promise = u.getOpenPromise();
-        const db_request = window.indexedDB.open("converse-test-persistent");
-        db_request.onsuccess = function () {
-            const db = db_request.result;
-            const bare_jid = "romeo@montague.lit";
-            let store;
-            try {
-                store= db.transaction([bare_jid], "readwrite").objectStore(bare_jid);
-            } catch (e) {
-                return promise.resolve();
-            }
-            const request = store.clear();
-            request.onsuccess = promise.resolve();
-            request.onerror = promise.resolve();
-        };
-        db_request.onerror = function (ev) {
-            return promise.reject(ev.target.error);
+function clearIndexedDB () {
+    const promise = u.getOpenPromise();
+    const db_request = window.indexedDB.open("converse-test-persistent");
+    db_request.onsuccess = function () {
+        const db = db_request.result;
+        const bare_jid = "romeo@montague.lit";
+        let store;
+        try {
+            store= db.transaction([bare_jid], "readwrite").objectStore(bare_jid);
+        } catch (e) {
+            return promise.resolve();
         }
-        return promise;
-    }
-
-    function clearStores () {
-        [localStorage, sessionStorage].forEach(
-            s => Object.keys(s).forEach(k => k.match(/^converse-test-/) && s.removeItem(k))
-        );
-        const cache_key = `converse.room-bookmarksromeo@montague.lit`;
-        window.sessionStorage.removeItem(cache_key+'fetched');
+        const request = store.clear();
+        request.onsuccess = promise.resolve();
+        request.onerror = promise.resolve();
+    };
+    db_request.onerror = function (ev) {
+        return promise.reject(ev.target.error);
     }
-
-    initConverse = async (settings) => {
-        clearStores();
-        await clearIndexedDB();
-
-        _converse = await converse.initialize(Object.assign({
-            'animate': false,
-            'auto_subscribe': false,
-            'bosh_service_url': 'montague.lit/http-bind',
-            'discover_connection_methods': false,
-            'enable_smacks': false,
-            'i18n': 'en',
-            // 'persistent_store': 'IndexedDB',
-            'loglevel': 'warn',
-            'no_trimming': true,
-            'play_sounds': false,
-            'use_emojione': false,
-            'view_mode': mock.view_mode
-        }, settings || {}));
-
-        window._converse = _converse;
-
-        _converse.api.vcard.get = function (model, force) {
-            let jid;
-            if (typeof model === 'string' || model instanceof String) {
-                jid = model;
-            } else if (!model.get('vcard_updated') || force) {
-                jid = model.get('jid') || model.get('muc_jid');
-            }
-            let fullname;
-            if (!jid || jid == 'romeo@montague.lit') {
-                jid = 'romeo@montague.lit';
-                fullname = 'Romeo Montague' ;
-            } else {
-                const name = jid.split('@')[0].replace(/\./g, ' ').split(' ');
-                const last = name.length-1;
-                name[0] =  name[0].charAt(0).toUpperCase()+name[0].slice(1);
-                name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1);
-                fullname = name.join(' ');
-            }
-            const vcard = $iq().c('vCard').c('FN').t(fullname).nodeTree;
-            return {
-                'stanza': vcard,
-                'fullname': vcard.querySelector('FN')?.textContent,
-                'image': vcard.querySelector('PHOTO BINVAL')?.textContent,
-                'image_type': vcard.querySelector('PHOTO TYPE')?.textContent,
-                'url': vcard.querySelector('URL')?.textContent,
-                'vcard_updated': dayjs().format(),
-                'vcard_error': undefined
-            };
-        };
-        if (settings?.auto_login !== false) {
-            _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
-            await _converse.api.waitUntil('afterResourceBinding');
+    return promise;
+}
+
+function clearStores () {
+    [localStorage, sessionStorage].forEach(
+        s => Object.keys(s).forEach(k => k.match(/^converse-test-/) && s.removeItem(k))
+    );
+    const cache_key = `converse.room-bookmarksromeo@montague.lit`;
+    window.sessionStorage.removeItem(cache_key+'fetched');
+}
+
+const initConverse = async (settings) => {
+    clearStores();
+    await clearIndexedDB();
+
+    _converse = await converse.initialize(Object.assign({
+        'animate': false,
+        'auto_subscribe': false,
+        'bosh_service_url': 'montague.lit/http-bind',
+        'discover_connection_methods': false,
+        'enable_smacks': false,
+        'i18n': 'en',
+        // 'persistent_store': 'IndexedDB',
+        'loglevel': 'warn',
+        'no_trimming': true,
+        'play_sounds': false,
+        'use_emojione': false,
+        'view_mode': mock.view_mode
+    }, settings || {}));
+
+    window._converse = _converse;
+
+    _converse.api.vcard.get = function (model, force) {
+        let jid;
+        if (typeof model === 'string' || model instanceof String) {
+            jid = model;
+        } else if (!model.get('vcard_updated') || force) {
+            jid = model.get('jid') || model.get('muc_jid');
+        }
+        let fullname;
+        if (!jid || jid == 'romeo@montague.lit') {
+            jid = 'romeo@montague.lit';
+            fullname = 'Romeo Montague' ;
+        } else {
+            const name = jid.split('@')[0].replace(/\./g, ' ').split(' ');
+            const last = name.length-1;
+            name[0] =  name[0].charAt(0).toUpperCase()+name[0].slice(1);
+            name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1);
+            fullname = name.join(' ');
         }
-        window.converse_disable_effects = true;
-        return _converse;
+        const vcard = $iq().c('vCard').c('FN').t(fullname).nodeTree;
+        return {
+            'stanza': vcard,
+            'fullname': vcard.querySelector('FN')?.textContent,
+            'image': vcard.querySelector('PHOTO BINVAL')?.textContent,
+            'image_type': vcard.querySelector('PHOTO TYPE')?.textContent,
+            'url': vcard.querySelector('URL')?.textContent,
+            'vcard_updated': dayjs().format(),
+            'vcard_error': undefined
+        };
+    };
+    if (settings?.auto_login !== false) {
+        _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
+        await _converse.api.waitUntil('afterResourceBinding');
     }
-});
-
-converse.load();
+    window.converse_disable_effects = true;
+    return _converse;
+}

+ 11 - 0
src/entry.js

@@ -64,4 +64,15 @@ const converse = {
 }
 
 window.converse = converse;
+
+/**
+ * Once Converse.js has loaded, it'll dispatch a custom event with the name `converse-loaded`.
+ * You can listen for this event in order to be informed as soon as converse.js has been
+ * loaded and parsed, which would mean it's safe to call `converse.initialize`.
+ * @event converse-loaded
+ * @example window.addEventListener('converse-loaded', () => converse.initialize());
+ */
+const ev = new CustomEvent('converse-loaded', {'detail': { converse }});
+window.dispatchEvent(ev);
+
 export default converse;

+ 0 - 10
src/headless/core.js

@@ -1419,13 +1419,3 @@ Object.assign(converse, {
         u,
     }
 });
-
-/**
- * Once Converse.js has loaded, it'll dispatch a custom event with the name `converse-loaded`.
- * You can listen for this event in order to be informed as soon as converse.js has been
- * loaded and parsed, which would mean it's safe to call `converse.initialize`.
- * @event converse-loaded
- * @example window.addEventListener('converse-loaded', () => converse.initialize());
- */
-const ev = new CustomEvent('converse-loaded', {'detail': { converse }});
-window.dispatchEvent(ev);

+ 29 - 28
webpack.html

@@ -9,37 +9,38 @@
     <script src="3rdparty/libsignal-protocol.js"></script>
     <link rel="manifest" href="./manifest.json">
     <link rel="shortcut icon" type="image/ico" href="favicon.ico"/>
-    <script src="https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js"></script>
 </head>
 <body class="reset"></body>
 <script>
-    converse.plugins.add('converse-debug', {
-        initialize () {
-            const { _converse } = this;
-            window._converse = _converse;
-        }
-    });
-    converse.initialize({
-        muc_subscribe_to_rai: true,
-        theme: 'concord',
-        show_send_button: true,
-        auto_away: 300,
-        message_limit: 300,
-        auto_register_muc_nickname: true,
-        loglevel: 'debug',
-        modtools_disable_assign: ['owner', 'moderator', 'participant', 'visitor'],
-        modtools_disable_query: ['moderator', 'participant', 'visitor'],
-        enable_smacks: true,
-        // connection_options: { 'worker': '/dist/shared-connection-worker.js' },
-        persistent_store: 'IndexedDB',
-        message_archiving: 'always',
-        muc_domain: 'conference.chat.example.org',
-        muc_respect_autojoin: true,
-        view_mode: 'fullscreen',
-        websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
-        // bosh_service_url: 'http://chat.example.org:5280/http-bind',
-        muc_show_logs_before_join: true,
-        whitelisted_plugins: ['converse-debug', 'converse-batched-probe'],
+    window.addEventListener('converse-loaded', () => {
+        converse.plugins.add('converse-debug', {
+            initialize () {
+                const { _converse } = this;
+                window._converse = _converse;
+            }
+        });
+        converse.initialize({
+            muc_subscribe_to_rai: true,
+            theme: 'concord',
+            show_send_button: true,
+            auto_away: 300,
+            message_limit: 300,
+            auto_register_muc_nickname: true,
+            loglevel: 'debug',
+            modtools_disable_assign: ['owner', 'moderator', 'participant', 'visitor'],
+            modtools_disable_query: ['moderator', 'participant', 'visitor'],
+            enable_smacks: true,
+            // connection_options: { 'worker': '/dist/shared-connection-worker.js' },
+            persistent_store: 'IndexedDB',
+            message_archiving: 'always',
+            muc_domain: 'conference.chat.example.org',
+            muc_respect_autojoin: true,
+            view_mode: 'fullscreen',
+            websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
+            // bosh_service_url: 'http://chat.example.org:5280/http-bind',
+            muc_show_logs_before_join: true,
+            whitelisted_plugins: ['converse-debug', 'converse-batched-probe'],
+        });
     });
 </script>
 </html>

+ 1 - 1
webpack.prod.js

@@ -5,7 +5,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 const common = require("./webpack.common.js");
 const path = require('path');
 const webpack = require('webpack');
-const { merge}  = require("webpack-merge");
+const { merge }  = require("webpack-merge");
 
 const plugins = [
     new MiniCssExtractPlugin({filename: '../dist/converse.min.css'}),

+ 6 - 3
webpack.serve.js

@@ -1,18 +1,21 @@
-/* global module */
+/* global module, __dirname */
 const HTMLWebpackPlugin = require('html-webpack-plugin');
 const common = require("./webpack.common.js");
 const { merge } = require("webpack-merge");
+const path = require("path");
 
 module.exports = merge(common, {
     mode: "development",
     devtool: "inline-source-map",
     devServer: {
-        contentBase: "./"
+        static: [ path.resolve(__dirname, '') ],
+        port: 3003
     },
     plugins: [
         new HTMLWebpackPlugin({
             title: 'Converse.js Dev',
-            template: 'webpack.html'
+            template: 'webpack.html',
+            filename: 'index.html'
         })
     ],
 });

部分文件因文件數量過多而無法顯示