소스 검색

Move disco plugin into folder and import lodash utilities separately

JC Brand 4 년 전
부모
커밋
7b7ec45db8

+ 1 - 1
karma.conf.js

@@ -27,7 +27,6 @@ module.exports = function(config) {
 
       { pattern: "spec/converse.js", type: 'module' },
       { pattern: "spec/corrections.js", type: 'module' },
-      { pattern: "spec/disco.js", type: 'module' },
       { pattern: "spec/emojis.js", type: 'module' },
       { pattern: "spec/eventemitter.js", type: 'module' },
       { pattern: "spec/http-file-upload.js", type: 'module' },
@@ -44,6 +43,7 @@ module.exports = function(config) {
       { pattern: "spec/utils.js", type: 'module' },
       { pattern: "spec/xmppstatus.js", type: 'module' },
       { pattern: "src/headless/plugins/muc/tests/affiliations.js", type: 'module' },
+      { pattern: "src/headless/plugins/tests/disco.js", type: 'module' },
       { pattern: "src/plugins/bookmark-views/tests/bookmarks.js", type: 'module' },
       { pattern: "src/plugins/chatview/tests/chatbox.js", type: 'module' },
       { pattern: "src/plugins/chatview/tests/me-messages.js", type: 'module' },

+ 33 - 35
package-lock.json

@@ -2837,10 +2837,10 @@
 					}
 				},
 				"skeletor.js": {
-					"version": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561",
-					"from": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561",
+					"version": "github:skeletorjs/skeletor#482acb03ff4ec92a5cabccf023294912a574132c",
+					"from": "github:skeletorjs/skeletor#482acb03ff4ec92a5cabccf023294912a574132c",
 					"requires": {
-						"lodash": "^4.17.14"
+						"lodash-es": "^4.17.15"
 					}
 				},
 				"strophe.js": {
@@ -4495,9 +4495,9 @@
 			}
 		},
 		"@octokit/openapi-types": {
-			"version": "6.0.0",
-			"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-6.0.0.tgz",
-			"integrity": "sha512-CnDdK7ivHkBtJYzWzZm7gEkanA7gKH6a09Eguz7flHw//GacPJLmkHA3f3N++MJmlxD1Fl+mB7B32EEpSCwztQ==",
+			"version": "6.1.0",
+			"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-6.1.0.tgz",
+			"integrity": "sha512-Z9fDZVbGj4dFLErEoXUSuZhk3wJ8KVGnbrUwoPijsQ9EyNwOeQ+U2jSqaHUz8WtgIWf0aeO59oJyhMpWCKaabg==",
 			"dev": true
 		},
 		"@octokit/plugin-enterprise-rest": {
@@ -4554,18 +4554,16 @@
 			}
 		},
 		"@octokit/request": {
-			"version": "5.4.14",
-			"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.14.tgz",
-			"integrity": "sha512-VkmtacOIQp9daSnBmDI92xNIeLuSRDOIuplp/CJomkvzt7M18NXgG044Cx/LFKLgjKt9T2tZR6AtJayba9GTSA==",
+			"version": "5.4.15",
+			"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.15.tgz",
+			"integrity": "sha512-6UnZfZzLwNhdLRreOtTkT9n57ZwulCve8q3IT/Z477vThu6snfdkBuhxnChpOKNGxcQ71ow561Qoa6uqLdPtag==",
 			"dev": true,
 			"requires": {
 				"@octokit/endpoint": "^6.0.1",
 				"@octokit/request-error": "^2.0.0",
 				"@octokit/types": "^6.7.1",
-				"deprecation": "^2.0.0",
 				"is-plain-object": "^5.0.0",
 				"node-fetch": "^2.6.1",
-				"once": "^1.4.0",
 				"universal-user-agent": "^6.0.0"
 			},
 			"dependencies": {
@@ -4641,9 +4639,9 @@
 			}
 		},
 		"@octokit/types": {
-			"version": "6.13.0",
-			"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.13.0.tgz",
-			"integrity": "sha512-W2J9qlVIU11jMwKHUp5/rbVUeErqelCsO5vW5PKNb7wAXQVUz87Rc+imjlEvpvbH8yUb+KHmv8NEjVZdsdpyxA==",
+			"version": "6.13.1",
+			"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.13.1.tgz",
+			"integrity": "sha512-UF/PL0y4SKGx/p1azFf7e6j9lB78tVwAFvnHtslzOJ6VipshYks74qm9jjTEDlCyaTmbhbk2h3Run5l0CtCF6A==",
 			"dev": true,
 			"requires": {
 				"@octokit/openapi-types": "^6.0.0"
@@ -5979,9 +5977,9 @@
 			}
 		},
 		"before-after-hook": {
-			"version": "2.2.0",
-			"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.0.tgz",
-			"integrity": "sha512-jH6rKQIfroBbhEXVmI7XmXe3ix5S/PgJqpzdDPnR8JGLHWNYLsYZ6tK5iWOF/Ra3oqEX0NobXGlzbiylIzVphQ==",
+			"version": "2.2.1",
+			"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.1.tgz",
+			"integrity": "sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw==",
 			"dev": true
 		},
 		"better-assert": {
@@ -9795,9 +9793,9 @@
 			"optional": true
 		},
 		"filesize": {
-			"version": "6.1.0",
-			"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
-			"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg=="
+			"version": "6.2.6",
+			"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.2.6.tgz",
+			"integrity": "sha512-329LZkP3cIi17Eha17gaMEtgl1IJJG/zmv5NIjk6BECybiO+88D970mp2ke7U96DT0h3NT2wimXo/XrdL+7qbQ=="
 		},
 		"fill-range": {
 			"version": "4.0.0",
@@ -13635,9 +13633,9 @@
 			"dev": true
 		},
 		"map-obj": {
-			"version": "4.2.0",
-			"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.0.tgz",
-			"integrity": "sha512-NAq0fCmZYGz9UFEQyndp7sisrow4GroyGeKluyKC/chuITZsPyOyC1UJZPJlVFImhXdROIP5xqouRLThT3BbpQ==",
+			"version": "4.2.1",
+			"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz",
+			"integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==",
 			"dev": true
 		},
 		"map-visit": {
@@ -13837,9 +13835,9 @@
 					},
 					"dependencies": {
 						"hosted-git-info": {
-							"version": "2.8.8",
-							"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
-							"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+							"version": "2.8.9",
+							"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+							"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
 							"dev": true
 						},
 						"normalize-package-data": {
@@ -18640,9 +18638,9 @@
 			}
 		},
 		"object-inspect": {
-			"version": "1.9.0",
-			"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
-			"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==",
+			"version": "1.10.2",
+			"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz",
+			"integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==",
 			"dev": true
 		},
 		"object-is": {
@@ -20813,9 +20811,9 @@
 			}
 		},
 		"rxjs": {
-			"version": "6.6.6",
-			"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz",
-			"integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==",
+			"version": "6.6.7",
+			"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+			"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
 			"dev": true,
 			"requires": {
 				"tslib": "^1.9.0"
@@ -22185,9 +22183,9 @@
 			},
 			"dependencies": {
 				"ws": {
-					"version": "7.4.4",
-					"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
-					"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
+					"version": "7.4.5",
+					"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
+					"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
 					"optional": true
 				}
 			}

+ 1 - 1
src/headless/headless.js

@@ -9,7 +9,7 @@ import "./plugins/caps.js";         // XEP-0115 Entity Capabilities
 import "./plugins/carbons.js";      // XEP-0280 Message Carbons
 import "./plugins/chat/index.js";   // RFC-6121 Instant messaging
 import "./plugins/chatboxes/index.js";
-import "./plugins/disco.js";        // XEP-0030 Service discovery
+import "./plugins/disco/index.js";  // XEP-0030 Service discovery
 import "./plugins/headlines.js";    // Support for headline messages
 import "./plugins/mam/index.js";    // XEP-0313 Message Archive Management
 import "./plugins/muc/index.js";    // XEP-0045 Multi-user chat

+ 0 - 813
src/headless/plugins/disco.js

@@ -1,813 +0,0 @@
-/**
- * @module converse-disco
- * @copyright The Converse.js contributors
- * @license Mozilla Public License (MPLv2)
- * @description Converse plugin which add support for XEP-0030: Service Discovery
- */
-import log from "../log.js";
-import sizzle from "sizzle";
-import { Collection } from "@converse/skeletor/src/collection";
-import { Model } from '@converse/skeletor/src/model.js';
-import { _converse, api, converse } from "../core.js";
-import { isObject } from "lodash-es";
-
-const { Strophe, $iq, utils } = converse.env;
-
-converse.plugins.add('converse-disco', {
-
-    initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by converse.js's plugin machinery.
-         */
-
-        // Promises exposed by this plugin
-        api.promises.add('discoInitialized');
-        api.promises.add('streamFeaturesAdded');
-
-
-        /**
-         * @class
-         * @namespace _converse.DiscoEntity
-         * @memberOf _converse
-         */
-        _converse.DiscoEntity = Model.extend({
-            /* A Disco Entity is a JID addressable entity that can be queried
-             * for features.
-             *
-             * See XEP-0030: https://xmpp.org/extensions/xep-0030.html
-             */
-            idAttribute: 'jid',
-
-            initialize (attrs, options) {
-                this.waitUntilFeaturesDiscovered = utils.getResolveablePromise();
-
-                this.dataforms = new Collection();
-                let id = `converse.dataforms-${this.get('jid')}`;
-                this.dataforms.browserStorage = _converse.createStore(id, 'session');
-
-                this.features = new Collection();
-                id = `converse.features-${this.get('jid')}`;
-                this.features.browserStorage = _converse.createStore(id, 'session');
-                this.listenTo(this.features, 'add', this.onFeatureAdded)
-
-                this.fields = new Collection();
-                id = `converse.fields-${this.get('jid')}`;
-                this.fields.browserStorage = _converse.createStore(id, 'session');
-                this.listenTo(this.fields, 'add', this.onFieldAdded)
-
-                this.identities = new Collection();
-                id = `converse.identities-${this.get('jid')}`;
-                this.identities.browserStorage = _converse.createStore(id, 'session');
-                this.fetchFeatures(options);
-
-                this.items = new _converse.DiscoEntities();
-                id = `converse.disco-items-${this.get('jid')}`;
-                this.items.browserStorage = _converse.createStore(id, 'session');
-                this.items.fetch();
-            },
-
-            /**
-             * Returns a Promise which resolves with a map indicating
-             * whether a given identity is provided by this entity.
-             * @private
-             * @method _converse.DiscoEntity#getIdentity
-             * @param { String } category - The identity category
-             * @param { String } type - The identity type
-             */
-            async getIdentity (category, type) {
-                await this.waitUntilFeaturesDiscovered;
-                return this.identities.findWhere({
-                    'category': category,
-                    'type': type
-                });
-            },
-
-            /**
-             * Returns a Promise which resolves with a map indicating
-             * whether a given feature is supported.
-             * @private
-             * @method _converse.DiscoEntity#hasFeature
-             * @param { String } feature - The feature that might be supported.
-             */
-            async hasFeature (feature) {
-                await this.waitUntilFeaturesDiscovered
-                if (this.features.findWhere({'var': feature})) {
-                    return this;
-                }
-            },
-
-            onFeatureAdded (feature) {
-                feature.entity = this;
-                /**
-                 * Triggered when Converse has learned of a service provided by the XMPP server.
-                 * See XEP-0030.
-                 * @event _converse#serviceDiscovered
-                 * @type { Model }
-                 * @example _converse.api.listen.on('featuresDiscovered', feature => { ... });
-                 */
-                api.trigger('serviceDiscovered', feature);
-            },
-
-            onFieldAdded (field) {
-                field.entity = this;
-                /**
-                 * Triggered when Converse has learned of a disco extension field.
-                 * See XEP-0030.
-                 * @event _converse#discoExtensionFieldDiscovered
-                 * @example _converse.api.listen.on('discoExtensionFieldDiscovered', () => { ... });
-                 */
-                api.trigger('discoExtensionFieldDiscovered', field);
-            },
-
-            async fetchFeatures (options) {
-                if (options.ignore_cache) {
-                    this.queryInfo();
-                } else {
-                    const store_id = this.features.browserStorage.name;
-                    const result = await this.features.browserStorage.store.getItem(store_id);
-                    if (result && result.length === 0 || result === null) {
-                        this.queryInfo();
-                    } else {
-                        this.features.fetch({
-                            add: true,
-                            success: () => {
-                                this.waitUntilFeaturesDiscovered.resolve(this);
-                                this.trigger('featuresDiscovered');
-                            }
-                        });
-                        this.identities.fetch({add: true});
-                    }
-                }
-            },
-
-            async queryInfo () {
-                let stanza;
-                try {
-                    stanza = await api.disco.info(this.get('jid'), null);
-                } catch (iq) {
-                    iq === null ? log.error(`Timeout for disco#info query for ${this.get('jid')}`) : log.error(iq);
-                    this.waitUntilFeaturesDiscovered.resolve(this);
-                    return;
-                }
-                this.onInfo(stanza);
-            },
-
-            onDiscoItems (stanza) {
-                sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"] item`, stanza).forEach(item => {
-                    if (item.getAttribute("node")) {
-                        // XXX: Ignore nodes for now.
-                        // See: https://xmpp.org/extensions/xep-0030.html#items-nodes
-                        return;
-                    }
-                    const jid = item.getAttribute('jid');
-                    if (this.items.get(jid) === undefined) {
-                        const entity = _converse.disco_entities.get(jid);
-                        if (entity) {
-                            this.items.add(entity);
-                        } else {
-                            this.items.create({'jid': jid});
-                        }
-                    }
-                });
-            },
-
-            async queryForItems () {
-                if (this.identities.where({'category': 'server'}).length === 0) {
-                    // Don't fetch features and items if this is not a
-                    // server or a conference component.
-                    return;
-                }
-                const stanza = await api.disco.items(this.get('jid'));
-                this.onDiscoItems(stanza);
-            },
-
-            onInfo (stanza) {
-                Array.from(stanza.querySelectorAll('identity')).forEach(identity => {
-                    this.identities.create({
-                        'category': identity.getAttribute('category'),
-                        'type': identity.getAttribute('type'),
-                        'name': identity.getAttribute('name')
-                    });
-                });
-
-                sizzle(`x[type="result"][xmlns="${Strophe.NS.XFORM}"]`, stanza).forEach(form => {
-                    const data = {};
-                    sizzle('field', form).forEach(field => {
-                        data[field.getAttribute('var')] = {
-                            'value': field.querySelector('value')?.textContent,
-                            'type': field.getAttribute('type')
-                        };
-                    });
-                    this.dataforms.create(data);
-                });
-
-                if (stanza.querySelector(`feature[var="${Strophe.NS.DISCO_ITEMS}"]`)) {
-                    this.queryForItems();
-                }
-                Array.from(stanza.querySelectorAll('feature')).forEach(feature => {
-                    this.features.create({
-                        'var': feature.getAttribute('var'),
-                        'from': stanza.getAttribute('from')
-                    });
-                });
-
-                // XEP-0128 Service Discovery Extensions
-                sizzle('x[type="result"][xmlns="jabber:x:data"] field', stanza).forEach(field => {
-                    this.fields.create({
-                        'var': field.getAttribute('var'),
-                        'value': field.querySelector('value')?.textContent,
-                        'from': stanza.getAttribute('from')
-                    });
-                });
-
-                this.waitUntilFeaturesDiscovered.resolve(this);
-                this.trigger('featuresDiscovered');
-            }
-        });
-
-        _converse.DiscoEntities = Collection.extend({
-            model: _converse.DiscoEntity,
-
-            fetchEntities () {
-                return new Promise((resolve, reject) => {
-                    this.fetch({
-                        add: true,
-                        success: resolve,
-                        error (m, e) {
-                            log.error(e);
-                            reject (new Error("Could not fetch disco entities"));
-                        }
-                    });
-                });
-            }
-        });
-
-
-        function addClientFeatures () {
-            // See https://xmpp.org/registrar/disco-categories.html
-            api.disco.own.identities.add('client', 'web', 'Converse');
-
-            api.disco.own.features.add(Strophe.NS.CHATSTATES);
-            api.disco.own.features.add(Strophe.NS.DISCO_INFO);
-            api.disco.own.features.add(Strophe.NS.ROSTERX); // Limited support
-            if (api.settings.get("message_carbons")) {
-                api.disco.own.features.add(Strophe.NS.CARBONS);
-            }
-            /**
-             * Triggered in converse-disco once the core disco features of
-             * Converse have been added.
-             * @event _converse#addClientFeatures
-             * @example _converse.api.listen.on('addClientFeatures', () => { ... });
-             */
-            api.trigger('addClientFeatures');
-            return this;
-        }
-
-
-        function initStreamFeatures () {
-            // Initialize the stream_features collection, and if we're
-            // re-attaching to a pre-existing BOSH session, we restore the
-            // features from cache.
-            // Otherwise the features will be created once we've received them
-            // from the server (see populateStreamFeatures).
-            if (!_converse.stream_features) {
-                const bare_jid = Strophe.getBareJidFromJid(_converse.jid);
-                const id = `converse.stream-features-${bare_jid}`;
-                api.promises.add('streamFeaturesAdded');
-                _converse.stream_features = new Collection();
-                _converse.stream_features.browserStorage = _converse.createStore(id, "session");
-            }
-        }
-
-
-        function populateStreamFeatures () {
-            // Strophe.js sets the <stream:features> element on the
-            // Strophe.Connection instance (_converse.connection).
-            //
-            // Once this is done, we populate the _converse.stream_features collection
-            // and trigger streamFeaturesAdded.
-            initStreamFeatures();
-            Array.from(_converse.connection.features.childNodes).forEach(feature => {
-                _converse.stream_features.create({
-                    'name': feature.nodeName,
-                    'xmlns': feature.getAttribute('xmlns')
-                });
-            });
-            notifyStreamFeaturesAdded();
-        }
-
-
-        function notifyStreamFeaturesAdded () {
-            /**
-             * Triggered as soon as the stream features are known.
-             * If you want to check whether a stream feature is supported before proceeding,
-             * then you'll first want to wait for this event.
-             * @event _converse#streamFeaturesAdded
-             * @example _converse.api.listen.on('streamFeaturesAdded', () => { ... });
-             */
-            api.trigger('streamFeaturesAdded');
-        }
-
-
-        const plugin = this;
-        plugin._identities = [];
-        plugin._features = [];
-
-        function onDiscoInfoRequest (stanza) {
-            const node = stanza.getElementsByTagName('query')[0].getAttribute('node');
-            const attrs = {xmlns: Strophe.NS.DISCO_INFO};
-            if (node) { attrs.node = node; }
-
-            const iqresult = $iq({'type': 'result', 'id': stanza.getAttribute('id')});
-            const from = stanza.getAttribute('from');
-            if (from !== null) {
-                iqresult.attrs({'to': from});
-            }
-            iqresult.c('query', attrs);
-            plugin._identities.forEach(identity => {
-                const attrs = {
-                    'category': identity.category,
-                    'type': identity.type
-                };
-                if (identity.name) {
-                    attrs.name = identity.name;
-                }
-                if (identity.lang) {
-                    attrs['xml:lang'] = identity.lang;
-                }
-                iqresult.c('identity', attrs).up();
-            });
-            plugin._features.forEach(feature => iqresult.c('feature', {'var': feature}).up());
-            api.send(iqresult.tree());
-            return true;
-        }
-
-
-        async function initializeDisco () {
-            addClientFeatures();
-            _converse.connection.addHandler(onDiscoInfoRequest, Strophe.NS.DISCO_INFO, 'iq', 'get', null, null);
-
-            _converse.disco_entities = new _converse.DiscoEntities();
-            const id = `converse.disco-entities-${_converse.bare_jid}`;
-            _converse.disco_entities.browserStorage = _converse.createStore(id, 'session');
-            const collection = await _converse.disco_entities.fetchEntities();
-            if (collection.length === 0 || !collection.get(_converse.domain)) {
-                // If we don't have an entity for our own XMPP server,
-                // create one.
-                _converse.disco_entities.create({'jid': _converse.domain});
-            }
-            /**
-             * Triggered once the `converse-disco` plugin has been initialized and the
-             * `_converse.disco_entities` collection will be available and populated with at
-             * least the service discovery features of the user's own server.
-             * @event _converse#discoInitialized
-             * @example _converse.api.listen.on('discoInitialized', () => { ... });
-             */
-            api.trigger('discoInitialized');
-        }
-
-        /******************** Event Handlers ********************/
-
-        api.listen.on('userSessionInitialized', async () => {
-            initStreamFeatures();
-            if (_converse.connfeedback.get('connection_status') === Strophe.Status.ATTACHED) {
-                // When re-attaching to a BOSH session, we fetch the stream features from the cache.
-                await new Promise((success, error) => _converse.stream_features.fetch({ success, error }));
-                notifyStreamFeaturesAdded();
-            }
-        });
-        api.listen.on('beforeResourceBinding', populateStreamFeatures);
-
-        api.listen.on('reconnected', initializeDisco);
-        api.listen.on('connected', initializeDisco);
-
-        api.listen.on('beforeTearDown', async () => {
-            api.promises.add('streamFeaturesAdded')
-            if (_converse.stream_features) {
-                await _converse.stream_features.clearStore();
-                delete _converse.stream_features;
-            }
-        });
-
-        api.listen.on('clearSession', () => {
-            if (_converse.shouldClearCache() && _converse.disco_entities) {
-                Array.from(_converse.disco_entities.models).forEach(e => e.features.clearStore());
-                Array.from(_converse.disco_entities.models).forEach(e => e.identities.clearStore());
-                Array.from(_converse.disco_entities.models).forEach(e => e.dataforms.clearStore());
-                Array.from(_converse.disco_entities.models).forEach(e => e.fields.clearStore());
-                _converse.disco_entities.clearStore();
-                delete _converse.disco_entities;
-            }
-        });
-
-
-        /************************ API ************************/
-
-        Object.assign(api, {
-            /**
-             * The XEP-0030 service discovery API
-             *
-             * This API lets you discover information about entities on the
-             * XMPP network.
-             *
-             * @namespace api.disco
-             * @memberOf api
-             */
-            disco: {
-                /**
-                 * @namespace api.disco.stream
-                 * @memberOf api.disco
-                 */
-                stream: {
-                    /**
-                     * @method api.disco.stream.getFeature
-                     * @param {String} name The feature name
-                     * @param {String} xmlns The XML namespace
-                     * @example _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver')
-                     */
-                    async getFeature (name, xmlns) {
-                        await api.waitUntil('streamFeaturesAdded');
-                        if (!name || !xmlns) {
-                            throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature");
-                        }
-                        if (_converse.stream_features === undefined && !api.connection.connected()) {
-                            // Happens during tests when disco lookups happen asynchronously after teardown.
-                            const msg = `Tried to get feature ${name} ${xmlns} but _converse.stream_features has been torn down`;
-                            log.warn(msg);
-                            return;
-                        }
-                        return _converse.stream_features.findWhere({'name': name, 'xmlns': xmlns});
-                    }
-                },
-
-                /**
-                 * @namespace api.disco.own
-                 * @memberOf api.disco
-                 */
-                own: {
-                    /**
-                     * @namespace api.disco.own.identities
-                     * @memberOf api.disco.own
-                     */
-                    identities: {
-                        /**
-                         * Lets you add new identities for this client (i.e. instance of Converse)
-                         * @method api.disco.own.identities.add
-                         *
-                         * @param {String} category - server, client, gateway, directory, etc.
-                         * @param {String} type - phone, pc, web, etc.
-                         * @param {String} name - "Converse"
-                         * @param {String} lang - en, el, de, etc.
-                         *
-                         * @example _converse.api.disco.own.identities.clear();
-                         */
-                        add (category, type, name, lang) {
-                            for (var i=0; i<plugin._identities.length; i++) {
-                                if (plugin._identities[i].category == category &&
-                                    plugin._identities[i].type == type &&
-                                    plugin._identities[i].name == name &&
-                                    plugin._identities[i].lang == lang) {
-                                    return false;
-                                }
-                            }
-                            plugin._identities.push({category: category, type: type, name: name, lang: lang});
-                        },
-                        /**
-                         * Clears all previously registered identities.
-                         * @method api.disco.own.identities.clear
-                         * @example _converse.api.disco.own.identities.clear();
-                         */
-                        clear () {
-                            plugin._identities = []
-                        },
-                        /**
-                         * Returns all of the identities registered for this client
-                         * (i.e. instance of Converse).
-                         * @method api.disco.identities.get
-                         * @example const identities = api.disco.own.identities.get();
-                         */
-                        get () {
-                            return plugin._identities;
-                        }
-                    },
-
-                    /**
-                     * @namespace api.disco.own.features
-                     * @memberOf api.disco.own
-                     */
-                    features: {
-                        /**
-                         * Lets you register new disco features for this client (i.e. instance of Converse)
-                         * @method api.disco.own.features.add
-                         * @param {String} name - e.g. http://jabber.org/protocol/caps
-                         * @example _converse.api.disco.own.features.add("http://jabber.org/protocol/caps");
-                         */
-                        add (name) {
-                            for (var i=0; i<plugin._features.length; i++) {
-                                if (plugin._features[i] == name) { return false; }
-                            }
-                            plugin._features.push(name);
-                        },
-                        /**
-                         * Clears all previously registered features.
-                         * @method api.disco.own.features.clear
-                         * @example _converse.api.disco.own.features.clear();
-                         */
-                        clear () {
-                            plugin._features = []
-                        },
-                        /**
-                         * Returns all of the features registered for this client (i.e. instance of Converse).
-                         * @method api.disco.own.features.get
-                         * @example const features = api.disco.own.features.get();
-                         */
-                        get () {
-                            return plugin._features;
-                        }
-                    }
-                },
-
-                /**
-                 * Query for information about an XMPP entity
-                 *
-                 * @method api.disco.info
-                 * @param {string} jid The Jabber ID of the entity to query
-                 * @param {string} [node] A specific node identifier associated with the JID
-                 * @returns {promise} Promise which resolves once we have a result from the server.
-                 */
-                info (jid, node) {
-                    const attrs = {xmlns: Strophe.NS.DISCO_INFO};
-                    if (node) {
-                        attrs.node = node;
-                    }
-                    const info = $iq({
-                        'from': _converse.connection.jid,
-                        'to':jid,
-                        'type':'get'
-                    }).c('query', attrs);
-                    return api.sendIQ(info);
-                },
-
-                /**
-                 * Query for items associated with an XMPP entity
-                 *
-                 * @method api.disco.items
-                 * @param {string} jid The Jabber ID of the entity to query for items
-                 * @param {string} [node] A specific node identifier associated with the JID
-                 * @returns {promise} Promise which resolves once we have a result from the server.
-                 */
-                items (jid, node) {
-                    const attrs = {'xmlns': Strophe.NS.DISCO_ITEMS};
-                    if (node) {
-                        attrs.node = node;
-                    }
-                    return api.sendIQ(
-                        $iq({
-                            'from': _converse.connection.jid,
-                            'to':jid,
-                            'type':'get'
-                        }).c('query', attrs)
-                    );
-                },
-
-                /**
-                 * Namespace for methods associated with disco entities
-                 *
-                 * @namespace api.disco.entities
-                 * @memberOf api.disco
-                 */
-                entities: {
-                    /**
-                     * Get the corresponding `DiscoEntity` instance.
-                     *
-                     * @method api.disco.entities.get
-                     * @param {string} jid The Jabber ID of the entity
-                     * @param {boolean} [create] Whether the entity should be created if it doesn't exist.
-                     * @example _converse.api.disco.entities.get(jid);
-                     */
-                    async get (jid, create=false) {
-                        await api.waitUntil('discoInitialized');
-                        if (!jid) {
-                            return _converse.disco_entities;
-                        }
-                        if (_converse.disco_entities === undefined && !api.connection.connected()) {
-                            // Happens during tests when disco lookups happen asynchronously after teardown.
-                            const msg = `Tried to look up entity ${jid} but _converse.disco_entities has been torn down`;
-                            log.warn(msg);
-                            return;
-                        }
-                        const entity = _converse.disco_entities.get(jid);
-                        if (entity || !create) {
-                            return entity;
-                        }
-                        return api.disco.entities.create(jid);
-                    },
-
-                    /**
-                     * Create a new disco entity. It's identity and features
-                     * will automatically be fetched from cache or from the
-                     * XMPP server.
-                     *
-                     * Fetching from cache can be disabled by passing in
-                     * `ignore_cache: true` in the options parameter.
-                     *
-                     * @method api.disco.entities.create
-                     * @param {string} jid The Jabber ID of the entity
-                     * @param {object} [options] Additional options
-                     * @param {boolean} [options.ignore_cache]
-                     *     If true, fetch all features from the XMPP server instead of restoring them from cache
-                     * @example _converse.api.disco.entities.create(jid, {'ignore_cache': true});
-                     */
-                    create (jid, options) {
-                        return _converse.disco_entities.create({'jid': jid}, options);
-                    }
-                },
-
-                /**
-                 * @namespace api.disco.features
-                 * @memberOf api.disco
-                 */
-                features: {
-                    /**
-                     * Return a given feature of a disco entity
-                     *
-                     * @method api.disco.features.get
-                     * @param {string} feature The feature that might be
-                     *     supported. In the XML stanza, this is the `var`
-                     *     attribute of the `<feature>` element. For
-                     *     example: `http://jabber.org/protocol/muc`
-                     * @param {string} jid The JID of the entity
-                     *     (and its associated items) which should be queried
-                     * @returns {promise} A promise which resolves with a list containing
-                     *     _converse.Entity instances representing the entity
-                     *     itself or those items associated with the entity if
-                     *     they support the given feature.
-                     * @example
-                     * api.disco.features.get(Strophe.NS.MAM, _converse.bare_jid);
-                     */
-                    async get (feature, jid) {
-                        if (!jid) {
-                            throw new TypeError('You need to provide an entity JID');
-                        }
-                        await api.waitUntil('discoInitialized');
-                        let entity = await api.disco.entities.get(jid, true);
-
-                        if (_converse.disco_entities === undefined && !api.connection.connected()) {
-                            // Happens during tests when disco lookups happen asynchronously after teardown.
-                            const msg = `Tried to get feature ${feature} for ${jid} but _converse.disco_entities has been torn down`;
-                            log.warn(msg);
-                            return;
-                        }
-                        entity = await entity.waitUntilFeaturesDiscovered;
-                        const promises = [...entity.items.map(i => i.hasFeature(feature)), entity.hasFeature(feature)];
-                        const result = await Promise.all(promises);
-                        return result.filter(isObject);
-                    }
-                },
-
-                /**
-                 * Used to determine whether an entity supports a given feature.
-                 *
-                 * @method api.disco.supports
-                 * @param {string} feature The feature that might be
-                 *     supported. In the XML stanza, this is the `var`
-                 *     attribute of the `<feature>` element. For
-                 *     example: `http://jabber.org/protocol/muc`
-                 * @param {string} jid The JID of the entity
-                 *     (and its associated items) which should be queried
-                 * @returns {promise} A promise which resolves with `true` or `false`.
-                 * @example
-                 * if (await api.disco.supports(Strophe.NS.MAM, _converse.bare_jid)) {
-                 *     // The feature is supported
-                 * } else {
-                 *     // The feature is not supported
-                 * }
-                 */
-                async supports (feature, jid) {
-                    const features = await api.disco.features.get(feature, jid);
-                    return features.length > 0;
-                },
-
-                /**
-                 * Refresh the features, fields and identities associated with a
-                 * disco entity by refetching them from the server
-                 * @method api.disco.refresh
-                 * @param {string} jid The JID of the entity whose features are refreshed.
-                 * @returns {promise} A promise which resolves once the features have been refreshed
-                 * @example
-                 * await api.disco.refresh('room@conference.example.org');
-                 */
-                async refresh (jid) {
-                    if (!jid) {
-                        throw new TypeError('api.disco.refresh: You need to provide an entity JID');
-                    }
-                    await api.waitUntil('discoInitialized');
-                    let entity = await api.disco.entities.get(jid);
-                    if (entity) {
-                        entity.features.reset();
-                        entity.fields.reset();
-                        entity.identities.reset();
-                        if (!entity.waitUntilFeaturesDiscovered.isPending) {
-                            entity.waitUntilFeaturesDiscovered = utils.getResolveablePromise()
-                        }
-                        entity.queryInfo();
-                    } else {
-                        // Create it if it doesn't exist
-                        entity = await api.disco.entities.create(jid, {'ignore_cache': true});
-                    }
-                    return entity.waitUntilFeaturesDiscovered;
-                },
-
-                /**
-                 * @deprecated Use {@link api.disco.refresh} instead.
-                 * @method api.disco.refreshFeatures
-                 */
-                refreshFeatures (jid) {
-                    return api.refresh(jid);
-                },
-
-                /**
-                 * Return all the features associated with a disco entity
-                 *
-                 * @method api.disco.getFeatures
-                 * @param {string} jid The JID of the entity whose features are returned.
-                 * @returns {promise} A promise which resolves with the returned features
-                 * @example
-                 * const features = await api.disco.getFeatures('room@conference.example.org');
-                 */
-                async getFeatures (jid) {
-                    if (!jid) {
-                        throw new TypeError('api.disco.getFeatures: You need to provide an entity JID');
-                    }
-                    await api.waitUntil('discoInitialized');
-                    let entity = await api.disco.entities.get(jid, true);
-                    entity = await entity.waitUntilFeaturesDiscovered;
-                    return entity.features;
-                },
-
-                /**
-                 * Return all the service discovery extensions fields
-                 * associated with an entity.
-                 *
-                 * See [XEP-0129: Service Discovery Extensions](https://xmpp.org/extensions/xep-0128.html)
-                 *
-                 * @method api.disco.getFields
-                 * @param {string} jid The JID of the entity whose fields are returned.
-                 * @example
-                 * const fields = await api.disco.getFields('room@conference.example.org');
-                 */
-                async getFields (jid) {
-                    if (!jid) {
-                        throw new TypeError('api.disco.getFields: You need to provide an entity JID');
-                    }
-                    await api.waitUntil('discoInitialized');
-                    let entity = await api.disco.entities.get(jid, true);
-                    entity = await entity.waitUntilFeaturesDiscovered;
-                    return entity.fields;
-                },
-
-                /**
-                 * Get the identity (with the given category and type) for a given disco entity.
-                 *
-                 * For example, when determining support for PEP (personal eventing protocol), you
-                 * want to know whether the user's own JID has an identity with
-                 * `category='pubsub'` and `type='pep'` as explained in this section of
-                 * XEP-0163: https://xmpp.org/extensions/xep-0163.html#support
-                 *
-                 * @method api.disco.getIdentity
-                 * @param {string} The identity category.
-                 *     In the XML stanza, this is the `category`
-                 *     attribute of the `<identity>` element.
-                 *     For example: 'pubsub'
-                 * @param {string} type The identity type.
-                 *     In the XML stanza, this is the `type`
-                 *     attribute of the `<identity>` element.
-                 *     For example: 'pep'
-                 * @param {string} jid The JID of the entity which might have the identity
-                 * @returns {promise} A promise which resolves with a map indicating
-                 *     whether an identity with a given type is provided by the entity.
-                 * @example
-                 * api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid).then(
-                 *     function (identity) {
-                 *         if (identity) {
-                 *             // The entity DOES have this identity
-                 *         } else {
-                 *             // The entity DOES NOT have this identity
-                 *         }
-                 *     }
-                 * ).catch(e => log.error(e));
-                 */
-                async getIdentity (category, type, jid) {
-                    const e = await api.disco.entities.get(jid, true);
-                    if (e === undefined && !api.connection.connected()) {
-                        // Happens during tests when disco lookups happen asynchronously after teardown.
-                        const msg = `Tried to look up category ${category} for ${jid} but _converse.disco_entities has been torn down`;
-                        log.warn(msg);
-                        return;
-                    }
-                    return e.getIdentity(category, type);
-                }
-            }
-        });
-    }
-});

