Selaa lähdekoodia

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 vuotta sitten
vanhempi
commit
dd609c1cec
10 muutettua tiedostoa jossa 1026 lisäystä ja 998 poistoa
  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>
     <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
 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.
     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`:
 .. _`dependency-libsignal`:
 
 
 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::
 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
 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:
 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"
     // Commonly used utilities and variables can be found under the "env"
     // namespace of the "converse" global.
     // 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
 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.
 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
 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``
 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
 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
 .. 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: {
     resolve: {
         extensions: ['.js'],
         extensions: ['.js'],
         modules: [
         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')
             path.join(__dirname, 'node_modules/converse.js/src')
         ],
         ],
         alias: {
         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`:
 .. _`dependencies`:

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 377 - 306
package-lock.json


+ 3 - 3
package.json

@@ -83,7 +83,7 @@
     "exports-loader": "^0.7.0",
     "exports-loader": "^0.7.0",
     "fast-text-encoding": "^1.0.3",
     "fast-text-encoding": "^1.0.3",
     "file-loader": "^6.0.0",
     "file-loader": "^6.0.0",
-    "html-webpack-plugin": "^4.3.0",
+    "html-webpack-plugin": "^5.3.1",
     "http-server": "^0.12.3",
     "http-server": "^0.12.3",
     "imports-loader": "^0.8.0",
     "imports-loader": "^0.8.0",
     "install": "^0.13.0",
     "install": "^0.13.0",
@@ -110,8 +110,8 @@
     "sinon": "^9.2.4",
     "sinon": "^9.2.4",
     "style-loader": "^0.23.1",
     "style-loader": "^0.23.1",
     "webpack": "^5.36.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"
     "webpack-merge": "^5.7.3"
   },
   },
   "dependencies": {
   "dependencies": {

+ 587 - 594
spec/mock.js

@@ -1,10 +1,10 @@
-/*global converse */
-
 const mock = {};
 const mock = {};
 window.mock = 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;
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000;
 
 
@@ -25,7 +25,6 @@ mock.initConverse = function (promise_names=[], settings=null, func) {
         }
         }
         document.title = "Converse Tests";
         document.title = "Converse Tests";
 
 
-        await converseLoaded;
         await initConverse(settings);
         await initConverse(settings);
         await Promise.all((promise_names || []).map(_converse.api.waitUntil));
         await Promise.all((promise_names || []).map(_converse.api.waitUntil));
         try {
         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'),
                     'pubKey': new TextEncoder('utf-8').encode('1234'),
                     'privKey': new TextEncoder('utf-8').encode('4321')
                     '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;
 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;
 export default converse;

+ 0 - 10
src/headless/core.js

@@ -1419,13 +1419,3 @@ Object.assign(converse, {
         u,
         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>
     <script src="3rdparty/libsignal-protocol.js"></script>
     <link rel="manifest" href="./manifest.json">
     <link rel="manifest" href="./manifest.json">
     <link rel="shortcut icon" type="image/ico" href="favicon.ico"/>
     <link rel="shortcut icon" type="image/ico" href="favicon.ico"/>
-    <script src="https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js"></script>
 </head>
 </head>
 <body class="reset"></body>
 <body class="reset"></body>
 <script>
 <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>
 </script>
 </html>
 </html>

+ 1 - 1
webpack.prod.js

@@ -5,7 +5,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 const common = require("./webpack.common.js");
 const common = require("./webpack.common.js");
 const path = require('path');
 const path = require('path');
 const webpack = require('webpack');
 const webpack = require('webpack');
-const { merge}  = require("webpack-merge");
+const { merge }  = require("webpack-merge");
 
 
 const plugins = [
 const plugins = [
     new MiniCssExtractPlugin({filename: '../dist/converse.min.css'}),
     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 HTMLWebpackPlugin = require('html-webpack-plugin');
 const common = require("./webpack.common.js");
 const common = require("./webpack.common.js");
 const { merge } = require("webpack-merge");
 const { merge } = require("webpack-merge");
+const path = require("path");
 
 
 module.exports = merge(common, {
 module.exports = merge(common, {
     mode: "development",
     mode: "development",
     devtool: "inline-source-map",
     devtool: "inline-source-map",
     devServer: {
     devServer: {
-        contentBase: "./"
+        static: [ path.resolve(__dirname, '') ],
+        port: 3003
     },
     },
     plugins: [
     plugins: [
         new HTMLWebpackPlugin({
         new HTMLWebpackPlugin({
             title: 'Converse.js Dev',
             title: 'Converse.js Dev',
-            template: 'webpack.html'
+            template: 'webpack.html',
+            filename: 'index.html'
         })
         })
     ],
     ],
 });
 });

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä