소스 검색

Move functions out of core.js

JC Brand 4 년 전
부모
커밋
60826ac6c6

+ 11 - 34
src/converse.js

@@ -1,28 +1,30 @@
 /**
 /**
  * @description Converse.js (A browser based XMPP chat client)
  * @description Converse.js (A browser based XMPP chat client)
- * @copyright 2020, The Converse developers
+ * @copyright 2021, The Converse developers
  * @license Mozilla Public License (MPLv2)
  * @license Mozilla Public License (MPLv2)
  */
  */
 
 
-
 import "@converse/headless/headless";
 import "@converse/headless/headless";
 import "i18n";
 import "i18n";
 import "shared/registry.js";
 import "shared/registry.js";
+import { CustomElement } from 'shared/components/element';
+import { VIEW_PLUGINS } from './shared/constants.js';
+import { _converse, converse } from "@converse/headless/core";
 
 
 import 'shared/styles/index.scss';
 import 'shared/styles/index.scss';
 
 
-/* START: Removable components
- * --------------------
- * Any of the following components may be removed if they're not needed.
+/* START: Removable plugins
+ * ------------------------
+ * Any of the following plugin imports may be removed if the plugin is not needed
  */
  */
-import "./plugins/bookmark-views/index.js";       // Views for XEP-0048 Bookmarks
+import "./plugins/bookmark-views/index.js"; // Views for XEP-0048 Bookmarks
 import "./plugins/chatview/index.js";       // Renders standalone chat boxes for single user chat
 import "./plugins/chatview/index.js";       // Renders standalone chat boxes for single user chat
 import "./plugins/controlbox/index.js";     // The control box
 import "./plugins/controlbox/index.js";     // The control box
 import "./plugins/dragresize/index.js";     // Allows chat boxes to be resized by dragging them
 import "./plugins/dragresize/index.js";     // Allows chat boxes to be resized by dragging them
 import "./plugins/fullscreen/index.js";
 import "./plugins/fullscreen/index.js";
 import "./plugins/headlines-view/index.js";
 import "./plugins/headlines-view/index.js";
 import "./plugins/mam-views/index.js";
 import "./plugins/mam-views/index.js";
-import "./plugins/minimize/index.js";             // Allows chat boxes to be minimized
+import "./plugins/minimize/index.js";       // Allows chat boxes to be minimized
 import "./plugins/muc-views/index.js";      // Views related to MUC
 import "./plugins/muc-views/index.js";      // Views related to MUC
 import "./plugins/notifications/index.js";
 import "./plugins/notifications/index.js";
 import "./plugins/omemo/index.js";
 import "./plugins/omemo/index.js";
@@ -35,41 +37,16 @@ import "./plugins/rosterview/index.js";
 import "./plugins/singleton.js";
 import "./plugins/singleton.js";
 /* END: Removable components */
 /* END: Removable components */
 
 
-import { _converse, converse } from "@converse/headless/core";
-import { CustomElement } from 'shared/components/element';
 
 
 _converse.CustomElement = CustomElement;
 _converse.CustomElement = CustomElement;
 
 
-const WHITELISTED_PLUGINS = [
-    'converse-bookmark-views',
-    'converse-chatboxviews',
-    'converse-chatview',
-    'converse-controlbox',
-    'converse-dragresize',
-    'converse-fullscreen',
-    'converse-headlines-view',
-    'converse-mam-views',
-    'converse-minimize',
-    'converse-modal',
-    'converse-muc-views',
-    'converse-notification',
-    'converse-omemo',
-    'converse-profile',
-    'converse-push',
-    'converse-register',
-    'converse-roomslist',
-    'converse-rootview',
-    'converse-rosterview',
-    'converse-singleton'
-];
-
 const initialize = converse.initialize;
 const initialize = converse.initialize;
 
 
 converse.initialize = function (settings, callback) {
 converse.initialize = function (settings, callback) {
     if (Array.isArray(settings.whitelisted_plugins)) {
     if (Array.isArray(settings.whitelisted_plugins)) {
-        settings.whitelisted_plugins = settings.whitelisted_plugins.concat(WHITELISTED_PLUGINS);
+        settings.whitelisted_plugins = settings.whitelisted_plugins.concat(VIEW_PLUGINS);
     } else {
     } else {
-        settings.whitelisted_plugins = WHITELISTED_PLUGINS;
+        settings.whitelisted_plugins = VIEW_PLUGINS;
     }
     }
     return initialize(settings, callback);
     return initialize(settings, callback);
 }
 }

+ 18 - 347
src/headless/core.js

@@ -2,38 +2,31 @@
  * @copyright The Converse.js contributors
  * @copyright The Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  * @license Mozilla Public License (MPLv2)
  */
  */
-import Storage from '@converse/skeletor/src/storage.js';
 import URI from 'urijs';
 import URI from 'urijs';
 import _converse from '@converse/headless/shared/_converse';
 import _converse from '@converse/headless/shared/_converse';
 import advancedFormat from 'dayjs/plugin/advancedFormat';
 import advancedFormat from 'dayjs/plugin/advancedFormat';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
-import debounce from 'lodash-es/debounce';
 import i18n from '@converse/headless/shared/i18n';
 import i18n from '@converse/headless/shared/i18n';
 import invoke from 'lodash-es/invoke';
 import invoke from 'lodash-es/invoke';
 import isFunction from 'lodash-es/isFunction';
 import isFunction from 'lodash-es/isFunction';
 import isObject from 'lodash-es/isObject';
 import isObject from 'lodash-es/isObject';
-import localDriver from 'localforage-webextensionstorage-driver/local';
 import log from '@converse/headless/log.js';
 import log from '@converse/headless/log.js';
 import pluggable from 'pluggable.js/src/pluggable.js';
 import pluggable from 'pluggable.js/src/pluggable.js';
 import settings_api from '@converse/headless/api/settings.js';
 import settings_api from '@converse/headless/api/settings.js';
 import sizzle from 'sizzle';
 import sizzle from 'sizzle';
-import syncDriver from 'localforage-webextensionstorage-driver/sync';
-import u from '@converse/headless/utils/core';
-import { CORE_PLUGINS } from '@converse/headless/shared/constants.js';
+import u, { setUnloadEvent, replacePromise } from '@converse/headless/utils/core.js';
 import { Collection } from "@converse/skeletor/src/collection";
 import { Collection } from "@converse/skeletor/src/collection";
 import { Connection, MockConnection } from '@converse/headless/shared/connection.js';
 import { Connection, MockConnection } from '@converse/headless/shared/connection.js';
-import { initStorage } from '@converse/headless/shared/utils.js';
 import {
 import {
     clearUserSettings,
     clearUserSettings,
     getUserSettings,
     getUserSettings,
     initAppSettings,
     initAppSettings,
     updateUserSettings
     updateUserSettings
-} from '@converse/headless/shared/settings';
+} from '@converse/headless/shared/settings.js';
 import { Events } from '@converse/skeletor/src/events.js';
 import { Events } from '@converse/skeletor/src/events.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
 import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
 import { TimeoutError } from '@converse/headless/shared/errors';
 import { TimeoutError } from '@converse/headless/shared/errors';
-import { createStore, replacePromise } from '@converse/headless/shared/utils';
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
 import { html } from 'lit';
 import { html } from 'lit';
 import { sprintf } from 'sprintf-js';
 import { sprintf } from 'sprintf-js';
@@ -41,6 +34,16 @@ import { sprintf } from 'sprintf-js';
 export { _converse };
 export { _converse };
 export { i18n };
 export { i18n };
 
 
+import {
+    attemptNonPreboundSession,
+    cleanup,
+    initClientConfig,
+    initPlugins,
+    initSession,
+    initSessionStorage,
+    registerGlobalEventHandlers
+} from './utils/init.js';
+
 dayjs.extend(advancedFormat);
 dayjs.extend(advancedFormat);
 
 
 // Add Strophe Namespaces
 // Add Strophe Namespaces
@@ -646,115 +649,6 @@ _converse.isUniView = function () {
 };
 };
 
 
 
 