+ 413 - 0
src/headless/plugins/disco/api.js

@@ -0,0 +1,413 @@
+import { _converse, api, converse } from "@converse/headless/core.js";
+import isObject from "lodash-es/isObject";
+import log from "@converse/headless/log.js";
+
+const { Strophe, $iq, utils } = converse.env;
+
+
+export default {
+    /**
+     * The XEP-0030 service discovery API
+     *
+     * This API lets you discover information about entities on the
+     * XMPP network.
+     *
+     * @namespace api.disco
+     * @memberOf api
+     */
+    disco: {
+        /**
+         * @namespace api.disco.stream
+         * @memberOf api.disco
+         */
+        stream: {
+            /**
+             * @method api.disco.stream.getFeature
+             * @param {String} name The feature name
+             * @param {String} xmlns The XML namespace
+             * @example _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver')
+             */
+            async getFeature (name, xmlns) {
+                await api.waitUntil('streamFeaturesAdded');
+                if (!name || !xmlns) {
+                    throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature");
+                }
+                if (_converse.stream_features === undefined && !api.connection.connected()) {
+                    // Happens during tests when disco lookups happen asynchronously after teardown.
+                    const msg = `Tried to get feature ${name} ${xmlns} but _converse.stream_features has been torn down`;
+                    log.warn(msg);
+                    return;
+                }
+                return _converse.stream_features.findWhere({'name': name, 'xmlns': xmlns});
+            }
+        },
+
+        /**
+         * @namespace api.disco.own
+         * @memberOf api.disco
+         */
+        own: {
+            /**
+             * @namespace api.disco.own.identities
+             * @memberOf api.disco.own
+             */
+            identities: {
+                /**
+                 * Lets you add new identities for this client (i.e. instance of Converse)
+                 * @method api.disco.own.identities.add
+                 *
+                 * @param {String} category - server, client, gateway, directory, etc.
+                 * @param {String} type - phone, pc, web, etc.
+                 * @param {String} name - "Converse"
+                 * @param {String} lang - en, el, de, etc.
+                 *
+                 * @example _converse.api.disco.own.identities.clear();
+                 */
+                add (category, type, name, lang) {
+                    for (var i=0; i<_converse.disco._identities.length; i++) {
+                        if (_converse.disco._identities[i].category == category &&
+                            _converse.disco._identities[i].type == type &&
+                            _converse.disco._identities[i].name == name &&
+                            _converse.disco._identities[i].lang == lang) {
+                            return false;
+                        }
+                    }
+                    _converse.disco._identities.push({category: category, type: type, name: name, lang: lang});
+                },
+                /**
+                 * Clears all previously registered identities.
+                 * @method api.disco.own.identities.clear
+                 * @example _converse.api.disco.own.identities.clear();
+                 */
+                clear () {
+                    _converse.disco._identities = []
+                },
+                /**
+                 * Returns all of the identities registered for this client
+                 * (i.e. instance of Converse).
+                 * @method api.disco.identities.get
+                 * @example const identities = api.disco.own.identities.get();
+                 */
+                get () {
+                    return _converse.disco._identities;
+                }
+            },
+
+            /**
+             * @namespace api.disco.own.features
+             * @memberOf api.disco.own
+             */
+            features: {
+                /**
+                 * Lets you register new disco features for this client (i.e. instance of Converse)
+                 * @method api.disco.own.features.add
+                 * @param {String} name - e.g. http://jabber.org/protocol/caps
+                 * @example _converse.api.disco.own.features.add("http://jabber.org/protocol/caps");
+                 */
+                add (name) {
+                    for (var i=0; i<_converse.disco._features.length; i++) {
+                        if (_converse.disco._features[i] == name) { return false; }
+                    }
+                    _converse.disco._features.push(name);
+                },
+                /**
+                 * Clears all previously registered features.
+                 * @method api.disco.own.features.clear
+                 * @example _converse.api.disco.own.features.clear();
+                 */
+                clear () {
+                    _converse.disco._features = []
+                },
+                /**
+                 * Returns all of the features registered for this client (i.e. instance of Converse).
+                 * @method api.disco.own.features.get
+                 * @example const features = api.disco.own.features.get();
+                 */
+                get () {
+                    return _converse.disco._features;
+                }
+            }
+        },
+
+        /**
+         * Query for information about an XMPP entity
+         *
+         * @method api.disco.info
+         * @param {string} jid The Jabber ID of the entity to query
+         * @param {string} [node] A specific node identifier associated with the JID
+         * @returns {promise} Promise which resolves once we have a result from the server.
+         */
+        info (jid, node) {
+            const attrs = {xmlns: Strophe.NS.DISCO_INFO};
+            if (node) {
+                attrs.node = node;
+            }
+            const info = $iq({
+                'from': _converse.connection.jid,
+                'to':jid,
+                'type':'get'
+            }).c('query', attrs);
+            return api.sendIQ(info);
+        },
+
+        /**
+         * Query for items associated with an XMPP entity
+         *
+         * @method api.disco.items
+         * @param {string} jid The Jabber ID of the entity to query for items
+         * @param {string} [node] A specific node identifier associated with the JID
+         * @returns {promise} Promise which resolves once we have a result from the server.
+         */
+        items (jid, node) {
+            const attrs = {'xmlns': Strophe.NS.DISCO_ITEMS};
+            if (node) {
+                attrs.node = node;
+            }
+            return api.sendIQ(
+                $iq({
+                    'from': _converse.connection.jid,
+                    'to':jid,
+                    'type':'get'
+                }).c('query', attrs)
+            );
+        },
+
+        /**
+         * Namespace for methods associated with disco entities
+         *
+         * @namespace api.disco.entities
+         * @memberOf api.disco
+         */
+        entities: {
+            /**
+             * Get the corresponding `DiscoEntity` instance.
+             *
+             * @method api.disco.entities.get
+             * @param {string} jid The Jabber ID of the entity
+             * @param {boolean} [create] Whether the entity should be created if it doesn't exist.
+             * @example _converse.api.disco.entities.get(jid);
+             */
+            async get (jid, create=false) {
+                await api.waitUntil('discoInitialized');
+                if (!jid) {
+                    return _converse.disco_entities;
+                }
+                if (_converse.disco_entities === undefined && !api.connection.connected()) {
+                    // Happens during tests when disco lookups happen asynchronously after teardown.
+                    const msg = `Tried to look up entity ${jid} but _converse.disco_entities has been torn down`;
+                    log.warn(msg);
+                    return;
+                }
+                const entity = _converse.disco_entities.get(jid);
+                if (entity || !create) {
+                    return entity;
+                }
+                return api.disco.entities.create(jid);
+            },
+
+            /**
+             * Create a new disco entity. It's identity and features
+             * will automatically be fetched from cache or from the
+             * XMPP server.
+             *
+             * Fetching from cache can be disabled by passing in
+             * `ignore_cache: true` in the options parameter.
+             *
+             * @method api.disco.entities.create
+             * @param {string} jid The Jabber ID of the entity
+             * @param {object} [options] Additional options
+             * @param {boolean} [options.ignore_cache]
+             *     If true, fetch all features from the XMPP server instead of restoring them from cache
+             * @example _converse.api.disco.entities.create(jid, {'ignore_cache': true});
+             */
+            create (jid, options) {
+                return _converse.disco_entities.create({'jid': jid}, options);
+            }
+        },
+
+        /**
+         * @namespace api.disco.features
+         * @memberOf api.disco
+         */
+        features: {
+            /**
+             * Return a given feature of a disco entity
+             *
+             * @method api.disco.features.get
+             * @param {string} feature The feature that might be
+             *     supported. In the XML stanza, this is the `var`
+             *     attribute of the `<feature>` element. For
+             *     example: `http://jabber.org/protocol/muc`
+             * @param {string} jid The JID of the entity
+             *     (and its associated items) which should be queried
+             * @returns {promise} A promise which resolves with a list containing
+             *     _converse.Entity instances representing the entity
+             *     itself or those items associated with the entity if
+             *     they support the given feature.
+             * @example
+             * api.disco.features.get(Strophe.NS.MAM, _converse.bare_jid);
+             */
+            async get (feature, jid) {
+                if (!jid) {
+                    throw new TypeError('You need to provide an entity JID');
+                }
+                await api.waitUntil('discoInitialized');
+                let entity = await api.disco.entities.get(jid, true);
+
+                if (_converse.disco_entities === undefined && !api.connection.connected()) {
+                    // Happens during tests when disco lookups happen asynchronously after teardown.
+                    const msg = `Tried to get feature ${feature} for ${jid} but _converse.disco_entities has been torn down`;
+                    log.warn(msg);
+                    return;
+                }
+                entity = await entity.waitUntilFeaturesDiscovered;
+                const promises = [...entity.items.map(i => i.hasFeature(feature)), entity.hasFeature(feature)];
+                const result = await Promise.all(promises);
+                return result.filter(isObject);
+            }
+        },
+
+        /**
+         * Used to determine whether an entity supports a given feature.
+         *
+         * @method api.disco.supports
+         * @param {string} feature The feature that might be
+         *     supported. In the XML stanza, this is the `var`
+         *     attribute of the `<feature>` element. For
+         *     example: `http://jabber.org/protocol/muc`
+         * @param {string} jid The JID of the entity
+         *     (and its associated items) which should be queried
+         * @returns {promise} A promise which resolves with `true` or `false`.
+         * @example
+         * if (await api.disco.supports(Strophe.NS.MAM, _converse.bare_jid)) {
+         *     // The feature is supported
+         * } else {
+         *     // The feature is not supported
+         * }
+         */
+        async supports (feature, jid) {
+            const features = await api.disco.features.get(feature, jid);
+            return features.length > 0;
+        },
+
+        /**
+         * Refresh the features, fields and identities associated with a
+         * disco entity by refetching them from the server
+         * @method api.disco.refresh
+         * @param {string} jid The JID of the entity whose features are refreshed.
+         * @returns {promise} A promise which resolves once the features have been refreshed
+         * @example
+         * await api.disco.refresh('room@conference.example.org');
+         */
+        async refresh (jid) {
+            if (!jid) {
+                throw new TypeError('api.disco.refresh: You need to provide an entity JID');
+            }
+            await api.waitUntil('discoInitialized');
+            let entity = await api.disco.entities.get(jid);
+            if (entity) {
+                entity.features.reset();
+                entity.fields.reset();
+                entity.identities.reset();
+                if (!entity.waitUntilFeaturesDiscovered.isPending) {
+                    entity.waitUntilFeaturesDiscovered = utils.getResolveablePromise()
+                }
+                entity.queryInfo();
+            } else {
+                // Create it if it doesn't exist
+                entity = await api.disco.entities.create(jid, {'ignore_cache': true});
+            }
+            return entity.waitUntilFeaturesDiscovered;
+        },
+
+        /**
+         * @deprecated Use {@link api.disco.refresh} instead.
+         * @method api.disco.refreshFeatures
+         */
+        refreshFeatures (jid) {
+            return api.refresh(jid);
+        },
+
+        /**
+         * Return all the features associated with a disco entity
+         *
+         * @method api.disco.getFeatures
+         * @param {string} jid The JID of the entity whose features are returned.
+         * @returns {promise} A promise which resolves with the returned features
+         * @example
+         * const features = await api.disco.getFeatures('room@conference.example.org');
+         */
+        async getFeatures (jid) {
+            if (!jid) {
+                throw new TypeError('api.disco.getFeatures: You need to provide an entity JID');
+            }
+            await api.waitUntil('discoInitialized');
+            let entity = await api.disco.entities.get(jid, true);
+            entity = await entity.waitUntilFeaturesDiscovered;
+            return entity.features;
+        },
+
+        /**
+         * Return all the service discovery extensions fields
+         * associated with an entity.
+         *
+         * See [XEP-0129: Service Discovery Extensions](https://xmpp.org/extensions/xep-0128.html)
+         *
+         * @method api.disco.getFields
+         * @param {string} jid The JID of the entity whose fields are returned.
+         * @example
+         * const fields = await api.disco.getFields('room@conference.example.org');
+         */
+        async getFields (jid) {
+            if (!jid) {
+                throw new TypeError('api.disco.getFields: You need to provide an entity JID');
+            }
+            await api.waitUntil('discoInitialized');
+            let entity = await api.disco.entities.get(jid, true);
+            entity = await entity.waitUntilFeaturesDiscovered;
+            return entity.fields;
+        },
+
+        /**
+         * Get the identity (with the given category and type) for a given disco entity.
+         *
+         * For example, when determining support for PEP (personal eventing protocol), you
+         * want to know whether the user's own JID has an identity with
+         * `category='pubsub'` and `type='pep'` as explained in this section of
+         * XEP-0163: https://xmpp.org/extensions/xep-0163.html#support
+         *
+         * @method api.disco.getIdentity
+         * @param {string} The identity category.
+         *     In the XML stanza, this is the `category`
+         *     attribute of the `<identity>` element.
+         *     For example: 'pubsub'
+         * @param {string} type The identity type.
+         *     In the XML stanza, this is the `type`
+         *     attribute of the `<identity>` element.
+         *     For example: 'pep'
+         * @param {string} jid The JID of the entity which might have the identity
+         * @returns {promise} A promise which resolves with a map indicating
+         *     whether an identity with a given type is provided by the entity.
+         * @example
+         * api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid).then(
+         *     function (identity) {
+         *         if (identity) {
+         *             // The entity DOES have this identity
+         *         } else {
+         *             // The entity DOES NOT have this identity
+         *         }
+         *     }
+         * ).catch(e => log.error(e));
+         */
+        async getIdentity (category, type, jid) {
+            const e = await api.disco.entities.get(jid, true);
+            if (e === undefined && !api.connection.connected()) {
+                // Happens during tests when disco lookups happen asynchronously after teardown.
+                const msg = `Tried to look up category ${category} for ${jid} but _converse.disco_entities has been torn down`;
+                log.warn(msg);
+                return;
+            }
+            return e.getIdentity(category, type);
+        }
+    }
+}

+ 23 - 0
src/headless/plugins/disco/entities.js

@@ -0,0 +1,23 @@
+import DiscoEntity from './entity.js';
+import log from "@converse/headless/log.js";
+import { Collection } from "@converse/skeletor/src/collection";
+
+
+const DiscoEntities = Collection.extend({
+    model: DiscoEntity,
+
+    fetchEntities () {
+        return new Promise((resolve, reject) => {
+            this.fetch({
+                add: true,
+                success: resolve,
+                error (m, e) {
+                    log.error(e);
+                    reject (new Error("Could not fetch disco entities"));
+                }
+            });
+        });
+    }
+});
+
+export default DiscoEntities;

+ 208 - 0
src/headless/plugins/disco/entity.js

@@ -0,0 +1,208 @@
+import log from "@converse/headless/log.js";
+import sizzle from "sizzle";
+import { Collection } from "@converse/skeletor/src/collection";
+import { Model } from '@converse/skeletor/src/model.js';
+import { _converse, api, converse } from "@converse/headless/core.js";
+
+const { Strophe, utils } = converse.env;
+
+/**
+ * @class
+ * @namespace _converse.DiscoEntity
+ * @memberOf _converse
+ *
+ * A Disco Entity is a JID addressable entity that can be queried for features.
+ *
+ * See XEP-0030: https://xmpp.org/extensions/xep-0030.html
+ */
+const DiscoEntity = Model.extend({
+    idAttribute: 'jid',
+
+    initialize (attrs, options) {
+        this.waitUntilFeaturesDiscovered = utils.getResolveablePromise();
+
+        this.dataforms = new Collection();
+        let id = `converse.dataforms-${this.get('jid')}`;
+        this.dataforms.browserStorage = _converse.createStore(id, 'session');
+
+        this.features = new Collection();
+        id = `converse.features-${this.get('jid')}`;
+        this.features.browserStorage = _converse.createStore(id, 'session');
+        this.listenTo(this.features, 'add', this.onFeatureAdded)
+
+        this.fields = new Collection();
+        id = `converse.fields-${this.get('jid')}`;
+        this.fields.browserStorage = _converse.createStore(id, 'session');
+        this.listenTo(this.fields, 'add', this.onFieldAdded)
+
+        this.identities = new Collection();
+        id = `converse.identities-${this.get('jid')}`;
+        this.identities.browserStorage = _converse.createStore(id, 'session');
+        this.fetchFeatures(options);
+
+        this.items = new _converse.DiscoEntities();
+        id = `converse.disco-items-${this.get('jid')}`;
+        this.items.browserStorage = _converse.createStore(id, 'session');
+        this.items.fetch();
+    },
+
+    /**
+     * Returns a Promise which resolves with a map indicating
+     * whether a given identity is provided by this entity.
+     * @private
+     * @method _converse.DiscoEntity#getIdentity
+     * @param { String } category - The identity category
+     * @param { String } type - The identity type
+     */
+    async getIdentity (category, type) {
+        await this.waitUntilFeaturesDiscovered;
+        return this.identities.findWhere({
+            'category': category,
+            'type': type
+        });
+    },
+
+    /**
+     * Returns a Promise which resolves with a map indicating
+     * whether a given feature is supported.
+     * @private
+     * @method _converse.DiscoEntity#hasFeature
+     * @param { String } feature - The feature that might be supported.
+     */
+    async hasFeature (feature) {
+        await this.waitUntilFeaturesDiscovered
+        if (this.features.findWhere({'var': feature})) {
+            return this;
+        }
+    },
+
+    onFeatureAdded (feature) {
+        feature.entity = this;
+        /**
+         * Triggered when Converse has learned of a service provided by the XMPP server.
+         * See XEP-0030.
+         * @event _converse#serviceDiscovered
+         * @type { Model }
+         * @example _converse.api.listen.on('featuresDiscovered', feature => { ... });
+         */
+        api.trigger('serviceDiscovered', feature);
+    },
+
+    onFieldAdded (field) {
+        field.entity = this;
+        /**
+         * Triggered when Converse has learned of a disco extension field.
+         * See XEP-0030.
+         * @event _converse#discoExtensionFieldDiscovered
+         * @example _converse.api.listen.on('discoExtensionFieldDiscovered', () => { ... });
+         */
+        api.trigger('discoExtensionFieldDiscovered', field);
+    },
+
+    async fetchFeatures (options) {
+        if (options.ignore_cache) {
+            this.queryInfo();
+        } else {
+            const store_id = this.features.browserStorage.name;
+            const result = await this.features.browserStorage.store.getItem(store_id);
+            if (result && result.length === 0 || result === null) {
+                this.queryInfo();
+            } else {
+                this.features.fetch({
+                    add: true,
+                    success: () => {
+                        this.waitUntilFeaturesDiscovered.resolve(this);
+                        this.trigger('featuresDiscovered');
+                    }
+                });
+                this.identities.fetch({add: true});
+            }
+        }
+    },
+
+    async queryInfo () {
+        let stanza;
+        try {
+            stanza = await api.disco.info(this.get('jid'), null);
+        } catch (iq) {
+            iq === null ? log.error(`Timeout for disco#info query for ${this.get('jid')}`) : log.error(iq);
+            this.waitUntilFeaturesDiscovered.resolve(this);
+            return;
+        }
+        this.onInfo(stanza);
+    },
+
+    onDiscoItems (stanza) {
+        sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"] item`, stanza).forEach(item => {
+            if (item.getAttribute("node")) {
+                // XXX: Ignore nodes for now.
+                // See: https://xmpp.org/extensions/xep-0030.html#items-nodes
+                return;
+            }
+            const jid = item.getAttribute('jid');
+            if (this.items.get(jid) === undefined) {
+                const entity = _converse.disco_entities.get(jid);
+                if (entity) {
+                    this.items.add(entity);
+                } else {
+                    this.items.create({'jid': jid});
+                }
+            }
+        });
+    },
+
+    async queryForItems () {
+        if (this.identities.where({'category': 'server'}).length === 0) {
+            // Don't fetch features and items if this is not a
+            // server or a conference component.
+            return;
+        }
+        const stanza = await api.disco.items(this.get('jid'));
+        this.onDiscoItems(stanza);
+    },
+
+    onInfo (stanza) {
+        Array.from(stanza.querySelectorAll('identity')).forEach(identity => {
+            this.identities.create({
+                'category': identity.getAttribute('category'),
+                'type': identity.getAttribute('type'),
+                'name': identity.getAttribute('name')
+            });
+        });
+
+        sizzle(`x[type="result"][xmlns="${Strophe.NS.XFORM}"]`, stanza).forEach(form => {
+            const data = {};
+            sizzle('field', form).forEach(field => {
+                data[field.getAttribute('var')] = {
+                    'value': field.querySelector('value')?.textContent,
+                    'type': field.getAttribute('type')
+                };
+            });
+            this.dataforms.create(data);
+        });
+
+        if (stanza.querySelector(`feature[var="${Strophe.NS.DISCO_ITEMS}"]`)) {
+            this.queryForItems();
+        }
+        Array.from(stanza.querySelectorAll('feature')).forEach(feature => {
+            this.features.create({
+                'var': feature.getAttribute('var'),
+                'from': stanza.getAttribute('from')
+            });
+        });
+
+        // XEP-0128 Service Discovery Extensions
+        sizzle('x[type="result"][xmlns="jabber:x:data"] field', stanza).forEach(field => {
+            this.fields.create({
+                'var': field.getAttribute('var'),
+                'value': field.querySelector('value')?.textContent,
+                'from': stanza.getAttribute('from')
+            });
+        });
+
+        this.waitUntilFeaturesDiscovered.resolve(this);
+        this.trigger('featuresDiscovered');
+    }
+});
+
+export default DiscoEntity;