-async function initSessionStorage () {
-    await Storage.sessionStorageInitialized;
-    _converse.storage = {
-        'session': Storage.localForage.createInstance({
-            'name': _converse.isTestEnv() ? 'converse-test-session' : 'converse-session',
-            'description': 'sessionStorage instance',
-            'driver': ['sessionStorageWrapper']
-        })
-    };
-}
-
-function initPersistentStorage (store_name) {
-    if (api.settings.get('persistent_store') === 'sessionStorage') {
-        return;
-    } else if (_converse.api.settings.get("persistent_store") === 'BrowserExtLocal') {
-        Storage.localForage.defineDriver(localDriver).then(
-            () => Storage.localForage.setDriver('webExtensionLocalStorage')
-        );
-        _converse.storage['persistent'] = Storage.localForage;
-        return;
-
-    } else if (_converse.api.settings.get("persistent_store") === 'BrowserExtSync') {
-        Storage.localForage.defineDriver(syncDriver).then(
-            () => Storage.localForage.setDriver('webExtensionSyncStorage')
-        );
-        _converse.storage['persistent'] = Storage.localForage;
-        return;
-    }
-
-    const config = {
-        'name': _converse.isTestEnv() ? 'converse-test-persistent' : 'converse-persistent',
-        'storeName': store_name
-    }
-    if (_converse.api.settings.get("persistent_store") === 'localStorage') {
-        config['description'] = 'localStorage instance';
-        config['driver'] = [Storage.localForage.LOCALSTORAGE];
-    } else if (_converse.api.settings.get("persistent_store") === 'IndexedDB') {
-        config['description'] = 'indexedDB instance';
-        config['driver'] = [Storage.localForage.INDEXEDDB];
-    }
-    _converse.storage['persistent'] = Storage.localForage.createInstance(config);
-}
-
-function initPlugins () {
-    // If initialize gets called a second time (e.g. during tests), then we
-    // need to re-apply all plugins (for a new converse instance), and we
-    // therefore need to clear this array that prevents plugins from being
-    // initialized twice.
-    // If initialize is called for the first time, then this array is empty
-    // in any case.
-    _converse.pluggable.initialized_plugins = [];
-    const whitelist = CORE_PLUGINS.concat(_converse.api.settings.get("whitelisted_plugins"));
-
-    if (_converse.api.settings.get("singleton")) {
-        [
-            'converse-bookmarks',
-            'converse-controlbox',
-            'converse-headline',
-            'converse-register'
-        ].forEach(name => _converse.api.settings.get("blacklisted_plugins").push(name));
-    }
-
-    _converse.pluggable.initializePlugins(
-        { _converse },
-        whitelist,
-        _converse.api.settings.get("blacklisted_plugins")
-    );
-
-    /**
-     * Triggered once all plugins have been initialized. This is a useful event if you want to
-     * register event handlers but would like your own handlers to be overridable by
-     * plugins. In that case, you need to first wait until all plugins have been
-     * initialized, so that their overrides are active. One example where this is used
-     * is in [converse-notifications.js](https://github.com/jcbrand/converse.js/blob/master/src/converse-notification.js)`.
-     *
-     * Also available as an [ES2015 Promise](http://es6-features.org/#PromiseUsage)
-     * which can be listened to with `_converse.api.waitUntil`.
-     *
-     * @event _converse#pluginsInitialized
-     * @memberOf _converse
-     * @example _converse.api.listen.on('pluginsInitialized', () => { ... });
-     */
-    _converse.api.trigger('pluginsInitialized');
-}
-
-
-async function initClientConfig () {
-    /* The client config refers to configuration of the client which is
-     * independent of any particular user.
-     * What this means is that config values need to persist across
-     * user sessions.
-     */
-    const id = 'converse.client-config';
-    _converse.config = new Model({ id, 'trusted': true });
-    _converse.config.browserStorage = createStore(id, "session");
-    await new Promise(r => _converse.config.fetch({'success': r, 'error': r}));
-    /**
-     * Triggered once the XMPP-client configuration has been initialized.
-     * The client configuration is independent of any particular and its values
-     * persist across user sessions.
-     *
-     * @event _converse#clientConfigInitialized
-     * @example
-     * _converse.api.listen.on('clientConfigInitialized', () => { ... });
-     */
-    _converse.api.trigger('clientConfigInitialized');
-}
-
-
 export async function tearDown () {
 export async function tearDown () {
     await _converse.api.trigger('beforeTearDown', {'synchronous': true});
     await _converse.api.trigger('beforeTearDown', {'synchronous': true});
     window.removeEventListener('click', _converse.onUserActivity);
     window.removeEventListener('click', _converse.onUserActivity);
@@ -768,65 +662,6 @@ export async function tearDown () {
 }
 }
 
 
 
 
-async function attemptNonPreboundSession (credentials, automatic) {
-    const { api } = _converse;
-    if (api.settings.get("authentication") === _converse.LOGIN) {
-        // XXX: If EITHER ``keepalive`` or ``auto_login`` is ``true`` and
-        // ``authentication`` is set to ``login``, then Converse will try to log the user in,
-        // since we don't have a way to distinguish between wether we're
-        // restoring a previous session (``keepalive``) or whether we're
-        // automatically setting up a new session (``auto_login``).
-        // So we can't do the check (!automatic || _converse.api.settings.get("auto_login")) here.
-        if (credentials) {
-            connect(credentials);
-        } else if (_converse.api.settings.get("credentials_url")) {
-            // We give credentials_url preference, because
-            // _converse.connection.pass might be an expired token.
-            connect(await getLoginCredentials());
-        } else if (_converse.jid && (_converse.api.settings.get("password") || _converse.connection.pass)) {
-            connect();
-        } else if (!_converse.isTestEnv() && 'credentials' in navigator) {
-            connect(await getLoginCredentialsFromBrowser());
-        } else {
-            !_converse.isTestEnv() && log.warn("attemptNonPreboundSession: Couldn't find credentials to log in with");
-        }
-    } else if ([_converse.ANONYMOUS, _converse.EXTERNAL].includes(_converse.api.settings.get("authentication")) && (!automatic || _converse.api.settings.get("auto_login"))) {
-        connect();
-    }
-}
-
-
-function connect (credentials) {
-    if ([_converse.ANONYMOUS, _converse.EXTERNAL].includes(_converse.api.settings.get("authentication"))) {
-        if (!_converse.jid) {
-            throw new Error("Config Error: when using anonymous login " +
-                "you need to provide the server's domain via the 'jid' option. " +
-                "Either when calling converse.initialize, or when calling " +
-                "_converse.api.user.login.");
-        }
-        if (!_converse.connection.reconnecting) {
-            _converse.connection.reset();
-        }
-        _converse.connection.connect(_converse.jid.toLowerCase());
-    } else if (_converse.api.settings.get("authentication") === _converse.LOGIN) {
-        const password = credentials ? credentials.password : (_converse.connection?.pass || _converse.api.settings.get("password"));
-        if (!password) {
-            if (_converse.api.settings.get("auto_login")) {
-                throw new Error("autoLogin: If you use auto_login and "+
-                    "authentication='login' then you also need to provide a password.");
-            }
-            _converse.connection.setDisconnectionCause(Strophe.Status.AUTHFAIL, undefined, true);
-            _converse.api.connection.disconnect();
-            return;
-        }
-        if (!_converse.connection.reconnecting) {
-            _converse.connection.reset();
-        }
-        _converse.connection.connect(_converse.jid, password);
-    }
-}
-
-
 _converse.shouldClearCache = () => (
 _converse.shouldClearCache = () => (
     !_converse.config.get('trusted') ||
     !_converse.config.get('trusted') ||
     api.settings.get('clear_cache_on_logout') ||
     api.settings.get('clear_cache_on_logout') ||
@@ -890,63 +725,6 @@ _converse.initConnection = function () {
 }
 }
 
 
 
 
-async function initSession (jid) {
-    const is_shared_session = api.settings.get('connection_options').worker;
-
-    const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase();
-    const id = `converse.session-${bare_jid}`;
-    if (_converse.session?.get('id') !== id) {
-        initPersistentStorage(bare_jid);
-
-        _converse.session = new Model({ id });
-        initStorage(_converse.session, id, is_shared_session ? "persistent" : "session");
-        await new Promise(r => _converse.session.fetch({'success': r, 'error': r}));
-
-        if (!is_shared_session && _converse.session.get('active')) {
-            // If the `active` flag is set, it means this tab was cloned from
-            // another (e.g. via middle-click), and its session data was copied over.
-            _converse.session.clear();
-            _converse.session.save({id});
-        }
-        saveJIDtoSession(jid);
-        /**
-         * Triggered once the user's session has been initialized. The session is a
-         * cache which stores information about the user's current session.
-         * @event _converse#userSessionInitialized
-         * @memberOf _converse
-         */
-        _converse.api.trigger('userSessionInitialized');
-    } else {
-        saveJIDtoSession(jid);
-    }
-}
-
-
-function saveJIDtoSession (jid) {
-    jid = _converse.session.get('jid') || jid;
-    if (_converse.api.settings.get("authentication") !== _converse.ANONYMOUS && !Strophe.getResourceFromJid(jid)) {
-        jid = jid.toLowerCase() + Connection.generateResource();
-    }
-    _converse.jid = jid;
-    _converse.bare_jid = Strophe.getBareJidFromJid(jid);
-    _converse.resource = Strophe.getResourceFromJid(jid);
-    _converse.domain = Strophe.getDomainFromJid(jid);
-    _converse.session.save({
-       'jid': jid,
-       'bare_jid': _converse.bare_jid,
-       'resource': _converse.resource,
-       'domain': _converse.domain,
-        // We use the `active` flag to determine whether we should use the values from sessionStorage.
-        // When "cloning" a tab (e.g. via middle-click), the `active` flag will be set and we'll create
-        // a new empty user session, otherwise it'll be false and we can re-use the user session.
-       'active': true
-    });
-    // Set JID on the connection object so that when we call `connection.bind`
-    // the new resource is found by Strophe.js and sent to the XMPP server.
-    _converse.connection.jid = jid;
-}
-
-
 /**
 /**
  * Stores the passed in JID for the current user, potentially creating a
  * Stores the passed in JID for the current user, potentially creating a
  * resource if the JID is bare.
  * resource if the JID is bare.
@@ -961,7 +739,7 @@ function saveJIDtoSession (jid) {
  * @params { String } jid
  * @params { String } jid
  */
  */
 _converse.setUserJID = async function (jid) {
 _converse.setUserJID = async function (jid) {
-    await initSession(jid);
+    await initSession(_converse, jid);
     /**
     /**
      * Triggered whenever the user's JID has been updated
      * Triggered whenever the user's JID has been updated
      * @event _converse#setUserJID
      * @event _converse#setUserJID
@@ -986,77 +764,6 @@ function setUpXMLLogging () {
     _converse.connection.xmlOutput = body => log.debug(body.outerHTML, 'color: darkcyan');
     _converse.connection.xmlOutput = body => log.debug(body.outerHTML, 'color: darkcyan');
 }
 }
 
 
-async function getLoginCredentials () {
-    let credentials;
-    let wait = 0;
-    while (!credentials) {
-        try {
-            credentials = await fetchLoginCredentials(wait); // eslint-disable-line no-await-in-loop
-        } catch (e) {
-            log.error('Could not fetch login credentials');
-            log.error(e);
-        }
-        // If unsuccessful, we wait 2 seconds between subsequent attempts to
-        // fetch the credentials.
-        wait = 2000;
-    }
-    return credentials;
-}
-
-async function getLoginCredentialsFromBrowser () {
-    try {
-        const creds = await navigator.credentials.get({'password': true});
-        if (creds && creds.type == 'password' && u.isValidJID(creds.id)) {
-            await _converse.setUserJID(creds.id);
-            return {'jid': creds.id, 'password': creds.password};
-        }
-    } catch (e) {
-        log.error(e);
-    }
-}
-
-
-// Make sure everything is reset in case this is a subsequent call to
-// converse.initialize (happens during tests).
-async function cleanup () {
-    await api.trigger('cleanup', {'synchronous': true});
-    _converse.router.history.stop();
-    unregisterGlobalEventHandlers();
-    _converse.connection?.reset();
-    _converse.stopListening();
-    _converse.off();
-    if (_converse.promises['initialized'].isResolved) {
-        api.promises.add('initialized')
-    }
-}
-
-
-function fetchLoginCredentials (wait=0) {
-    return new Promise(
-        debounce((resolve, reject) => {
-            const xhr = new XMLHttpRequest();
-            xhr.open('GET', api.settings.get("credentials_url"), true);
-            xhr.setRequestHeader('Accept', 'application/json, text/javascript');
-            xhr.onload = () => {
-                if (xhr.status >= 200 && xhr.status < 400) {
-                    const data = JSON.parse(xhr.responseText);
-                    _converse.setUserJID(data.jid).then(() => {
-                        resolve({
-                            jid: data.jid,
-                            password: data.password
-                        });
-                    });
-                } else {
-                    reject(new Error(`${xhr.status}: ${xhr.responseText}`));
-                }
-            };
-            xhr.onerror = reject;
-            xhr.send();
-        }, wait)
-    );
-}
-
-
 _converse.saveWindowState = function (ev) {
 _converse.saveWindowState = function (ev) {
     // XXX: eventually we should be able to just use
     // XXX: eventually we should be able to just use
     // document.visibilityState (when we drop support for older
     // document.visibilityState (when we drop support for older
@@ -1088,28 +795,6 @@ _converse.saveWindowState = function (ev) {
     api.trigger('windowStateChanged', {state});
     api.trigger('windowStateChanged', {state});
 }
 }
 
 
-
-function registerGlobalEventHandlers () {
-    document.addEventListener("visibilitychange", _converse.saveWindowState);
-    _converse.saveWindowState({'type': document.hidden ? "blur" : "focus"}); // Set initial state
-    /**
-     * Called once Converse has registered its global event handlers
-     * (for events such as window resize or unload).
-     * Plugins can listen to this event as cue to register their own
-     * global event handlers.
-     * @event _converse#registeredGlobalEventHandlers
-     * @example _converse.api.listen.on('registeredGlobalEventHandlers', () => { ... });
-     */
-    api.trigger('registeredGlobalEventHandlers');
-}
-
-
-function unregisterGlobalEventHandlers () {
-    document.removeEventListener("visibilitychange", _converse.saveWindowState);
-    api.trigger('unregisteredGlobalEventHandlers');
-}
-
-
 _converse.ConnectionFeedback = Model.extend({
 _converse.ConnectionFeedback = Model.extend({
     defaults: {
     defaults: {
         'connection_status': Strophe.Status.DISCONNECTED,
         'connection_status': Strophe.Status.DISCONNECTED,
@@ -1121,20 +806,6 @@ _converse.ConnectionFeedback = Model.extend({
 });
 });
 
 
 
 
-function setUnloadEvent () {
-    if ('onpagehide' in window) {
-        // Pagehide gets thrown in more cases than unload. Specifically it
-        // gets thrown when the page is cached and not just
-        // closed/destroyed. It's the only viable event on mobile Safari.
-        // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
-        _converse.unloadevent = 'pagehide';
-    } else if ('onbeforeunload' in window) {
-        _converse.unloadevent = 'beforeunload';
-    } else if ('onunload' in window) {
-        _converse.unloadevent = 'unload';
-    }
-}
-
 export const converse = window.converse || {};
 export const converse = window.converse || {};
 
 
 
 
@@ -1193,7 +864,7 @@ Object.assign(converse, {
      * });
      * });
      */
      */
     async initialize (settings) {
     async initialize (settings) {
-        await cleanup();
+        await cleanup(_converse);
 
 
         setUnloadEvent();
         setUnloadEvent();
         initAppSettings(settings);
         initAppSettings(settings);
@@ -1222,11 +893,11 @@ Object.assign(converse, {
          */
          */
         _converse.send_initial_presence = true;
         _converse.send_initial_presence = true;
 
 
-        await initSessionStorage();
-        await initClientConfig();
+        await initSessionStorage(_converse);
+        await initClientConfig(_converse);
         await i18n.initialize();
         await i18n.initialize();
-        initPlugins();
-        registerGlobalEventHandlers();
+        initPlugins(_converse);
+        registerGlobalEventHandlers(_converse);
 
 
         try {
         try {
             !History.started && _converse.router.history.start();
             !History.started && _converse.router.history.start();

+ 1 - 1
src/headless/plugins/bookmarks/collection.js

@@ -4,7 +4,7 @@ import log from "@converse/headless/log.js";
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 
 
 const { Strophe, $iq, sizzle } = converse.env;
 const { Strophe, $iq, sizzle } = converse.env;
 
 

+ 1 - 1
src/headless/plugins/chat/model.js

@@ -7,7 +7,7 @@ import pick from "lodash-es/pick";
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { _converse, api, converse } from "../../core.js";
 import { _converse, api, converse } from "../../core.js";
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 import { debouncedPruneHistory, pruneHistory } from '@converse/headless/shared/chat/utils.js';
 import { debouncedPruneHistory, pruneHistory } from '@converse/headless/shared/chat/utils.js';
 import { getMediaURLs } from '@converse/headless/shared/parsers';
 import { getMediaURLs } from '@converse/headless/shared/parsers';
 import { parseMessage } from './parsers.js';
 import { parseMessage } from './parsers.js';

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

@@ -1,6 +1,6 @@
 import { Collection } from "@converse/skeletor/src/collection";
 import { Collection } from "@converse/skeletor/src/collection";
 import { _converse, api } from "../../core.js";
 import { _converse, api } from "../../core.js";
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 
 
 const ChatBoxes = Collection.extend({
 const ChatBoxes = Collection.extend({
     comparator: 'time_opened',
     comparator: 'time_opened',

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

@@ -12,7 +12,7 @@ import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
 import { _converse, api, converse } from '../../core.js';
 import { _converse, api, converse } from '../../core.js';
 import { computeAffiliationsDelta, setAffiliations, getAffiliationList }  from './affiliations/utils.js';
 import { computeAffiliationsDelta, setAffiliations, getAffiliationList }  from './affiliations/utils.js';
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 import { isArchived, getMediaURLs } from '@converse/headless/shared/parsers';
 import { isArchived, getMediaURLs } from '@converse/headless/shared/parsers';
 import { parseMUCMessage, parseMUCPresence } from './parsers.js';
 import { parseMUCMessage, parseMUCPresence } from './parsers.js';
 import { sendMarker } from '@converse/headless/shared/actions';
 import { sendMarker } from '@converse/headless/shared/actions';

+ 1 - 1
src/headless/plugins/roster/contacts.js

@@ -5,7 +5,7 @@ import { Collection } from "@converse/skeletor/src/collection";
 import { Model } from "@converse/skeletor/src/model";
 import { Model } from "@converse/skeletor/src/model";
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 import { rejectPresenceSubscription } from './utils.js';
 import { rejectPresenceSubscription } from './utils.js';
 
 
 const { Strophe, $iq, sizzle, u } = converse.env;
 const { Strophe, $iq, sizzle, u } = converse.env;

+ 1 - 1
src/headless/plugins/roster/presence.js

@@ -2,7 +2,7 @@ import isNaN from "lodash-es/isNaN";
 import { Collection } from "@converse/skeletor/src/collection";
 import { Collection } from "@converse/skeletor/src/collection";
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { converse } from "@converse/headless/core";
 import { converse } from "@converse/headless/core";
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 
 
 const { Strophe, dayjs, sizzle } = converse.env;
 const { Strophe, dayjs, sizzle } = converse.env;
 
 

+ 1 - 1
src/headless/plugins/roster/utils.js

@@ -1,7 +1,7 @@
 import log from "@converse/headless/log";
 import log from "@converse/headless/log";
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 
 
 const { $pres } = converse.env;
 const { $pres } = converse.env;
 
 

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

@@ -1,5 +1,5 @@
 import { _converse, api, converse } from '@converse/headless/core';
 import { _converse, api, converse } from '@converse/headless/core';
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 
 
 const { Strophe, $build } = converse.env;
 const { Strophe, $build } = converse.env;
 
 

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

@@ -8,7 +8,7 @@ import log from "@converse/headless/log";
 import { Collection } from "@converse/skeletor/src/collection";
 import { Collection } from "@converse/skeletor/src/collection";
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { _converse, api, converse } from "../core.js";
 import { _converse, api, converse } from "../core.js";
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 
 
 const { Strophe, $iq, dayjs } = converse.env;
 const { Strophe, $iq, dayjs } = converse.env;
 const u = converse.env.utils;
 const u = converse.env.utils;

+ 1 - 1
src/headless/shared/_converse.js

@@ -3,7 +3,7 @@ import log from '@converse/headless/log';
 import { CONNECTION_STATUS } from '@converse/headless/shared/constants';
 import { CONNECTION_STATUS } from '@converse/headless/shared/constants';
 import { Router } from '@converse/skeletor/src/router.js';
 import { Router } from '@converse/skeletor/src/router.js';
 import { TimeoutError } from '@converse/headless/shared/errors';
 import { TimeoutError } from '@converse/headless/shared/errors';
-import { createStore, getDefaultStore } from '@converse/headless/shared/utils.js';
+import { createStore, getDefaultStore } from '@converse/headless/utils/storage.js';
 import { getInitSettings } from '@converse/headless/shared/settings';
 import { getInitSettings } from '@converse/headless/shared/settings';
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
 
 

+ 1 - 1
src/headless/shared/parsers.js

@@ -4,7 +4,7 @@ import log from '@converse/headless/log';
 import sizzle from 'sizzle';
 import sizzle from 'sizzle';
 import { Strophe } from 'strophe.js/src/strophe';
 import { Strophe } from 'strophe.js/src/strophe';
 import { _converse, api } from '@converse/headless/core';
 import { _converse, api } from '@converse/headless/core';
-import { decodeHTMLEntities } from '@converse/headless/shared/utils';
+import { decodeHTMLEntities } from '@converse/headless/utils/core.js';
 import { rejectMessage } from '@converse/headless/shared/actions';
 import { rejectMessage } from '@converse/headless/shared/actions';
 import {
 import {
     isAudioDomainAllowed,
     isAudioDomainAllowed,

+ 1 - 1
src/headless/shared/settings.js

@@ -5,7 +5,7 @@ import log from '@converse/headless/log';
 import pick from 'lodash-es/pick';
 import pick from 'lodash-es/pick';
 import u from '@converse/headless/utils/core';
 import u from '@converse/headless/utils/core';
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 
 
 let init_settings = {}; // Container for settings passed in via converse.initialize
 let init_settings = {}; // Container for settings passed in via converse.initialize
 let app_settings = {};
 let app_settings = {};

+ 55 - 2
src/headless/utils/core.js

@@ -3,14 +3,16 @@
  * @license Mozilla Public License (MPLv2)
  * @license Mozilla Public License (MPLv2)
  * @description This is the core utilities module.
  * @description This is the core utilities module.
  */
  */
+import DOMPurify from 'dompurify';
+import _converse from '@converse/headless/shared/_converse.js';
 import compact from "lodash-es/compact";
 import compact from "lodash-es/compact";
 import isElement from "lodash-es/isElement";
 import isElement from "lodash-es/isElement";
 import isObject from "lodash-es/isObject";
 import isObject from "lodash-es/isObject";
 import last from "lodash-es/last";
 import last from "lodash-es/last";
-import log from "@converse/headless/log";
+import log from '@converse/headless/log.js';
 import sizzle from "sizzle";
 import sizzle from "sizzle";
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
-import { Strophe } from 'strophe.js/src/strophe';
+import { Strophe } from 'strophe.js/src/strophe.js';
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
 
 
 /**
 /**
@@ -545,4 +547,55 @@ u.waitUntil = function (func, max_wait=300, check_delay=3) {
     return promise;
     return promise;
 };
 };
 
 
+export function setUnloadEvent () {
+    if ('onpagehide' in window) {
+        // Pagehide gets thrown in more cases than unload. Specifically it
+        // gets thrown when the page is cached and not just
+        // closed/destroyed. It's the only viable event on mobile Safari.
+        // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
+        _converse.unloadevent = 'pagehide';
+    } else if ('onbeforeunload' in window) {
+        _converse.unloadevent = 'beforeunload';
+    } else if ('onunload' in window) {
+        _converse.unloadevent = 'unload';
+    }
+}
+
+export async function getLoginCredentialsFromBrowser () {
+    try {
+        const creds = await navigator.credentials.get({'password': true});
+        if (creds && creds.type == 'password' && u.isValidJID(creds.id)) {
+            await _converse.setUserJID(creds.id);
+            return {'jid': creds.id, 'password': creds.password};
+        }
+    } catch (e) {
+        log.error(e);
+    }
+}
+
+export function replacePromise (name) {
+    const existing_promise = _converse.promises[name];
+    if (!existing_promise) {
+        throw new Error(`Tried to replace non-existing promise: ${name}`);
+    }
+    if (existing_promise.replace) {
+        const promise = getOpenPromise();
+        promise.replace = existing_promise.replace;
+        _converse.promises[name] = promise;
+    } else {
+        log.debug(`Not replacing promise "${name}"`);
+    }
+}
+
+const element = document.createElement('div');
+
+export function decodeHTMLEntities (str) {
+    if (str && typeof str === 'string') {
+        element.innerHTML = DOMPurify.sanitize(str);
+        str = element.textContent;
+        element.textContent = '';
+    }
+    return str;
+}
+
 export default u;
 export default u;

+ 312 - 0
src/headless/utils/init.js

@@ -0,0 +1,312 @@
+import Storage from '@converse/skeletor/src/storage.js';
+import _converse from '@converse/headless/shared/_converse';
+import debounce from 'lodash-es/debounce';
+import localDriver from 'localforage-webextensionstorage-driver/local';
+import log from '@converse/headless/log';
+import syncDriver from 'localforage-webextensionstorage-driver/sync';
+import { CORE_PLUGINS } from '@converse/headless/shared/constants.js';
+import { Connection } from '@converse/headless/shared/connection.js';
+import { Model } from '@converse/skeletor/src/model.js';
+import { Strophe } from 'strophe.js/src/strophe';
+import { createStore, initStorage } from '@converse/headless/utils/storage.js';
+import { getLoginCredentialsFromBrowser } from '@converse/headless/utils/core.js';
+
+
+export function initPlugins (_converse) {
+    // If initialize gets called a second time (e.g. during tests), then we
+    // need to re-apply all plugins (for a new converse instance), and we
+    // therefore need to clear this array that prevents plugins from being
+    // initialized twice.
+    // If initialize is called for the first time, then this array is empty
+    // in any case.
+    _converse.pluggable.initialized_plugins = [];
+    const whitelist = CORE_PLUGINS.concat(_converse.api.settings.get("whitelisted_plugins"));
+
+    if (_converse.api.settings.get("singleton")) {
+        [
+            'converse-bookmarks',
+            'converse-controlbox',
+            'converse-headline',
+            'converse-register'
+        ].forEach(name => _converse.api.settings.get("blacklisted_plugins").push(name));
+    }
+
+    _converse.pluggable.initializePlugins(
+        { _converse },
+        whitelist,
+        _converse.api.settings.get("blacklisted_plugins")
+    );
+
+    /**
+     * Triggered once all plugins have been initialized. This is a useful event if you want to
+     * register event handlers but would like your own handlers to be overridable by
+     * plugins. In that case, you need to first wait until all plugins have been
+     * initialized, so that their overrides are active. One example where this is used
+     * is in [converse-notifications.js](https://github.com/jcbrand/converse.js/blob/master/src/converse-notification.js)`.
+     *
+     * Also available as an [ES2015 Promise](http://es6-features.org/#PromiseUsage)
+     * which can be listened to with `_converse.api.waitUntil`.
+     *
+     * @event _converse#pluginsInitialized
+     * @memberOf _converse
+     * @example _converse.api.listen.on('pluginsInitialized', () => { ... });
+     */
+    _converse.api.trigger('pluginsInitialized');
+}
+
+export async function initClientConfig (_converse) {
+    /* The client config refers to configuration of the client which is
+     * independent of any particular user.
+     * What this means is that config values need to persist across
+     * user sessions.
+     */
+    const id = 'converse.client-config';
+    _converse.config = new Model({ id, 'trusted': true });
+    _converse.config.browserStorage = createStore(id, "session");
+    await new Promise(r => _converse.config.fetch({'success': r, 'error': r}));
+    /**
+     * Triggered once the XMPP-client configuration has been initialized.
+     * The client configuration is independent of any particular and its values
+     * persist across user sessions.
+     *
+     * @event _converse#clientConfigInitialized
+     * @example
+     * _converse.api.listen.on('clientConfigInitialized', () => { ... });
+     */
+    _converse.api.trigger('clientConfigInitialized');
+}
+
+export async function initSessionStorage (_converse) {
+    await Storage.sessionStorageInitialized;
+    _converse.storage = {
+        'session': Storage.localForage.createInstance({
+            'name': _converse.isTestEnv() ? 'converse-test-session' : 'converse-session',
+            'description': 'sessionStorage instance',
+            'driver': ['sessionStorageWrapper']
+        })
+    };
+}
+
+function initPersistentStorage (_converse, store_name) {
+    if (_converse.api.settings.get('persistent_store') === 'sessionStorage') {
+        return;
+    } else if (_converse.api.settings.get("persistent_store") === 'BrowserExtLocal') {
+        Storage.localForage.defineDriver(localDriver).then(
+            () => Storage.localForage.setDriver('webExtensionLocalStorage')
+        );
+        _converse.storage['persistent'] = Storage.localForage;
+        return;
+
+    } else if (_converse.api.settings.get("persistent_store") === 'BrowserExtSync') {
+        Storage.localForage.defineDriver(syncDriver).then(
+            () => Storage.localForage.setDriver('webExtensionSyncStorage')
+        );
+        _converse.storage['persistent'] = Storage.localForage;
+        return;
+    }
+
+    const config = {
+        'name': _converse.isTestEnv() ? 'converse-test-persistent' : 'converse-persistent',
+        'storeName': store_name
+    }
+    if (_converse.api.settings.get("persistent_store") === 'localStorage') {
+        config['description'] = 'localStorage instance';
+        config['driver'] = [Storage.localForage.LOCALSTORAGE];
+    } else if (_converse.api.settings.get("persistent_store") === 'IndexedDB') {
+        config['description'] = 'indexedDB instance';
+        config['driver'] = [Storage.localForage.INDEXEDDB];
+    }
+    _converse.storage['persistent'] = Storage.localForage.createInstance(config);
+}
+
+function saveJIDtoSession (_converse, jid) {
+    jid = _converse.session.get('jid') || jid;
+    if (_converse.api.settings.get("authentication") !== _converse.ANONYMOUS && !Strophe.getResourceFromJid(jid)) {
+        jid = jid.toLowerCase() + Connection.generateResource();
+    }
+    _converse.jid = jid;
+    _converse.bare_jid = Strophe.getBareJidFromJid(jid);
+    _converse.resource = Strophe.getResourceFromJid(jid);
+    _converse.domain = Strophe.getDomainFromJid(jid);
+    _converse.session.save({
+       'jid': jid,
+       'bare_jid': _converse.bare_jid,
+       'resource': _converse.resource,
+       'domain': _converse.domain,
+        // We use the `active` flag to determine whether we should use the values from sessionStorage.
+        // When "cloning" a tab (e.g. via middle-click), the `active` flag will be set and we'll create
+        // a new empty user session, otherwise it'll be false and we can re-use the user session.
+       'active': true
+    });
+    // Set JID on the connection object so that when we call `connection.bind`
+    // the new resource is found by Strophe.js and sent to the XMPP server.
+    _converse.connection.jid = jid;
+}
+
+export async function initSession (_converse, jid) {
+    const is_shared_session = _converse.api.settings.get('connection_options').worker;
+
+    const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase();
+    const id = `converse.session-${bare_jid}`;
+    if (_converse.session?.get('id') !== id) {
+        initPersistentStorage(_converse, bare_jid);
+
+        _converse.session = new Model({ id });
+        initStorage(_converse.session, id, is_shared_session ? "persistent" : "session");
+        await new Promise(r => _converse.session.fetch({'success': r, 'error': r}));
+
+        if (!is_shared_session && _converse.session.get('active')) {
+            // If the `active` flag is set, it means this tab was cloned from
+            // another (e.g. via middle-click), and its session data was copied over.
+            _converse.session.clear();
+            _converse.session.save({id});
+        }
+        saveJIDtoSession(_converse, jid);
+        /**
+         * Triggered once the user's session has been initialized. The session is a
+         * cache which stores information about the user's current session.
+         * @event _converse#userSessionInitialized
+         * @memberOf _converse
+         */
+        _converse.api.trigger('userSessionInitialized');
+    } else {
+        saveJIDtoSession(_converse, jid);
+    }
+}
+
+export function registerGlobalEventHandlers (_converse) {
+    document.addEventListener("visibilitychange", _converse.saveWindowState);
+    _converse.saveWindowState({'type': document.hidden ? "blur" : "focus"}); // Set initial state
+    /**
+     * Called once Converse has registered its global event handlers
+     * (for events such as window resize or unload).
+     * Plugins can listen to this event as cue to register their own
+     * global event handlers.
+     * @event _converse#registeredGlobalEventHandlers
+     * @example _converse.api.listen.on('registeredGlobalEventHandlers', () => { ... });
+     */
+    _converse.api.trigger('registeredGlobalEventHandlers');
+}
+
+
+function unregisterGlobalEventHandlers (_converse) {
+    const { api } = _converse;
+    document.removeEventListener("visibilitychange", _converse.saveWindowState);
+    api.trigger('unregisteredGlobalEventHandlers');
+}
+
+// Make sure everything is reset in case this is a subsequent call to
+// converse.initialize (happens during tests).
+export async function cleanup (_converse) {
+    const { api } = _converse;
+    await api.trigger('cleanup', {'synchronous': true});
+    _converse.router.history.stop();
+    unregisterGlobalEventHandlers(_converse);
+    _converse.connection?.reset();
+    _converse.stopListening();
+    _converse.off();
+    if (_converse.promises['initialized'].isResolved) {
+        api.promises.add('initialized')
+    }
+}
+
+async function getLoginCredentials () {
+    let credentials;
+    let wait = 0;
+    while (!credentials) {
+        try {
+            credentials = await fetchLoginCredentials(wait); // eslint-disable-line no-await-in-loop
+        } catch (e) {
+            log.error('Could not fetch login credentials');
+            log.error(e);
+        }
+        // If unsuccessful, we wait 2 seconds between subsequent attempts to
+        // fetch the credentials.
+        wait = 2000;
+    }
+    return credentials;
+}
+
+
+function fetchLoginCredentials (wait=0) {
+    return new Promise(
+        debounce((resolve, reject) => {
+            const xhr = new XMLHttpRequest();
+            xhr.open('GET', _converse.api.settings.get("credentials_url"), true);
+            xhr.setRequestHeader('Accept', 'application/json, text/javascript');
+            xhr.onload = () => {
+                if (xhr.status >= 200 && xhr.status < 400) {
+                    const data = JSON.parse(xhr.responseText);
+                    _converse.setUserJID(data.jid).then(() => {
+                        resolve({
+                            jid: data.jid,
+                            password: data.password
+                        });
+                    });
+                } else {
+                    reject(new Error(`${xhr.status}: ${xhr.responseText}`));
+                }
+            };
+            xhr.onerror = reject;
+            xhr.send();
+        }, wait)
+    );
+}
+
+export async function attemptNonPreboundSession (credentials, automatic) {
+    const { api } = _converse;
+    if (api.settings.get("authentication") === _converse.LOGIN) {
+        // XXX: If EITHER ``keepalive`` or ``auto_login`` is ``true`` and
+        // ``authentication`` is set to ``login``, then Converse will try to log the user in,
+        // since we don't have a way to distinguish between wether we're
+        // restoring a previous session (``keepalive``) or whether we're
+        // automatically setting up a new session (``auto_login``).
+        // So we can't do the check (!automatic || _converse.api.settings.get("auto_login")) here.
+        if (credentials) {
+            connect(credentials);
+        } else if (_converse.api.settings.get("credentials_url")) {
+            // We give credentials_url preference, because
+            // _converse.connection.pass might be an expired token.
+            connect(await getLoginCredentials());
+        } else if (_converse.jid && (_converse.api.settings.get("password") || _converse.connection.pass)) {
+            connect();
+        } else if (!_converse.isTestEnv() && 'credentials' in navigator) {
+            connect(await getLoginCredentialsFromBrowser());
+        } else {
+            !_converse.isTestEnv() && log.warn("attemptNonPreboundSession: Couldn't find credentials to log in with");
+        }
+    } else if ([_converse.ANONYMOUS, _converse.EXTERNAL].includes(_converse.api.settings.get("authentication")) && (!automatic || _converse.api.settings.get("auto_login"))) {
+        connect();
+    }
+}
+
+
+function connect (credentials) {
+    if ([_converse.ANONYMOUS, _converse.EXTERNAL].includes(_converse.api.settings.get("authentication"))) {
+        if (!_converse.jid) {
+            throw new Error("Config Error: when using anonymous login " +
+                "you need to provide the server's domain via the 'jid' option. " +
+                "Either when calling converse.initialize, or when calling " +
+                "_converse.api.user.login.");
+        }
+        if (!_converse.connection.reconnecting) {
+            _converse.connection.reset();
+        }
+        _converse.connection.connect(_converse.jid.toLowerCase());
+    } else if (_converse.api.settings.get("authentication") === _converse.LOGIN) {
+        const password = credentials ? credentials.password : (_converse.connection?.pass || _converse.api.settings.get("password"));
+        if (!password) {
+            if (_converse.api.settings.get("auto_login")) {
+                throw new Error("autoLogin: If you use auto_login and "+
+                    "authentication='login' then you also need to provide a password.");
+            }
+            _converse.connection.setDisconnectionCause(Strophe.Status.AUTHFAIL, undefined, true);
+            _converse.api.connection.disconnect();
+            return;
+        }
+        if (!_converse.connection.reconnecting) {
+            _converse.connection.reset();
+        }
+        _converse.connection.connect(_converse.jid, password);
+    }
+}

+ 0 - 28
src/headless/shared/utils.js → src/headless/utils/storage.js

@@ -1,8 +1,5 @@
-import DOMPurify from 'dompurify';
 import Storage from '@converse/skeletor/src/storage.js';
 import Storage from '@converse/skeletor/src/storage.js';
-import log from '@converse/headless/log';
 import { _converse, api } from '@converse/headless/core';
 import { _converse, api } from '@converse/headless/core';
-import { getOpenPromise } from '@converse/openpromise';
 
 
 export function getDefaultStore () {
 export function getDefaultStore () {
     if (_converse.config.get('trusted')) {
     if (_converse.config.get('trusted')) {
@@ -36,28 +33,3 @@ export function initStorage (model, id, type) {
         model.listenTo(_converse, 'beforeLogout', flush);
         model.listenTo(_converse, 'beforeLogout', flush);
     }
     }
 }
 }
-
-export function replacePromise (name) {
-    const existing_promise = _converse.promises[name];
-    if (!existing_promise) {
-        throw new Error(`Tried to replace non-existing promise: ${name}`);
-    }
-    if (existing_promise.replace) {
-        const promise = getOpenPromise();
-        promise.replace = existing_promise.replace;
-        _converse.promises[name] = promise;
-    } else {
-        log.debug(`Not replacing promise "${name}"`);
-    }
-}
-
-const element = document.createElement('div');
-
-export function decodeHTMLEntities (str) {
-    if (str && typeof str === 'string') {
-        element.innerHTML = DOMPurify.sanitize(str);
-        str = element.textContent;
-        element.textContent = '';
-    }
-    return str;
-}

+ 1 - 1
src/plugins/bookmark-views/bookmarks-list.js

@@ -2,7 +2,7 @@ import log from '@converse/headless/log';
 import tpl_bookmarks_list from './templates/list.js';
 import tpl_bookmarks_list from './templates/list.js';
 import { ElementView } from '@converse/skeletor/src/element.js';
 import { ElementView } from '@converse/skeletor/src/element.js';
 import { _converse, api, converse } from '@converse/headless/core';
 import { _converse, api, converse } from '@converse/headless/core';
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 import { render } from 'lit';
 import { render } from 'lit';
 
 
 const u = converse.env.utils;
 const u = converse.env.utils;

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

@@ -2,7 +2,7 @@ import MinimizedChatsToggle from './toggle.js';
 import tpl_chats_panel from './templates/chats-panel.js';
 import tpl_chats_panel from './templates/chats-panel.js';
 import { ElementView } from '@converse/skeletor/src/element.js';
 import { ElementView } from '@converse/skeletor/src/element.js';
 import { _converse, api } from '@converse/headless/core';
 import { _converse, api } from '@converse/headless/core';
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 import { render } from 'lit';
 import { render } from 'lit';
 
 
 
 

+ 1 - 1
src/plugins/omemo/devicelist.js

@@ -1,7 +1,7 @@
 import log from '@converse/headless/log';
 import log from '@converse/headless/log';
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { _converse, api, converse } from '@converse/headless/core';
 import { _converse, api, converse } from '@converse/headless/core';
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 import { restoreOMEMOSession } from './utils.js';
 import { restoreOMEMOSession } from './utils.js';
 
 
 const { Strophe, $build, $iq, sizzle } = converse.env;
 const { Strophe, $build, $iq, sizzle } = converse.env;

+ 1 - 1
src/plugins/omemo/utils.js

@@ -10,7 +10,7 @@ import { KEY_ALGO, UNTRUSTED, TAG_LENGTH } from './consts.js';
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { _converse, converse, api } from '@converse/headless/core';
 import { _converse, converse, api } from '@converse/headless/core';
 import { html } from 'lit';
 import { html } from 'lit';
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 import { isAudioURL, isImageURL, isVideoURL, getURI } from '@converse/headless/utils/url.js';
 import { isAudioURL, isImageURL, isVideoURL, getURI } from '@converse/headless/utils/url.js';
 import concat from 'lodash-es/concat';
 import concat from 'lodash-es/concat';
 import { until } from 'lit/directives/until.js';
 import { until } from 'lit/directives/until.js';

+ 1 - 1
src/plugins/roomslist/view.js

@@ -4,7 +4,7 @@ import { ElementView } from '@converse/skeletor/src/element.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 import { render } from 'lit';
 import { render } from 'lit';
 
 
 const { Strophe } = converse.env;
 const { Strophe } = converse.env;

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

@@ -3,7 +3,7 @@ import tpl_roster_filter from "./templates/roster_filter.js";
 import { CustomElement } from 'shared/components/element.js';
 import { CustomElement } from 'shared/components/element.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { _converse, api } from "@converse/headless/core";
 import { _converse, api } from "@converse/headless/core";
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 
 
 export const RosterFilter = Model.extend({
 export const RosterFilter = Model.extend({
     initialize () {
     initialize () {

+ 1 - 1
src/shared/chat/emoji-dropdown.js

@@ -2,7 +2,7 @@ import DropdownBase from "shared/components/dropdown.js";
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
 import { html } from "lit";
 import { html } from "lit";
-import { initStorage } from '@converse/headless/shared/utils.js';
+import { initStorage } from '@converse/headless/utils/storage.js';
 import { until } from 'lit/directives/until.js';
 import { until } from 'lit/directives/until.js';
 
 
 const u = converse.env.utils;
 const u = converse.env.utils;

+ 27 - 0
src/shared/constants.js

@@ -0,0 +1,27 @@
+// These are all the view-layer plugins.
+//
+// For the full Converse build, this list serves
+// as a whitelist (see src/converse.js) in addition to the
+// CORE_PLUGINS list in src/headless/consts.js.
+export const VIEW_PLUGINS = [
+    'converse-bookmark-views',
+    'converse-chatboxviews',
+    'converse-chatview',
+    'converse-controlbox',
+    'converse-dragresize',
+    'converse-fullscreen',
+    'converse-headlines-view',
+    'converse-mam-views',
+    'converse-minimize',
+    'converse-modal',
+    'converse-muc-views',
+    'converse-notification',
+    'converse-omemo',
+    'converse-profile',
+    'converse-push',
+    'converse-register',
+    'converse-roomslist',
+    'converse-rootview',
+    'converse-rosterview',
+    'converse-singleton'
+];