+ 60 - 0
src/headless/plugins/disco/index.js

@@ -0,0 +1,60 @@
+/**
+ * @copyright The Converse.js contributors
+ * @license Mozilla Public License (MPLv2)
+ * @description Converse plugin which add support for XEP-0030: Service Discovery
+ */
+import DiscoEntities from './entities.js';
+import DiscoEntity from './entity.js';
+import { _converse, api, converse } from '@converse/headless/core.js';
+import { initializeDisco, initStreamFeatures, notifyStreamFeaturesAdded, populateStreamFeatures } from './utils.js';
+import disco_api from './api.js';
+
+const { Strophe } = converse.env;
+
+converse.plugins.add('converse-disco', {
+    initialize () {
+        Object.assign(api, disco_api);
+
+        api.promises.add('discoInitialized');
+        api.promises.add('streamFeaturesAdded');
+
+        _converse.DiscoEntity = DiscoEntity;
+        _converse.DiscoEntities = DiscoEntities;
+
+        _converse.disco = {
+            _identities: [],
+            _features: []
+        };
+
+        api.listen.on('userSessionInitialized', async () => {
+            initStreamFeatures();
+            if (_converse.connfeedback.get('connection_status') === Strophe.Status.ATTACHED) {
+                // When re-attaching to a BOSH session, we fetch the stream features from the cache.
+                await new Promise((success, error) => _converse.stream_features.fetch({ success, error }));
+                notifyStreamFeaturesAdded();
+            }
+        });
+        api.listen.on('beforeResourceBinding', populateStreamFeatures);
+        api.listen.on('reconnected', initializeDisco);
+        api.listen.on('connected', initializeDisco);
+
+        api.listen.on('beforeTearDown', async () => {
+            api.promises.add('streamFeaturesAdded');
+            if (_converse.stream_features) {
+                await _converse.stream_features.clearStore();
+                delete _converse.stream_features;
+            }
+        });
+
+        api.listen.on('clearSession', () => {
+            if (_converse.shouldClearCache() && _converse.disco_entities) {
+                Array.from(_converse.disco_entities.models).forEach(e => e.features.clearStore());
+                Array.from(_converse.disco_entities.models).forEach(e => e.identities.clearStore());
+                Array.from(_converse.disco_entities.models).forEach(e => e.dataforms.clearStore());
+                Array.from(_converse.disco_entities.models).forEach(e => e.fields.clearStore());
+                _converse.disco_entities.clearStore();
+                delete _converse.disco_entities;
+            }
+        });
+    }
+});

+ 0 - 0
spec/disco.js → src/headless/plugins/disco/tests/disco.js


+ 125 - 0
src/headless/plugins/disco/utils.js

@@ -0,0 +1,125 @@
+import { _converse, api, converse } from "@converse/headless/core.js";
+import { Collection } from "@converse/skeletor/src/collection";
+
+const { Strophe, $iq } = converse.env;
+
+
+function onDiscoInfoRequest (identities, features, stanza) {
+    const node = stanza.getElementsByTagName('query')[0].getAttribute('node');
+    const attrs = {xmlns: Strophe.NS.DISCO_INFO};
+    if (node) { attrs.node = node; }
+
+    const iqresult = $iq({'type': 'result', 'id': stanza.getAttribute('id')});
+    const from = stanza.getAttribute('from');
+    if (from !== null) {
+        iqresult.attrs({'to': from});
+    }
+    iqresult.c('query', attrs);
+    _converse.disco._identities.forEach(identity => {
+        const attrs = {
+            'category': identity.category,
+            'type': identity.type
+        };
+        if (identity.name) {
+            attrs.name = identity.name;
+        }
+        if (identity.lang) {
+            attrs['xml:lang'] = identity.lang;
+        }
+        iqresult.c('identity', attrs).up();
+    });
+    _converse.disco._features.forEach(feature => iqresult.c('feature', {'var': feature}).up());
+    api.send(iqresult.tree());
+    return true;
+}
+
+
+function addClientFeatures () {
+    // See https://xmpp.org/registrar/disco-categories.html
+    api.disco.own.identities.add('client', 'web', 'Converse');
+
+    api.disco.own.features.add(Strophe.NS.CHATSTATES);
+    api.disco.own.features.add(Strophe.NS.DISCO_INFO);
+    api.disco.own.features.add(Strophe.NS.ROSTERX); // Limited support
+    if (api.settings.get("message_carbons")) {
+        api.disco.own.features.add(Strophe.NS.CARBONS);
+    }
+    /**
+     * Triggered in converse-disco once the core disco features of
+     * Converse have been added.
+     * @event _converse#addClientFeatures
+     * @example _converse.api.listen.on('addClientFeatures', () => { ... });
+     */
+    api.trigger('addClientFeatures');
+    return this;
+}
+
+
+export async function initializeDisco () {
+    addClientFeatures();
+    _converse.connection.addHandler(
+        stanza => onDiscoInfoRequest(stanza),
+        Strophe.NS.DISCO_INFO,
+        'iq', 'get', null, null
+    );
+
+    _converse.disco_entities = new _converse.DiscoEntities();
+    const id = `converse.disco-entities-${_converse.bare_jid}`;
+    _converse.disco_entities.browserStorage = _converse.createStore(id, 'session');
+    const collection = await _converse.disco_entities.fetchEntities();
+    if (collection.length === 0 || !collection.get(_converse.domain)) {
+        // If we don't have an entity for our own XMPP server,
+        // create one.
+        _converse.disco_entities.create({'jid': _converse.domain});
+    }
+    /**
+     * Triggered once the `converse-disco` plugin has been initialized and the
+     * `_converse.disco_entities` collection will be available and populated with at
+     * least the service discovery features of the user's own server.
+     * @event _converse#discoInitialized
+     * @example _converse.api.listen.on('discoInitialized', () => { ... });
+     */
+    api.trigger('discoInitialized');
+}
+
+export function initStreamFeatures () {
+    // Initialize the stream_features collection, and if we're
+    // re-attaching to a pre-existing BOSH session, we restore the
+    // features from cache.
+    // Otherwise the features will be created once we've received them
+    // from the server (see populateStreamFeatures).
+    if (!_converse.stream_features) {
+        const bare_jid = Strophe.getBareJidFromJid(_converse.jid);
+        const id = `converse.stream-features-${bare_jid}`;
+        api.promises.add('streamFeaturesAdded');
+        _converse.stream_features = new Collection();
+        _converse.stream_features.browserStorage = _converse.createStore(id, "session");
+    }
+}
+
+export function notifyStreamFeaturesAdded () {
+    /**
+     * Triggered as soon as the stream features are known.
+     * If you want to check whether a stream feature is supported before proceeding,
+     * then you'll first want to wait for this event.
+     * @event _converse#streamFeaturesAdded
+     * @example _converse.api.listen.on('streamFeaturesAdded', () => { ... });
+     */
+    api.trigger('streamFeaturesAdded');
+}
+
+export function populateStreamFeatures () {
+    // Strophe.js sets the <stream:features> element on the
+    // Strophe.Connection instance (_converse.connection).
+    //
+    // Once this is done, we populate the _converse.stream_features collection
+    // and trigger streamFeaturesAdded.
+    initStreamFeatures();
+    Array.from(_converse.connection.features.childNodes).forEach(feature => {
+        _converse.stream_features.create({
+            'name': feature.nodeName,
+            'xmlns': feature.getAttribute('xmlns')
+        });
+    });
+    notifyStreamFeaturesAdded();
+}

+ 1 - 1
src/headless/plugins/mam/index.js

@@ -4,7 +4,7 @@
  * @license Mozilla Public License (MPLv2)
  */
 import mam_api from './api.js';
-import '../disco';
+import '../disco/index.js';
 import {
     onMAMError,
     onMAMPreferences,

+ 2 - 1
src/headless/plugins/muc/affiliations/utils.js

@@ -2,9 +2,10 @@
  * @copyright The Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
+import difference from 'lodash-es/difference';
+import indexOf from 'lodash-es/indexOf';
 import log from "@converse/headless/log";
 import { api, converse } from '@converse/headless/core.js';
-import { difference, indexOf } from 'lodash-es';
 import { parseMemberListIQ } from '../parsers.js';
 
 const { Strophe, $iq, u } = converse.env;

+ 5 - 6
src/headless/plugins/muc/index.js

@@ -1,24 +1,23 @@
 /**
- * @module converse-muc
  * @copyright The Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  * @description Implements the non-view logic for XEP-0045 Multi-User Chat
  */
 import '../chat/index.js';
-import '../disco';
+import '../disco/index.js';
 import '../emoji/index.js';
 import ChatRoomMessageMixin from './message.js';
 import ChatRoomMixin from './muc.js';
 import ChatRoomOccupant from './occupant.js';
 import ChatRoomOccupants from './occupants.js';
-import log from '../../log';
-import muc_api from './api.js';
 import affiliations_api from './affiliations/api.js';
-import { computeAffiliationsDelta } from './affiliations/utils.js';
+import isObject from 'lodash-es/isObject';
+import log from '@converse/headless/log';
+import muc_api from './api.js';
 import u from '../../utils/form';
 import { Collection } from '@converse/skeletor/src/collection';
 import { _converse, api, converse } from '../../core.js';
-import { isObject } from 'lodash-es';
+import { computeAffiliationsDelta } from './affiliations/utils.js';
 
 export const ROLES = ['moderator', 'participant', 'visitor'];
 export const AFFILIATIONS = ['owner', 'admin', 'member', 'outcast', 'none'];

+ 1 - 1
src/headless/plugins/pubsub.js

@@ -3,7 +3,7 @@
  * @copyright The Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
-import "./disco";
+import "./disco/index.js";
 import { _converse, api, converse } from "../core.js";
 import log from "../log.js";
 

+ 2 - 1
src/headless/plugins/status.js

@@ -3,7 +3,8 @@
  * @copyright The Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
-import { isNaN, isObject } from "lodash-es";
+import isNaN from "lodash-es/isNaN";
+import isObject from "lodash-es/isObject";
 import { Model } from '@converse/skeletor/src/model.js';
 import { _converse, api, converse } from "@converse/headless/core";
 

+ 6 - 3
src/headless/utils/core.js

@@ -3,11 +3,14 @@
  * @license Mozilla Public License (MPLv2)
  * @description This is the core utilities module.
  */
-import { Strophe } from 'strophe.js/src/strophe';
-import { Model } from '@converse/skeletor/src/model.js';
-import { compact, last, isElement, isObject } from "lodash-es";
+import compact from "lodash-es/compact";
+import isElement from "lodash-es/isElement";
+import isObject from "lodash-es/isObject";
+import last from "lodash-es/last";
 import log from "@converse/headless/log";
 import sizzle from "sizzle";
+import { Model } from '@converse/skeletor/src/model.js';
+import { Strophe } from 'strophe.js/src/strophe';
 
 /**
  * The utils object

+ 3 - 2
src/modals/add-contact.js

@@ -1,9 +1,10 @@
 import 'shared/autocomplete/index.js';
 import BootstrapModal from "./base.js";
+import compact from 'lodash-es/compact';
+import debounce from 'lodash-es/debounce';
 import tpl_add_contact_modal from "./templates/add-contact.js";
-import { __ } from '../i18n';
+import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
-import { compact, debounce } from "lodash-es";
 
 const { Strophe } = converse.env;
 const u = converse.env.utils;

+ 1 - 1
src/plugins/bookmark-views/mixins.js

@@ -1,7 +1,7 @@
+import invokeMap from 'lodash-es/invokeMap';
 import { Model } from '@converse/skeletor/src/model.js';
 import { __ } from 'i18n';
 import { _converse, api, converse } from '@converse/headless/core';
-import { invokeMap } from 'lodash-es';
 
 const { u } = converse.env;
 

+ 1 - 1
src/plugins/dragresize/mixin.js

@@ -1,6 +1,6 @@
+import debounce from 'lodash-es/debounce';
 import { _converse } from '@converse/headless/core';
 import { applyDragResistance } from './utils.js';
-import { debounce } from 'lodash-es';
 
 const DragResizableMixin = {
     initDragResize () {

+ 1 - 1
src/plugins/minimize/index.js

@@ -5,6 +5,7 @@
  */
 import './components/minimized-chat.js';
 import 'plugins/chatview/index.js';
+import debounce from 'lodash-es/debounce';
 import MinimizedChats from './view.js';
 import MinimizedChatsToggle from './toggle.js';
 import { _converse, api, converse } from '@converse/headless/core';
@@ -16,7 +17,6 @@ import {
     onMinimizedChanged,
     trimChats
 } from './utils.js';
-import { debounce } from 'lodash-es';
 
 import './styles/minimize.scss';
 

+ 1 - 1
src/plugins/muc-views/modals/muc-list.js

@@ -1,4 +1,5 @@
 import BootstrapModal from "modals/base.js";
+import head from "lodash-es/head";
 import log from "@converse/headless/log";
 import tpl_list_chatrooms_modal from "../templates/muc-list.js";
 import tpl_muc_description from "../templates/muc-description.js";
@@ -6,7 +7,6 @@ import tpl_spinner from "templates/spinner.js";
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
 import { getAttributes } from '@converse/headless/shared/parsers';
-import { head } from "lodash-es";
 
 const { Strophe, $iq, sizzle } = converse.env;
 const u = converse.env.utils;

+ 1 - 1
src/plugins/register/panel.js

@@ -1,4 +1,5 @@
 import log from "@converse/headless/log";
+import pick from "lodash-es/pick";
 import tpl_form_input from "templates/form_input.js";
 import tpl_form_url from "templates/form_url.js";
 import tpl_form_username from "templates/form_username.js";
@@ -8,7 +9,6 @@ import utils from "@converse/headless/utils/form";
 import { ElementView } from "@converse/skeletor/src/element";
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
-import { pick } from "lodash-es";
 import { render } from 'lit-html';
 
 // Strophe methods for building stanzas

+ 1 - 1
src/plugins/rosterview/filterview.js

@@ -1,8 +1,8 @@
+import debounce from "lodash-es/debounce";
 import tpl_roster_filter from "./templates/roster_filter.js";
 import { ElementView } from '@converse/skeletor/src/element.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { _converse, api } from "@converse/headless/core";
-import { debounce } from "lodash-es";
 import { render } from 'lit-html';
 
 export const RosterFilter = Model.extend({

+ 1 - 1
src/utils/html.js

@@ -4,6 +4,7 @@
  * @description This is the DOM/HTML utilities module.
  */
 import URI from "urijs";
+import isFunction from "lodash-es/isFunction";
 import log from '@converse/headless/log';
 import tpl_audio from  "../templates/audio.js";
 import tpl_file from "../templates/file.js";
@@ -20,7 +21,6 @@ import tpl_video from "../templates/video.js";
 import u from "../headless/utils/core";
 import { api, converse } from  "@converse/headless/core";
 import { html, render } from "lit-html";
-import { isFunction } from "lodash-es";
 
 const { sizzle } = converse.env;