diff --git a/dev.html b/dev.html
index 2886085b71..35d07cf75b 100644
--- a/dev.html
+++ b/dev.html
@@ -40,8 +40,8 @@
muc_show_logs_before_join: true,
notify_all_room_messages: ['discuss@conference.conversejs.org'],
view_mode: 'fullscreen',
- // websocket_url: 'wss://conversejs.org/xmpp-websocket',
- websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
+ websocket_url: 'wss://conversejs.org/xmpp-websocket',
+ // websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
whitelisted_plugins: ['converse-debug'],
// connection_options: { worker: '/dist/shared-connection-worker.js' }
});
diff --git a/src/headless/index.js b/src/headless/index.js
index c88420f2a4..17fda66a1b 100644
--- a/src/headless/index.js
+++ b/src/headless/index.js
@@ -2,12 +2,12 @@ import './shared/constants.js';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import api from './shared/api/index.js';
import _converse from './shared/_converse';
+_converse.api = api;
+
import dayjs from 'dayjs';
import i18n from './shared/i18n';
import { converse } from './shared/api/public.js';
-_converse.api = api;
-
dayjs.extend(advancedFormat);
/* START: Removable components
diff --git a/src/headless/plugins/bosh.js b/src/headless/plugins/bosh.js
index c7acfea071..c57b7aef00 100644
--- a/src/headless/plugins/bosh.js
+++ b/src/headless/plugins/bosh.js
@@ -10,6 +10,8 @@ import log from "../log.js";
import { BOSH_WAIT } from '../shared/constants.js';
import { Model } from '@converse/skeletor/src/model.js';
import { setUserJID, } from '../utils/init.js';
+import { isTestEnv } from '../utils/session.js';
+import { createStore } from '../utils/storage.js';
const { Strophe } = converse.env;
@@ -33,7 +35,7 @@ converse.plugins.add('converse-bosh', {
const id = BOSH_SESSION_ID;
if (!_converse.bosh_session) {
_converse.bosh_session = new Model({id});
- _converse.bosh_session.browserStorage = _converse.createStore(id, "session");
+ _converse.bosh_session.browserStorage = createStore(id, "session");
await new Promise(resolve => _converse.bosh_session.fetch({'success': resolve, 'error': resolve}));
}
if (_converse.jid) {
@@ -93,7 +95,7 @@ converse.plugins.add('converse-bosh', {
_converse.connection.restore(jid, _converse.connection.onConnectStatusChanged);
return true;
} catch (e) {
- !_converse.isTestEnv() && log.warn("Could not restore session for jid: "+jid+" Error message: "+e.message);
+ !isTestEnv() && log.warn("Could not restore session for jid: "+jid+" Error message: "+e.message);
return false;
}
}
diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js
index 2511918a82..d5fc7e05a1 100644
--- a/src/headless/plugins/chat/model.js
+++ b/src/headless/plugins/chat/model.js
@@ -11,7 +11,8 @@ import { filesize } from "filesize";
import { getMediaURLsMetadata } from '../../shared/parsers.js';
import { getOpenPromise } from '@converse/openpromise';
import { initStorage } from '../../utils/storage.js';
-import { isUniView, isEmptyMessage } from '../../utils/core.js';
+import { isEmptyMessage } from '../../utils/index.js';
+import { isUniView } from '../../utils/session.js';
import { parseMessage } from './parsers.js';
import { sendMarker } from '../../shared/actions.js';
diff --git a/src/headless/plugins/chat/parsers.js b/src/headless/plugins/chat/parsers.js
index f71edb66fd..6fd8e0fa3a 100644
--- a/src/headless/plugins/chat/parsers.js
+++ b/src/headless/plugins/chat/parsers.js
@@ -2,7 +2,7 @@ import _converse from '../../shared/_converse.js';
import api, { converse } from '../../shared/api/index.js';
import dayjs from 'dayjs';
import log from '../../log.js';
-import u from '../../utils/core';
+import u from '../../utils/index.js';
import { rejectMessage } from '../../shared/actions';
import {
diff --git a/src/headless/plugins/chat/utils.js b/src/headless/plugins/chat/utils.js
index e8bac5cd6b..e72c3580a3 100644
--- a/src/headless/plugins/chat/utils.js
+++ b/src/headless/plugins/chat/utils.js
@@ -3,7 +3,7 @@ import api, { converse } from '../../shared/api/index.js';
import log from '../../log.js';
import { isArchived, isHeadline, isServerMessage, } from '../../shared/parsers';
import { parseMessage } from './parsers.js';
-import { shouldClearCache } from '../../utils/core.js';
+import { shouldClearCache } from '../../utils/session.js';
const { Strophe, u } = converse.env;
diff --git a/src/headless/plugins/disco/entity.js b/src/headless/plugins/disco/entity.js
index 43fa252660..4cecf887b5 100644
--- a/src/headless/plugins/disco/entity.js
+++ b/src/headless/plugins/disco/entity.js
@@ -5,6 +5,7 @@ import sizzle from 'sizzle';
import { Collection } from '@converse/skeletor/src/collection';
import { Model } from '@converse/skeletor/src/model.js';
import { getOpenPromise } from '@converse/openpromise';
+import { createStore } from '../../utils/storage.js';
const { Strophe } = converse.env;
@@ -28,21 +29,21 @@ class DiscoEntity extends Model {
this.dataforms = new Collection();
let id = `converse.dataforms-${this.get('jid')}`;
- this.dataforms.browserStorage = _converse.createStore(id, 'session');
+ this.dataforms.browserStorage = createStore(id, 'session');
this.features = new Collection();
id = `converse.features-${this.get('jid')}`;
- this.features.browserStorage = _converse.createStore(id, 'session');
+ this.features.browserStorage = 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.fields.browserStorage = 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.identities.browserStorage = createStore(id, 'session');
this.fetchFeatures(options);
}
diff --git a/src/headless/plugins/disco/utils.js b/src/headless/plugins/disco/utils.js
index 6e720fb99b..5b9c69f516 100644
--- a/src/headless/plugins/disco/utils.js
+++ b/src/headless/plugins/disco/utils.js
@@ -1,6 +1,7 @@
import _converse from '../../shared/_converse.js';
import api, { converse } from '../../shared/api/index.js';
import { Collection } from "@converse/skeletor/src/collection";
+import { createStore } from '../../utils/storage.js';
const { Strophe, $iq } = converse.env;
@@ -65,7 +66,7 @@ export async function initializeDisco () {
_converse.disco_entities = new _converse.DiscoEntities();
const id = `converse.disco-entities-${_converse.bare_jid}`;
- _converse.disco_entities.browserStorage = _converse.createStore(id, 'session');
+ _converse.disco_entities.browserStorage = createStore(id, 'session');
const collection = await _converse.disco_entities.fetchEntities();
if (collection.length === 0 || !collection.get(_converse.domain)) {
@@ -93,7 +94,7 @@ export function initStreamFeatures () {
const id = `converse.stream-features-${bare_jid}`;
api.promises.add('streamFeaturesAdded');
_converse.stream_features = new Collection();
- _converse.stream_features.browserStorage = _converse.createStore(id, "session");
+ _converse.stream_features.browserStorage = createStore(id, "session");
}
}
diff --git a/src/headless/plugins/mam/placeholder.js b/src/headless/plugins/mam/placeholder.js
index b57b2ce61c..699cdb9e58 100644
--- a/src/headless/plugins/mam/placeholder.js
+++ b/src/headless/plugins/mam/placeholder.js
@@ -1,5 +1,5 @@
import { Model } from '@converse/skeletor/src/model.js';
-import { getUniqueId } from '../../utils/core.js';
+import { getUniqueId } from '../../utils/index.js';
export default class MAMPlaceholderMessage extends Model {
diff --git a/src/headless/plugins/muc/api.js b/src/headless/plugins/muc/api.js
index 0edec8f553..b97bcc451a 100644
--- a/src/headless/plugins/muc/api.js
+++ b/src/headless/plugins/muc/api.js
@@ -1,9 +1,8 @@
import _converse from '../../shared/_converse.js';
-import api, { converse } from '../../shared/api/index.js';
+import api from '../../shared/api/index.js';
import log from '../../log';
import { Strophe } from 'strophe.js';
-
-const { u } = converse.env;
+import { getJIDFromURI } from '../../utils/jid.js';
export default {
@@ -35,9 +34,9 @@ export default {
if (jids === undefined) {
throw new TypeError('rooms.create: You need to provide at least one JID');
} else if (typeof jids === 'string') {
- return api.rooms.get(u.getJIDFromURI(jids), attrs, true);
+ return api.rooms.get(getJIDFromURI(jids), attrs, true);
}
- return jids.map(jid => api.rooms.get(u.getJIDFromURI(jid), attrs, true));
+ return jids.map(jid => api.rooms.get(getJIDFromURI(jid), attrs, true));
},
/**
@@ -142,7 +141,7 @@ export default {
await api.waitUntil('chatBoxesFetched');
async function _get (jid) {
- jid = u.getJIDFromURI(jid);
+ jid = getJIDFromURI(jid);
let model = await api.chatboxes.get(jid);
if (!model && create) {
model = await api.chatboxes.create(jid, attrs, _converse.ChatRoom);
diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js
index 5b6117958a..340516e2ba 100644
--- a/src/headless/plugins/muc/muc.js
+++ b/src/headless/plugins/muc/muc.js
@@ -12,9 +12,10 @@ import { TimeoutError } from '../../shared/errors.js';
import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js';
import { getOpenPromise } from '@converse/openpromise';
import { handleCorrection } from '../../shared/chat/utils.js';
-import { initStorage } from '../../utils/storage.js';
+import { initStorage, createStore } from '../../utils/storage.js';
import { isArchived, getMediaURLsMetadata } from '../../shared/parsers.js';
-import { isUniView, getUniqueId, safeSave } from '../../utils/core.js';
+import { getUniqueId, safeSave } from '../../utils/index.js';
+import { isUniView } from '../../utils/session.js';
import { parseMUCMessage, parseMUCPresence } from './parsers.js';
import { sendMarker } from '../../shared/actions.js';
@@ -399,19 +400,19 @@ const ChatRoomMixin = {
}, {})
)
);
- this.features.browserStorage = _converse.createStore(id, 'session');
+ this.features.browserStorage = createStore(id, 'session');
this.features.listenTo(_converse, 'beforeLogout', () => this.features.browserStorage.flush());
id = `converse.muc-config-${_converse.bare_jid}-${this.get('jid')}`;
this.config = new Model({ id });
- this.config.browserStorage = _converse.createStore(id, 'session');
+ this.config.browserStorage = createStore(id, 'session');
this.config.listenTo(_converse, 'beforeLogout', () => this.config.browserStorage.flush());
},
initOccupants () {
this.occupants = new _converse.ChatRoomOccupants();
const id = `converse.occupants-${_converse.bare_jid}${this.get('jid')}`;
- this.occupants.browserStorage = _converse.createStore(id, 'session');
+ this.occupants.browserStorage = createStore(id, 'session');
this.occupants.chatroom = this;
this.occupants.listenTo(_converse, 'beforeLogout', () => this.occupants.browserStorage.flush());
},
diff --git a/src/headless/plugins/muc/occupants.js b/src/headless/plugins/muc/occupants.js
index 5a907b8510..c47c6e5b04 100644
--- a/src/headless/plugins/muc/occupants.js
+++ b/src/headless/plugins/muc/occupants.js
@@ -7,7 +7,7 @@ import { Model } from '@converse/skeletor/src/model.js';
import { Strophe } from 'strophe.js';
import { getAffiliationList } from './affiliations/utils.js';
import { getAutoFetchedAffiliationLists } from './utils.js';
-import { getUniqueId } from '../../utils/core.js';
+import { getUniqueId } from '../../utils/index.js';
const { u } = converse.env;
diff --git a/src/headless/plugins/muc/utils.js b/src/headless/plugins/muc/utils.js
index e7a0e9af98..91500ea9dc 100644
--- a/src/headless/plugins/muc/utils.js
+++ b/src/headless/plugins/muc/utils.js
@@ -2,7 +2,7 @@ import _converse from '../../shared/_converse.js';
import api, { converse } from '../../shared/api/index.js';
import log from '../../log.js';
import { ROLES } from './constants.js';
-import { safeSave } from '../../utils/core.js';
+import { safeSave } from '../../utils/index.js';
const { Strophe, sizzle, u } = converse.env;
diff --git a/src/headless/plugins/ping/utils.js b/src/headless/plugins/ping/utils.js
index 35c818448b..91251e284b 100644
--- a/src/headless/plugins/ping/utils.js
+++ b/src/headless/plugins/ping/utils.js
@@ -1,5 +1,6 @@
import _converse from '../../shared/_converse.js';
import api, { converse } from '../../shared/api/index.js';
+import { isTestEnv } from '../../utils/session.js';
const { Strophe, $iq } = converse.env;
@@ -56,7 +57,7 @@ export function unregisterIntervalHandler () {
}
export function onEverySecond () {
- if (_converse.isTestEnv() || !api.connection.authenticated()) {
+ if (isTestEnv() || !api.connection.authenticated()) {
return;
}
const ping_interval = api.settings.get('ping_interval');
diff --git a/src/headless/plugins/roster/utils.js b/src/headless/plugins/roster/utils.js
index 9fc5cf4548..1b251a2fb9 100644
--- a/src/headless/plugins/roster/utils.js
+++ b/src/headless/plugins/roster/utils.js
@@ -5,7 +5,7 @@ import { Model } from '@converse/skeletor/src/model.js';
import { RosterFilter } from '../../plugins/roster/filter.js';
import { STATUS_WEIGHTS } from "../../shared/constants";
import { initStorage } from '../../utils/storage.js';
-import { shouldClearCache } from '../../utils/core.js';
+import { shouldClearCache } from '../../utils/session.js';
const { $pres } = converse.env;
diff --git a/src/headless/plugins/smacks/utils.js b/src/headless/plugins/smacks/utils.js
index 48cc52071f..a2a94004f3 100644
--- a/src/headless/plugins/smacks/utils.js
+++ b/src/headless/plugins/smacks/utils.js
@@ -2,12 +2,13 @@ import _converse from '../../shared/_converse.js';
import api, { converse } from '../../shared/api/index.js';
import log from '../../log.js';
import { getOpenPromise } from '@converse/openpromise';
+import { isTestEnv } from '../../utils/session.js';
const { Strophe } = converse.env;
const u = converse.env.utils;
function isStreamManagementSupported () {
- if (api.connection.isType('bosh') && !_converse.isTestEnv()) {
+ if (api.connection.isType('bosh') && !isTestEnv()) {
return false;
}
return api.disco.stream.getFeature('sm', Strophe.NS.SM);
@@ -172,7 +173,7 @@ export async function sendEnableStanza () {
_converse.connection._addSysHandler(el => promise.resolve(saveSessionData(el)), Strophe.NS.SM, 'enabled');
_converse.connection._addSysHandler(el => promise.resolve(onFailedStanza(el)), Strophe.NS.SM, 'failed');
- const resume = api.connection.isType('websocket') || _converse.isTestEnv();
+ const resume = api.connection.isType('websocket') || isTestEnv();
const stanza = u.toStanza(``);
api.send(stanza);
_converse.connection.flush();
diff --git a/src/headless/plugins/status/index.js b/src/headless/plugins/status/index.js
index f422bcec17..b67e297eb2 100644
--- a/src/headless/plugins/status/index.js
+++ b/src/headless/plugins/status/index.js
@@ -6,7 +6,7 @@ import XMPPStatus from './status.js';
import _converse from '../../shared/_converse.js';
import api, { converse } from '../../shared/api/index.js';
import status_api from './api.js';
-import { shouldClearCache } from '../../utils/core.js';
+import { shouldClearCache } from '../../utils/session.js';
import {
addStatusToMUCJoinPresence,
initStatus,
diff --git a/src/headless/plugins/vcard/utils.js b/src/headless/plugins/vcard/utils.js
index a93db5875d..8b5e9a6d92 100644
--- a/src/headless/plugins/vcard/utils.js
+++ b/src/headless/plugins/vcard/utils.js
@@ -2,7 +2,7 @@ import _converse from '../../shared/_converse.js';
import api, { converse } from '../../shared/api/index.js';
import log from "../../log.js";
import { initStorage } from '../../utils/storage.js';
-import { shouldClearCache } from '../../utils/core.js';
+import { shouldClearCache } from '../../utils/session.js';
const { Strophe, $iq, u } = converse.env;
diff --git a/src/headless/shared/_converse.js b/src/headless/shared/_converse.js
index 3052148f6f..244f662797 100644
--- a/src/headless/shared/_converse.js
+++ b/src/headless/shared/_converse.js
@@ -3,10 +3,7 @@ import log from '../log.js';
import pluggable from 'pluggable.js/src/pluggable.js';
import { Events } from '@converse/skeletor/src/events.js';
import { Router } from '@converse/skeletor/src/router.js';
-import { createStore, getDefaultStore } from '../utils/storage.js';
-import { getInitSettings } from './settings/utils.js';
import { getOpenPromise } from '@converse/openpromise';
-import { shouldClearCache } from '../utils/core.js';
import {
ACTIVE,
@@ -42,7 +39,6 @@ import {
const _converse = {
log,
- shouldClearCache, // TODO: Should be moved to utils with next major release
VERSION_NAME,
templates: {},
@@ -86,12 +82,6 @@ const _converse = {
default_connection_options: {'explicitResourceBinding': true},
router: new Router(),
- isTestEnv: () => {
- return getInitSettings()['bosh_service_url'] === 'montague.lit/http-bind';
- },
-
- getDefaultStore,
- createStore,
/**
* Translate the given string based on the current locale.
diff --git a/src/headless/shared/api/events.js b/src/headless/shared/api/events.js
index 5f4ed0840c..6aea30ff0a 100644
--- a/src/headless/shared/api/events.js
+++ b/src/headless/shared/api/events.js
@@ -1,5 +1,5 @@
import _converse from '../_converse.js';
-import isFunction from '../../utils/core.js';
+import { isFunction } from '../../utils/object.js';
export default {
diff --git a/src/headless/shared/api/promise.js b/src/headless/shared/api/promise.js
index 37e4d76baf..e44f6c7318 100644
--- a/src/headless/shared/api/promise.js
+++ b/src/headless/shared/api/promise.js
@@ -1,6 +1,7 @@
import _converse from '../_converse.js';
import { getOpenPromise } from '@converse/openpromise';
-import { waitUntil, isFunction } from '../../utils/core.js';
+import { waitUntil } from '../../utils/promise.js';
+import { isFunction } from '../../utils/object.js';
export default {
/**
diff --git a/src/headless/shared/api/public.js b/src/headless/shared/api/public.js
index 87cd9c5672..1772f7f0df 100644
--- a/src/headless/shared/api/public.js
+++ b/src/headless/shared/api/public.js
@@ -5,8 +5,9 @@ import dayjs from 'dayjs';
import i18n from '../i18n';
import log from '../../log.js';
import sizzle from 'sizzle';
-import u, { setUnloadEvent } from '../../utils/core.js';
+import u from '../../utils/index.js';
import { ANONYMOUS, CHAT_STATES, KEYCODES, VERSION_NAME } from '../constants.js';
+import { setUnloadEvent, isTestEnv } from '../../utils/session.js';
import { Collection } from "@converse/skeletor/src/collection";
import { Model } from '@converse/skeletor/src/model.js';
import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js';
@@ -124,7 +125,7 @@ export const converse = Object.assign(window.converse || {}, {
*/
api.trigger('initialized');
- if (_converse.isTestEnv()) {
+ if (isTestEnv()) {
return _converse;
}
},
diff --git a/src/headless/shared/api/user.js b/src/headless/shared/api/user.js
index 49ed271bef..a8ecb15b59 100644
--- a/src/headless/shared/api/user.js
+++ b/src/headless/shared/api/user.js
@@ -1,6 +1,7 @@
import _converse from '../_converse.js';
import presence_api from './presence.js';
-import u, { replacePromise } from '../../utils/core.js';
+import { isSameDomain } from '../../utils/jid.js';
+import { replacePromise } from '../../utils/session.js';
import { attemptNonPreboundSession, initConnection, setUserJID } from '../../utils/init.js';
import { getOpenPromise } from '@converse/openpromise';
import { user_settings_api } from '../settings/api.js';
@@ -46,7 +47,7 @@ export default {
async login (jid, password, automatic=false) {
const { api } = _converse;
jid = jid || api.settings.get('jid');
- if (!_converse.connection?.jid || (jid && !u.isSameDomain(_converse.connection.jid, jid))) {
+ if (!_converse.connection?.jid || (jid && !isSameDomain(_converse.connection.jid, jid))) {
initConnection();
}
if (api.settings.get("connection_options")?.worker && (await _converse.connection.restoreWorkerSession())) {
diff --git a/src/headless/shared/connection/index.js b/src/headless/shared/connection/index.js
index 7f13a9e2a1..8e25d47af4 100644
--- a/src/headless/shared/connection/index.js
+++ b/src/headless/shared/connection/index.js
@@ -6,7 +6,7 @@ import api from '../api/index.js';
import { ANONYMOUS, BOSH_WAIT, LOGOUT } from '../../shared/constants.js';
import { CONNECTION_STATUS } from '../constants';
import { Strophe } from 'strophe.js';
-import { clearSession, tearDown } from "../../utils/core.js";
+import { clearSession, tearDown } from "../../utils/session.js";
import { getOpenPromise } from '@converse/openpromise';
import { setUserJID, } from '../../utils/init.js';
diff --git a/src/headless/shared/parsers.js b/src/headless/shared/parsers.js
index 8f7aa4843e..bbb6ab6bb7 100644
--- a/src/headless/shared/parsers.js
+++ b/src/headless/shared/parsers.js
@@ -6,7 +6,7 @@ import log from '../log.js';
import sizzle from 'sizzle';
import { Strophe } from 'strophe.js';
import { URL_PARSE_OPTIONS } from './constants.js';
-import { decodeHTMLEntities } from '..//utils/core.js';
+import { decodeHTMLEntities } from '../utils/index.js';
import { rejectMessage } from './actions';
import {
isAudioURL,
diff --git a/src/headless/shared/settings/utils.js b/src/headless/shared/settings/utils.js
index e10820fd2b..eeff76606d 100644
--- a/src/headless/shared/settings/utils.js
+++ b/src/headless/shared/settings/utils.js
@@ -2,7 +2,7 @@ import _converse from '../_converse.js';
import isEqual from "lodash-es/isEqual.js";
import log from '../../log.js';
import pick from 'lodash-es/pick';
-import u from '../../utils/core';
+import { merge } from '../../utils/object.js';
import { DEFAULT_SETTINGS } from './constants.js';
import { Events } from '@converse/skeletor/src/events.js';
import { Model } from '@converse/skeletor/src/model.js';
@@ -38,13 +38,13 @@ export function getAppSetting (key) {
}
export function extendAppSettings (settings) {
- u.merge(DEFAULT_SETTINGS, settings);
+ merge(DEFAULT_SETTINGS, settings);
// When updating the settings, we need to avoid overwriting the
// initialization_settings (i.e. the settings passed in via converse.initialize).
const allowed_keys = Object.keys(settings).filter(k => k in DEFAULT_SETTINGS);
const allowed_site_settings = pick(init_settings, allowed_keys);
const updated_settings = Object.assign(pick(settings, allowed_keys), allowed_site_settings);
- u.merge(app_settings, updated_settings);
+ merge(app_settings, updated_settings);
}
/**
diff --git a/src/headless/utils/core.js b/src/headless/utils/core.js
deleted file mode 100644
index f817cd13a3..0000000000
--- a/src/headless/utils/core.js
+++ /dev/null
@@ -1,545 +0,0 @@
-/**
- * @copyright The Converse.js contributors
- * @license Mozilla Public License (MPLv2)
- * @description This is the core utilities module.
- */
-import DOMPurify from 'dompurify';
-import _converse from '../shared/_converse.js';
-import log from '../log.js';
-import sizzle from "sizzle";
-import { Model } from '@converse/skeletor/src/model.js';
-import { Strophe } from 'strophe.js';
-import { getOpenPromise } from '@converse/openpromise';
-import { settings_api } from '../shared/settings/api.js';
-import { stx , toStanza } from './stanza.js';
-import {
- getCurrentWord,
- getSelectValues,
- isMentionBoundary,
- placeCaretAtEnd,
- replaceCurrentWord,
- webForm2xForm
-} from './form.js';
-import {
- getOuterWidth,
- isElement,
- isTagEqual,
- queryChildren,
- stringToElement,
-} from './html.js';
-import {
- arrayBufferToHex,
- arrayBufferToString,
- stringToArrayBuffer,
- arrayBufferToBase64,
- base64ToArrayBuffer,
-} from './arraybuffer.js';
-import {
- isAudioURL,
- isGIFURL,
- isVideoURL,
- isImageURL,
- isURLWithImageExtension,
- checkFileTypes,
- getURI,
- shouldRenderMediaFromURL,
- isAllowedProtocolForMedia,
-} from './url.js';
-
-/**
- * The utils object
- * @namespace u
- */
-const u = {
- arrayBufferToBase64,
- arrayBufferToHex,
- arrayBufferToString,
- base64ToArrayBuffer,
- checkFileTypes,
- getSelectValues,
- getURI,
- isAllowedProtocolForMedia,
- isAudioURL,
- isGIFURL,
- isImageURL,
- isURLWithImageExtension,
- isVideoURL,
- shouldRenderMediaFromURL,
- stringToArrayBuffer,
- webForm2xForm,
-};
-
-export function isError (obj) {
- return Object.prototype.toString.call(obj) === "[object Error]";
-}
-
-export function isFunction (val) {
- return typeof val === 'function';
-}
-
-export function isEmptyMessage (attrs) {
- if (attrs instanceof Model) {
- attrs = attrs.attributes;
- }
- return !attrs['oob_url'] &&
- !attrs['file'] &&
- !(attrs['is_encrypted'] && attrs['plaintext']) &&
- !attrs['message'] &&
- !attrs['body'];
-}
-
-/**
- * We distinguish between UniView and MultiView instances.
- *
- * UniView means that only one chat is visible, even though there might be multiple ongoing chats.
- * MultiView means that multiple chats may be visible simultaneously.
- */
-export function isUniView () {
- return ['mobile', 'fullscreen', 'embedded'].includes(settings_api.get("view_mode"));
-}
-
-export function shouldClearCache () {
- const { api } = _converse;
- return !_converse.config.get('trusted') ||
- api.settings.get('clear_cache_on_logout') ||
- _converse.isTestEnv();
-}
-
-
-export async function tearDown () {
- const { api } = _converse;
- await api.trigger('beforeTearDown', {'synchronous': true});
- window.removeEventListener('click', _converse.onUserActivity);
- window.removeEventListener('focus', _converse.onUserActivity);
- window.removeEventListener('keypress', _converse.onUserActivity);
- window.removeEventListener('mousemove', _converse.onUserActivity);
- window.removeEventListener(_converse.unloadevent, _converse.onUserActivity);
- window.clearInterval(_converse.everySecondTrigger);
- api.trigger('afterTearDown');
- return _converse;
-}
-
-
-export function clearSession () {
- _converse.session?.destroy();
- delete _converse.session;
- shouldClearCache() && _converse.api.user.settings.clear();
- /**
- * Synchronouse event triggered once the user session has been cleared,
- * for example when the user has logged out or when Converse has
- * disconnected for some other reason.
- * @event _converse#clearSession
- */
- return _converse.api.trigger('clearSession', {'synchronous': true});
-}
-
-
-/**
- * Given a message object, return its text with @ chars
- * inserted before the mentioned nicknames.
- */
-export function prefixMentions (message) {
- let text = message.getMessageText();
- (message.get('references') || [])
- .sort((a, b) => b.begin - a.begin)
- .forEach(ref => {
- text = `${text.slice(0, ref.begin)}@${text.slice(ref.begin)}`
- });
- return text;
-}
-
-u.getJIDFromURI = function (jid) {
- return jid.startsWith('xmpp:') && jid.endsWith('?join')
- ? jid.replace(/^xmpp:/, '').replace(/\?join$/, '')
- : jid;
-}
-
-u.getLongestSubstring = function (string, candidates) {
- function reducer (accumulator, current_value) {
- if (string.startsWith(current_value)) {
- if (current_value.length > accumulator.length) {
- return current_value;
- } else {
- return accumulator;
- }
- } else {
- return accumulator;
- }
- }
- return candidates.reduce(reducer, '');
-}
-
-export function isValidJID (jid) {
- if (typeof jid === 'string') {
- return jid.split('@').filter(s => !!s).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
- }
- return false;
-}
-
-u.isValidMUCJID = function (jid) {
- return !jid.startsWith('@') && !jid.endsWith('@');
-};
-
-u.isSameBareJID = function (jid1, jid2) {
- if (typeof jid1 !== 'string' || typeof jid2 !== 'string') {
- return false;
- }
- return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
- Strophe.getBareJidFromJid(jid2).toLowerCase();
-};
-
-
-u.isSameDomain = function (jid1, jid2) {
- if (typeof jid1 !== 'string' || typeof jid2 !== 'string') {
- return false;
- }
- return Strophe.getDomainFromJid(jid1).toLowerCase() ===
- Strophe.getDomainFromJid(jid2).toLowerCase();
-};
-
-u.isNewMessage = function (message) {
- /* Given a stanza, determine whether it's a new
- * message, i.e. not a MAM archived one.
- */
- if (message instanceof Element) {
- return !(
- sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length &&
- sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, message).length
- );
- } else if (message instanceof Model) {
- message = message.attributes;
- }
- return !(message['is_delayed'] && message['is_archived']);
-};
-
-u.shouldCreateMessage = function (attrs) {
- return attrs['retracted'] || // Retraction received *before* the message
- !isEmptyMessage(attrs);
-}
-
-u.shouldCreateGroupchatMessage = function (attrs) {
- return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone);
-}
-
-u.isChatRoom = function (model) {
- return model && (model.get('type') === 'chatroom');
-}
-
-export function isErrorObject (o) {
- return o instanceof Error;
-}
-
-u.isErrorStanza = function (stanza) {
- if (!isElement(stanza)) {
- return false;
- }
- return stanza.getAttribute('type') === 'error';
-}
-
-u.isForbiddenError = function (stanza) {
- if (!isElement(stanza)) {
- return false;
- }
- return sizzle(`error[type="auth"] forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0;
-}
-
-u.isServiceUnavailableError = function (stanza) {
- if (!isElement(stanza)) {
- return false;
- }
- return sizzle(`error[type="cancel"] service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0;
-}
-
-/**
- * Merge the second object into the first one.
- * @method u#merge
- * @param {Object} dst
- * @param {Object} src
- */
-export function merge (dst, src) {
- for (const k in src) {
- if (!Object.prototype.hasOwnProperty.call(src, k)) continue;
- if (k === "__proto__" || k === "constructor") continue;
-
- if (dst[k] instanceof Object) {
- merge(dst[k], src[k]);
- } else {
- dst[k] = src[k];
- }
- }
-}
-
-
-u.contains = function (attr, query) {
- const checker = (item, key) => item.get(key).toLowerCase().includes(query.toLowerCase());
- return function (item) {
- if (typeof attr === 'object') {
- return Object.keys(attr).reduce((acc, k) => acc || checker(item, k), false);
- } else if (typeof attr === 'string') {
- return checker(item, attr);
- } else {
- throw new TypeError('contains: wrong attribute type. Must be string or array.');
- }
- };
-};
-
-u.getAttribute = function (key, item) {
- return item.get(key);
-};
-
-u.contains.not = function (attr, query) {
- return function (item) {
- return !(u.contains(attr, query)(item));
- };
-};
-
-u.isPersistableModel = function (model) {
- return model.collection && model.collection.browserStorage;
-};
-
-u.getResolveablePromise = getOpenPromise;
-u.getOpenPromise = getOpenPromise;
-
-/**
- * Call the callback once all the events have been triggered
- * @private
- * @method u#onMultipleEvents
- * @param { Array } events: An array of objects, with keys `object` and
- * `event`, representing the event name and the object it's triggered upon.
- * @param { Function } callback: The function to call once all events have
- * been triggered.
- */
-u.onMultipleEvents = function (events=[], callback) {
- let triggered = [];
-
- function handler (result) {
- triggered.push(result)
- if (events.length === triggered.length) {
- callback(triggered);
- triggered = [];
- }
- }
- events.forEach(e => e.object.on(e.event, handler));
-};
-
-
-export function safeSave (model, attributes, options) {
- if (u.isPersistableModel(model)) {
- model.save(attributes, options);
- } else {
- model.set(attributes, options);
- }
-}
-
-
-u.siblingIndex = function (el) {
- /* eslint-disable no-cond-assign */
- for (var i = 0; el = el.previousElementSibling; i++);
- return i;
-};
-
-/**
- * @param {Element} el
- * @param {string} name
- * @param {string} [type]
- * @param {boolean} [bubbles]
- * @param {boolean} [cancelable]
- */
-function triggerEvent (el, name, type="Event", bubbles=true, cancelable=true) {
- const evt = document.createEvent(type);
- evt.initEvent(name, bubbles, cancelable);
- el.dispatchEvent(evt);
-}
-
-export function getRandomInt (max) {
- return (Math.random() * max) | 0;
-}
-
-/**
- * @param {string} [suffix]
- * @return {string}
- */
-export function getUniqueId (suffix) {
- const uuid = crypto.randomUUID?.() ??
- 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
- const r = getRandomInt(16);
- const v = c === 'x' ? r : r & 0x3 | 0x8;
- return v.toString(16);
- });
- if (typeof(suffix) === "string" || typeof(suffix) === "number") {
- return uuid + ":" + suffix;
- } else {
- return uuid;
- }
-}
-
-
-/**
- * Clears the specified timeout and interval.
- * @method u#clearTimers
- * @param {ReturnType} timeout - Id if the timeout to clear.
- * @param {ReturnType} interval - Id of the interval to clear.
- * @copyright Simen Bekkhus 2016
- * @license MIT
- */
-function clearTimers(timeout, interval) {
- clearTimeout(timeout);
- clearInterval(interval);
-}
-
-
-/**
- * Creates a {@link Promise} that resolves if the passed in function returns a truthy value.
- * Rejects if it throws or does not return truthy within the given max_wait.
- * @method u#waitUntil
- * @param { Function } func - The function called every check_delay,
- * and the result of which is the resolved value of the promise.
- * @param { number } [max_wait=300] - The time to wait before rejecting the promise.
- * @param { number } [check_delay=3] - The time to wait before each invocation of {func}.
- * @returns {Promise} A promise resolved with the value of func,
- * or rejected with the exception thrown by it or it times out.
- * @copyright Simen Bekkhus 2016
- * @license MIT
- */
-export function waitUntil (func, max_wait=300, check_delay=3) {
- // Run the function once without setting up any listeners in case it's already true
- try {
- const result = func();
- if (result) {
- return Promise.resolve(result);
- }
- } catch (e) {
- return Promise.reject(e);
- }
-
- const promise = getOpenPromise();
- const timeout_err = new Error();
-
- function checker () {
- try {
- const result = func();
- if (result) {
- clearTimers(max_wait_timeout, interval);
- promise.resolve(result);
- }
- } catch (e) {
- clearTimers(max_wait_timeout, interval);
- promise.reject(e);
- }
- }
-
- const interval = setInterval(checker, check_delay);
-
- function handler () {
- clearTimers(max_wait_timeout, interval);
- const err_msg = `Wait until promise timed out: \n\n${timeout_err.stack}`;
- console.trace();
- log.error(err_msg);
- promise.reject(new Error(err_msg));
- }
-
- const max_wait_timeout = setTimeout(handler, max_wait);
-
- 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 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 function saveWindowState (ev) {
- // XXX: eventually we should be able to just use
- // document.visibilityState (when we drop support for older
- // browsers).
- let state;
- const event_map = {
- 'focus': "visible",
- 'focusin': "visible",
- 'pageshow': "visible",
- 'blur': "hidden",
- 'focusout': "hidden",
- 'pagehide': "hidden"
- };
- ev = ev || document.createEvent('Events');
- if (ev.type in event_map) {
- state = event_map[ev.type];
- } else {
- state = document.hidden ? "hidden" : "visible";
- }
- _converse.windowState = state;
- /**
- * Triggered when window state has changed.
- * Used to determine when a user left the page and when came back.
- * @event _converse#windowStateChanged
- * @type { object }
- * @property{ string } state - Either "hidden" or "visible"
- * @example _converse.api.listen.on('windowStateChanged', obj => { ... });
- */
- _converse.api.trigger('windowStateChanged', {state});
-}
-
-
-export default Object.assign({
- getCurrentWord,
- getOuterWidth,
- getRandomInt,
- isMentionBoundary,
- getUniqueId,
- isElement,
- isEmptyMessage,
- isErrorObject,
- isTagEqual,
- isValidJID,
- merge,
- placeCaretAtEnd,
- prefixMentions,
- queryChildren,
- replaceCurrentWord,
- safeSave,
- saveWindowState,
- shouldClearCache,
- stringToElement,
- stx,
- toStanza,
- triggerEvent,
- waitUntil, // TODO: remove. Only the API should be used
-}, u);
diff --git a/src/headless/utils/index.js b/src/headless/utils/index.js
new file mode 100644
index 0000000000..add8d3fea5
--- /dev/null
+++ b/src/headless/utils/index.js
@@ -0,0 +1,295 @@
+/**
+ * @copyright The Converse.js contributors
+ * @license Mozilla Public License (MPLv2)
+ * @description This is the core utilities module.
+ */
+import DOMPurify from 'dompurify';
+import sizzle from "sizzle";
+import { Model } from '@converse/skeletor/src/model.js';
+import { Strophe } from 'strophe.js';
+import { getOpenPromise } from '@converse/openpromise';
+import { stx , toStanza } from './stanza.js';
+import { saveWindowState, shouldClearCache } from './session.js';
+import { merge, isError, isFunction } from './object.js';
+import { createStore, getDefaultStore } from './storage.js';
+import { waitUntil } from './promise.js';
+import { isValidJID, isValidMUCJID, isSameBareJID } from './jid.js';
+import {
+ getCurrentWord,
+ getSelectValues,
+ isMentionBoundary,
+ placeCaretAtEnd,
+ replaceCurrentWord,
+ webForm2xForm
+} from './form.js';
+import {
+ getOuterWidth,
+ isElement,
+ isTagEqual,
+ queryChildren,
+ stringToElement,
+} from './html.js';
+import {
+ arrayBufferToHex,
+ arrayBufferToString,
+ stringToArrayBuffer,
+ arrayBufferToBase64,
+ base64ToArrayBuffer,
+} from './arraybuffer.js';
+import {
+ isAudioURL,
+ isGIFURL,
+ isVideoURL,
+ isImageURL,
+ isURLWithImageExtension,
+ checkFileTypes,
+ getURI,
+ shouldRenderMediaFromURL,
+ isAllowedProtocolForMedia,
+} from './url.js';
+
+
+/**
+ * The utils object
+ * @namespace u
+ */
+const u = {
+ arrayBufferToBase64,
+ arrayBufferToHex,
+ arrayBufferToString,
+ base64ToArrayBuffer,
+ checkFileTypes,
+ getSelectValues,
+ getURI,
+ isAllowedProtocolForMedia,
+ isAudioURL,
+ isError,
+ isFunction,
+ isGIFURL,
+ isImageURL,
+ isURLWithImageExtension,
+ isVideoURL,
+ shouldRenderMediaFromURL,
+ stringToArrayBuffer,
+ webForm2xForm,
+};
+
+
+export function isEmptyMessage (attrs) {
+ if (attrs instanceof Model) {
+ attrs = attrs.attributes;
+ }
+ return !attrs['oob_url'] &&
+ !attrs['file'] &&
+ !(attrs['is_encrypted'] && attrs['plaintext']) &&
+ !attrs['message'] &&
+ !attrs['body'];
+}
+
+/**
+ * Given a message object, return its text with @ chars
+ * inserted before the mentioned nicknames.
+ */
+export function prefixMentions (message) {
+ let text = message.getMessageText();
+ (message.get('references') || [])
+ .sort((a, b) => b.begin - a.begin)
+ .forEach(ref => {
+ text = `${text.slice(0, ref.begin)}@${text.slice(ref.begin)}`
+ });
+ return text;
+}
+
+u.getLongestSubstring = function (string, candidates) {
+ function reducer (accumulator, current_value) {
+ if (string.startsWith(current_value)) {
+ if (current_value.length > accumulator.length) {
+ return current_value;
+ } else {
+ return accumulator;
+ }
+ } else {
+ return accumulator;
+ }
+ }
+ return candidates.reduce(reducer, '');
+}
+
+u.isNewMessage = function (message) {
+ /* Given a stanza, determine whether it's a new
+ * message, i.e. not a MAM archived one.
+ */
+ if (message instanceof Element) {
+ return !(
+ sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length &&
+ sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, message).length
+ );
+ } else if (message instanceof Model) {
+ message = message.attributes;
+ }
+ return !(message['is_delayed'] && message['is_archived']);
+};
+
+u.shouldCreateMessage = function (attrs) {
+ return attrs['retracted'] || // Retraction received *before* the message
+ !isEmptyMessage(attrs);
+}
+
+u.shouldCreateGroupchatMessage = function (attrs) {
+ return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone);
+}
+
+u.isChatRoom = function (model) {
+ return model && (model.get('type') === 'chatroom');
+}
+
+export function isErrorObject (o) {
+ return o instanceof Error;
+}
+
+u.isErrorStanza = function (stanza) {
+ if (!isElement(stanza)) {
+ return false;
+ }
+ return stanza.getAttribute('type') === 'error';
+}
+
+u.isForbiddenError = function (stanza) {
+ if (!isElement(stanza)) {
+ return false;
+ }
+ return sizzle(`error[type="auth"] forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0;
+}
+
+u.isServiceUnavailableError = function (stanza) {
+ if (!isElement(stanza)) {
+ return false;
+ }
+ return sizzle(`error[type="cancel"] service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0;
+}
+
+u.getAttribute = function (key, item) {
+ return item.get(key);
+};
+
+u.isPersistableModel = function (model) {
+ return model.collection && model.collection.browserStorage;
+};
+
+u.getResolveablePromise = getOpenPromise;
+u.getOpenPromise = getOpenPromise;
+
+/**
+ * Call the callback once all the events have been triggered
+ * @private
+ * @method u#onMultipleEvents
+ * @param { Array } events: An array of objects, with keys `object` and
+ * `event`, representing the event name and the object it's triggered upon.
+ * @param { Function } callback: The function to call once all events have
+ * been triggered.
+ */
+u.onMultipleEvents = function (events=[], callback) {
+ let triggered = [];
+
+ function handler (result) {
+ triggered.push(result)
+ if (events.length === triggered.length) {
+ callback(triggered);
+ triggered = [];
+ }
+ }
+ events.forEach(e => e.object.on(e.event, handler));
+};
+
+
+export function safeSave (model, attributes, options) {
+ if (u.isPersistableModel(model)) {
+ model.save(attributes, options);
+ } else {
+ model.set(attributes, options);
+ }
+}
+
+
+u.siblingIndex = function (el) {
+ /* eslint-disable no-cond-assign */
+ for (var i = 0; el = el.previousElementSibling; i++);
+ return i;
+};
+
+/**
+ * @param {Element} el
+ * @param {string} name
+ * @param {string} [type]
+ * @param {boolean} [bubbles]
+ * @param {boolean} [cancelable]
+ */
+function triggerEvent (el, name, type="Event", bubbles=true, cancelable=true) {
+ const evt = document.createEvent(type);
+ evt.initEvent(name, bubbles, cancelable);
+ el.dispatchEvent(evt);
+}
+
+export function getRandomInt (max) {
+ return (Math.random() * max) | 0;
+}
+
+/**
+ * @param {string} [suffix]
+ * @return {string}
+ */
+export function getUniqueId (suffix) {
+ const uuid = crypto.randomUUID?.() ??
+ 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+ const r = getRandomInt(16);
+ const v = c === 'x' ? r : r & 0x3 | 0x8;
+ return v.toString(16);
+ });
+ if (typeof(suffix) === "string" || typeof(suffix) === "number") {
+ return uuid + ":" + suffix;
+ } else {
+ return uuid;
+ }
+}
+
+
+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 Object.assign({
+ createStore,
+ getCurrentWord,
+ getDefaultStore,
+ getOuterWidth,
+ getRandomInt,
+ getUniqueId,
+ isElement,
+ isEmptyMessage,
+ isErrorObject,
+ isMentionBoundary,
+ isSameBareJID,
+ isTagEqual,
+ isValidJID,
+ isValidMUCJID,
+ merge,
+ placeCaretAtEnd,
+ prefixMentions,
+ queryChildren,
+ replaceCurrentWord,
+ safeSave,
+ saveWindowState,
+ shouldClearCache,
+ stringToElement,
+ stx,
+ toStanza,
+ triggerEvent,
+ waitUntil, // TODO: remove. Only the API should be used
+}, u);
diff --git a/src/headless/utils/init.js b/src/headless/utils/init.js
index 39969fa531..e265b33c89 100644
--- a/src/headless/utils/init.js
+++ b/src/headless/utils/init.js
@@ -9,7 +9,8 @@ import { Connection, MockConnection } from '../shared/connection/index.js';
import { Model } from '@converse/skeletor/src/model.js';
import { Strophe } from 'strophe.js';
import { createStore, initStorage } from './storage.js';
-import { saveWindowState, isValidJID } from './core.js';
+import { isValidJID } from './jid.js';
+import { saveWindowState, isTestEnv } from './session.js';
function setUpXMLLogging () {
@@ -48,7 +49,7 @@ export function initConnection () {
}
}
- const XMPPConnection = _converse.isTestEnv() ? MockConnection : Connection;
+ const XMPPConnection = isTestEnv() ? MockConnection : Connection;
_converse.connection = new XMPPConnection(
getConnectionServiceURL(),
Object.assign(
@@ -139,7 +140,7 @@ export async function initSessionStorage (_converse) {
await Storage.sessionStorageInitialized;
_converse.storage = {
'session': Storage.localForage.createInstance({
- 'name': _converse.isTestEnv() ? 'converse-test-session' : 'converse-session',
+ 'name': isTestEnv() ? 'converse-test-session' : 'converse-session',
'description': 'sessionStorage instance',
'driver': ['sessionStorageWrapper']
})
@@ -166,7 +167,7 @@ function initPersistentStorage (_converse, store_name) {
}
const config = {
- 'name': _converse.isTestEnv() ? 'converse-test-persistent' : 'converse-persistent',
+ 'name': isTestEnv() ? 'converse-test-persistent' : 'converse-persistent',
'storeName': store_name
}
if (_converse.api.settings.get("persistent_store") === 'localStorage') {
@@ -409,12 +410,12 @@ export async function attemptNonPreboundSession (credentials, automatic) {
if (credentials) return connect(credentials);
}
- if (!_converse.isTestEnv() && 'credentials' in navigator) {
+ if (!isTestEnv() && 'credentials' in navigator) {
const credentials = await getLoginCredentialsFromBrowser();
if (credentials) return connect(credentials);
}
- if (!_converse.isTestEnv()) log.warn("attemptNonPreboundSession: Couldn't find credentials to log in with");
+ if (!isTestEnv()) log.warn("attemptNonPreboundSession: Couldn't find credentials to log in with");
} else if (
[ANONYMOUS, EXTERNAL].includes(api.settings.get("authentication")) &&
diff --git a/src/headless/utils/jid.js b/src/headless/utils/jid.js
new file mode 100644
index 0000000000..0c4afe28c4
--- /dev/null
+++ b/src/headless/utils/jid.js
@@ -0,0 +1,32 @@
+import { Strophe } from 'strophe.js';
+
+export function isValidJID (jid) {
+ if (typeof jid === 'string') {
+ return jid.split('@').filter((s) => !!s).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
+ }
+ return false;
+}
+
+export function isValidMUCJID (jid) {
+ return !jid.startsWith('@') && !jid.endsWith('@');
+}
+
+export function isSameBareJID (jid1, jid2) {
+ if (typeof jid1 !== 'string' || typeof jid2 !== 'string') {
+ return false;
+ }
+ return Strophe.getBareJidFromJid(jid1).toLowerCase() === Strophe.getBareJidFromJid(jid2).toLowerCase();
+}
+
+export function isSameDomain (jid1, jid2) {
+ if (typeof jid1 !== 'string' || typeof jid2 !== 'string') {
+ return false;
+ }
+ return Strophe.getDomainFromJid(jid1).toLowerCase() === Strophe.getDomainFromJid(jid2).toLowerCase();
+}
+
+export function getJIDFromURI (jid) {
+ return jid.startsWith('xmpp:') && jid.endsWith('?join')
+ ? jid.replace(/^xmpp:/, '').replace(/\?join$/, '')
+ : jid;
+}
diff --git a/src/headless/utils/object.js b/src/headless/utils/object.js
new file mode 100644
index 0000000000..6e0fff6b5f
--- /dev/null
+++ b/src/headless/utils/object.js
@@ -0,0 +1,25 @@
+/**
+ * Merge the second object into the first one.
+ * @param {Object} dst
+ * @param {Object} src
+ */
+export function merge (dst, src) {
+ for (const k in src) {
+ if (!Object.prototype.hasOwnProperty.call(src, k)) continue;
+ if (k === '__proto__' || k === 'constructor') continue;
+
+ if (dst[k] instanceof Object) {
+ merge(dst[k], src[k]);
+ } else {
+ dst[k] = src[k];
+ }
+ }
+}
+
+export function isError (obj) {
+ return Object.prototype.toString.call(obj) === '[object Error]';
+}
+
+export function isFunction (val) {
+ return typeof val === 'function';
+}
diff --git a/src/headless/utils/promise.js b/src/headless/utils/promise.js
new file mode 100644
index 0000000000..b3622f0998
--- /dev/null
+++ b/src/headless/utils/promise.js
@@ -0,0 +1,69 @@
+import log from '../log.js';
+import { getOpenPromise } from '@converse/openpromise';
+
+/**
+ * Clears the specified timeout and interval.
+ * @method u#clearTimers
+ * @param {ReturnType} timeout - Id if the timeout to clear.
+ * @param {ReturnType} interval - Id of the interval to clear.
+ * @copyright Simen Bekkhus 2016
+ * @license MIT
+ */
+function clearTimers(timeout, interval) {
+ clearTimeout(timeout);
+ clearInterval(interval);
+}
+
+/**
+ * Creates a {@link Promise} that resolves if the passed in function returns a truthy value.
+ * Rejects if it throws or does not return truthy within the given max_wait.
+ * @param { Function } func - The function called every check_delay,
+ * and the result of which is the resolved value of the promise.
+ * @param { number } [max_wait=300] - The time to wait before rejecting the promise.
+ * @param { number } [check_delay=3] - The time to wait before each invocation of {func}.
+ * @returns {Promise} A promise resolved with the value of func,
+ * or rejected with the exception thrown by it or it times out.
+ * @copyright Simen Bekkhus 2016
+ * @license MIT
+ */
+export function waitUntil (func, max_wait=300, check_delay=3) {
+ // Run the function once without setting up any listeners in case it's already true
+ try {
+ const result = func();
+ if (result) {
+ return Promise.resolve(result);
+ }
+ } catch (e) {
+ return Promise.reject(e);
+ }
+
+ const promise = getOpenPromise();
+ const timeout_err = new Error();
+
+ function checker () {
+ try {
+ const result = func();
+ if (result) {
+ clearTimers(max_wait_timeout, interval);
+ promise.resolve(result);
+ }
+ } catch (e) {
+ clearTimers(max_wait_timeout, interval);
+ promise.reject(e);
+ }
+ }
+
+ const interval = setInterval(checker, check_delay);
+
+ function handler () {
+ clearTimers(max_wait_timeout, interval);
+ const err_msg = `Wait until promise timed out: \n\n${timeout_err.stack}`;
+ console.trace();
+ log.error(err_msg);
+ promise.reject(new Error(err_msg));
+ }
+
+ const max_wait_timeout = setTimeout(handler, max_wait);
+
+ return promise;
+}
diff --git a/src/headless/utils/session.js b/src/headless/utils/session.js
new file mode 100644
index 0000000000..827d07a1dd
--- /dev/null
+++ b/src/headless/utils/session.js
@@ -0,0 +1,113 @@
+import _converse from '../shared/_converse.js';
+import log from '../log.js';
+import { getOpenPromise } from '@converse/openpromise';
+import { settings_api } from '../shared/settings/api.js';
+import { getInitSettings } from '../shared/settings/utils.js';
+
+/**
+ * We distinguish between UniView and MultiView instances.
+ *
+ * UniView means that only one chat is visible, even though there might be multiple ongoing chats.
+ * MultiView means that multiple chats may be visible simultaneously.
+ */
+export function isUniView () {
+ return ['mobile', 'fullscreen', 'embedded'].includes(settings_api.get("view_mode"));
+}
+
+export function isTestEnv () {
+ return getInitSettings()['bosh_service_url'] === 'montague.lit/http-bind';
+}
+
+export function saveWindowState (ev) {
+ // XXX: eventually we should be able to just use
+ // document.visibilityState (when we drop support for older
+ // browsers).
+ let state;
+ const event_map = {
+ 'focus': "visible",
+ 'focusin': "visible",
+ 'pageshow': "visible",
+ 'blur': "hidden",
+ 'focusout': "hidden",
+ 'pagehide': "hidden"
+ };
+ ev = ev || document.createEvent('Events');
+ if (ev.type in event_map) {
+ state = event_map[ev.type];
+ } else {
+ state = document.hidden ? "hidden" : "visible";
+ }
+ _converse.windowState = state;
+ /**
+ * Triggered when window state has changed.
+ * Used to determine when a user left the page and when came back.
+ * @event _converse#windowStateChanged
+ * @type { object }
+ * @property{ string } state - Either "hidden" or "visible"
+ * @example _converse.api.listen.on('windowStateChanged', obj => { ... });
+ */
+ _converse.api.trigger('windowStateChanged', {state});
+}
+
+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 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}"`);
+ }
+}
+
+export function shouldClearCache () {
+ const { api } = _converse;
+ return !_converse.config.get('trusted') ||
+ api.settings.get('clear_cache_on_logout') ||
+ isTestEnv();
+}
+
+
+export async function tearDown () {
+ const { api } = _converse;
+ await api.trigger('beforeTearDown', {'synchronous': true});
+ window.removeEventListener('click', _converse.onUserActivity);
+ window.removeEventListener('focus', _converse.onUserActivity);
+ window.removeEventListener('keypress', _converse.onUserActivity);
+ window.removeEventListener('mousemove', _converse.onUserActivity);
+ window.removeEventListener(_converse.unloadevent, _converse.onUserActivity);
+ window.clearInterval(_converse.everySecondTrigger);
+ api.trigger('afterTearDown');
+ return _converse;
+}
+
+
+export function clearSession () {
+ _converse.session?.destroy();
+ delete _converse.session;
+ shouldClearCache() && _converse.api.user.settings.clear();
+ /**
+ * Synchronouse event triggered once the user session has been cleared,
+ * for example when the user has logged out or when Converse has
+ * disconnected for some other reason.
+ * @event _converse#clearSession
+ */
+ return _converse.api.trigger('clearSession', {'synchronous': true});
+}
diff --git a/src/i18n/index.js b/src/i18n/index.js
index b2b6adc7ed..348e9b50e1 100644
--- a/src/i18n/index.js
+++ b/src/i18n/index.js
@@ -6,6 +6,7 @@
*/
import Jed from 'jed';
import { _converse, api, converse, log, i18n } from '@converse/headless';
+import { isTestEnv } from '@converse/headless/utils/session';
const { dayjs } = converse.env;
@@ -120,7 +121,7 @@ Object.assign(i18n, {
},
async initialize () {
- if (_converse.isTestEnv()) {
+ if (isTestEnv()) {
_converse.locale = 'en';
} else {
try {
diff --git a/src/plugins/chatview/message-form.js b/src/plugins/chatview/message-form.js
index c84f5a26f7..3157ee2e17 100644
--- a/src/plugins/chatview/message-form.js
+++ b/src/plugins/chatview/message-form.js
@@ -3,7 +3,7 @@ import { ElementView } from '@converse/skeletor/src/element.js';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless";
import { parseMessageForCommands } from './utils.js';
-import { prefixMentions } from '@converse/headless/utils/core.js';
+import { prefixMentions } from '@converse/headless/utils/index.js';
const { u } = converse.env;
diff --git a/src/plugins/controlbox/tests/login.js b/src/plugins/controlbox/tests/login.js
index cd8a25ba30..ab7d79c778 100644
--- a/src/plugins/controlbox/tests/login.js
+++ b/src/plugins/controlbox/tests/login.js
@@ -25,15 +25,15 @@ describe("The Login Form", function () {
cbview.querySelector('input[name="password"]').value = 'secret';
expect(_converse.config.get('trusted')).toBe(true);
- expect(_converse.getDefaultStore()).toBe('persistent');
+ expect(u.getDefaultStore()).toBe('persistent');
cbview.querySelector('input[type="submit"]').click();
expect(_converse.config.get('trusted')).toBe(true);
- expect(_converse.getDefaultStore()).toBe('persistent');
+ expect(u.getDefaultStore()).toBe('persistent');
checkbox.click();
cbview.querySelector('input[type="submit"]').click();
expect(_converse.config.get('trusted')).toBe(false);
- expect(_converse.getDefaultStore()).toBe('session');
+ expect(u.getDefaultStore()).toBe('session');
}));
it("checkbox can be set to false by default",
@@ -60,11 +60,11 @@ describe("The Login Form", function () {
cbview.querySelector('input[type="submit"]').click();
expect(_converse.config.get('trusted')).toBe(false);
- expect(_converse.getDefaultStore()).toBe('session');
+ expect(u.getDefaultStore()).toBe('session');
checkbox.click();
cbview.querySelector('input[type="submit"]').click();
expect(_converse.config.get('trusted')).toBe(true);
- expect(_converse.getDefaultStore()).toBe('persistent');
+ expect(u.getDefaultStore()).toBe('persistent');
}));
});
diff --git a/src/plugins/fullscreen/index.js b/src/plugins/fullscreen/index.js
index 5846bba947..3a096f7bc2 100644
--- a/src/plugins/fullscreen/index.js
+++ b/src/plugins/fullscreen/index.js
@@ -4,7 +4,7 @@
* @copyright 2022, the Converse.js contributors
*/
import { api, converse } from "@converse/headless";
-import { isUniView } from '@converse/headless/utils/core.js';
+import { isUniView } from '@converse/headless/utils/session.js';
import './styles/fullscreen.scss';
diff --git a/src/plugins/minimize/utils.js b/src/plugins/minimize/utils.js
index 8e4d332e1d..590e853154 100644
--- a/src/plugins/minimize/utils.js
+++ b/src/plugins/minimize/utils.js
@@ -1,5 +1,6 @@
import { _converse, api, converse } from '@converse/headless';
import { __ } from 'i18n';
+import { isTestEnv } from '@converse/headless/utils/session.js';
const { dayjs, u } = converse.env;
@@ -61,7 +62,7 @@ function getBoxesWidth (newchat) {
* @param { _converse.ChatBoxView|_converse.ChatRoomView|_converse.ControlBoxView|_converse.HeadlinesFeedView } [newchat]
*/
export function trimChats (newchat) {
- if (_converse.isTestEnv() || api.settings.get('no_trimming') || api.settings.get("view_mode") !== 'overlayed') {
+ if (isTestEnv() || api.settings.get('no_trimming') || api.settings.get("view_mode") !== 'overlayed') {
return;
}
const shown_chats = getShownChats();
diff --git a/src/plugins/muc-views/role-form.js b/src/plugins/muc-views/role-form.js
index d68ea17b6d..4c0e342174 100644
--- a/src/plugins/muc-views/role-form.js
+++ b/src/plugins/muc-views/role-form.js
@@ -2,7 +2,7 @@ import tplRoleForm from './templates/role-form.js';
import { CustomElement } from 'shared/components/element.js';
import { __ } from 'i18n';
import { api, converse, log } from '@converse/headless';
-import { isErrorObject } from '@converse/headless/utils/core.js';
+import { isErrorObject } from '@converse/headless/utils/index.js';
const { Strophe, sizzle } = converse.env;
diff --git a/src/plugins/notifications/utils.js b/src/plugins/notifications/utils.js
index 5603cc21ab..3ed15c0bd8 100644
--- a/src/plugins/notifications/utils.js
+++ b/src/plugins/notifications/utils.js
@@ -1,7 +1,8 @@
import Favico from 'favico.js-slevomat';
import { __ } from 'i18n';
import { _converse, api, converse, log } from '@converse/headless';
-import { isEmptyMessage } from '@converse/headless/utils/core.js';
+import { isEmptyMessage } from '@converse/headless/utils/index.js';
+import { isTestEnv } from '@converse/headless/utils/session.js';
const { Strophe } = converse.env;
const supports_html5_notification = 'Notification' in window;
@@ -12,11 +13,11 @@ let favicon;
export function isMessageToHiddenChat (attrs) {
- return _converse.isTestEnv() || (_converse.chatboxes.get(attrs.from)?.isHidden() ?? false);
+ return isTestEnv() || (_converse.chatboxes.get(attrs.from)?.isHidden() ?? false);
}
export function areDesktopNotificationsEnabled () {
- return _converse.isTestEnv() || (
+ return isTestEnv() || (
supports_html5_notification &&
api.settings.get('show_desktop_notifications') &&
Notification.permission === 'granted'
diff --git a/src/plugins/omemo/device.js b/src/plugins/omemo/device.js
index 725563d124..7c032a2959 100644
--- a/src/plugins/omemo/device.js
+++ b/src/plugins/omemo/device.js
@@ -2,7 +2,7 @@ import { IQError } from './errors.js';
import { Model } from '@converse/skeletor/src/model.js';
import { UNDECIDED } from './consts.js';
import { _converse, api, converse, log } from '@converse/headless';
-import { getRandomInt } from '@converse/headless/utils/core.js';
+import { getRandomInt } from '@converse/headless/utils/index.js';
import { parseBundle } from './utils.js';
const { Strophe, sizzle, $iq } = converse.env;
diff --git a/src/plugins/omemo/index.js b/src/plugins/omemo/index.js
index ac9e93aa2e..997c9e9220 100644
--- a/src/plugins/omemo/index.js
+++ b/src/plugins/omemo/index.js
@@ -13,7 +13,7 @@ import Devices from './devices.js';
import OMEMOStore from './store.js';
import omemo_api from './api.js';
import { _converse, api, converse, log } from '@converse/headless';
-import { shouldClearCache } from '@converse/headless/utils/core.js';
+import { shouldClearCache } from '@converse/headless/utils/session.js';
import {
createOMEMOMessageStanza,
encryptFile,
diff --git a/src/plugins/omemo/utils.js b/src/plugins/omemo/utils.js
index 048d3333b5..a52a19bd4c 100644
--- a/src/plugins/omemo/utils.js
+++ b/src/plugins/omemo/utils.js
@@ -9,7 +9,7 @@ import { __ } from 'i18n';
import { _converse, converse, api, log } from '@converse/headless';
import { html } from 'lit';
import { initStorage } from '@converse/headless/utils/storage.js';
-import { isError } from '@converse/headless/utils/core.js';
+import { isError } from '@converse/headless/utils/object.js';
import { isAudioURL, isImageURL, isVideoURL, getURI } from '@converse/headless/utils/url.js';
import { until } from 'lit/directives/until.js';
import {
diff --git a/src/plugins/roomslist/templates/roomslist.js b/src/plugins/roomslist/templates/roomslist.js
index 023e0063e0..e20fc5276a 100644
--- a/src/plugins/roomslist/templates/roomslist.js
+++ b/src/plugins/roomslist/templates/roomslist.js
@@ -3,7 +3,7 @@ import 'plugins/muc-views/modals/muc-list.js';
import { __ } from 'i18n';
import { _converse, api } from "@converse/headless";
import { html } from "lit";
-import { isUniView } from '@converse/headless/utils/core.js';
+import { isUniView } from '@converse/headless/utils/session.js';
import { addBookmarkViaEvent } from 'plugins/bookmark-views/utils.js';
diff --git a/src/plugins/rosterview/modals/add-contact.js b/src/plugins/rosterview/modals/add-contact.js
index f176220997..2c9854fff7 100644
--- a/src/plugins/rosterview/modals/add-contact.js
+++ b/src/plugins/rosterview/modals/add-contact.js
@@ -1,11 +1,10 @@
import 'shared/autocomplete/index.js';
import BaseModal from "plugins/modal/modal.js";
-import api from '@converse/headless/shared/api';
import debounce from 'lodash-es/debounce';
import tplAddContactModal from "./templates/add-contact.js";
import { Strophe } from 'strophe.js';
import { __ } from 'i18n';
-import { _converse } from "@converse/headless";
+import { _converse, api } from "@converse/headless";
import { addClass, removeClass } from 'utils/html.js';
export default class AddContactModal extends BaseModal {
diff --git a/src/plugins/rosterview/templates/group.js b/src/plugins/rosterview/templates/group.js
index 199c7c304a..577d78a8e5 100644
--- a/src/plugins/rosterview/templates/group.js
+++ b/src/plugins/rosterview/templates/group.js
@@ -2,7 +2,7 @@ import 'shared/components/icons.js';
import { __ } from 'i18n';
import { _converse, converse } from "@converse/headless";
import { html } from "lit";
-import { isUniView } from '@converse/headless/utils/core.js';
+import { isUniView } from '@converse/headless/utils/session.js';
import { repeat } from 'lit/directives/repeat.js';
import { toggleGroup } from '../utils.js';
diff --git a/src/templates/form_textarea.js b/src/templates/form_textarea.js
index 5ee7f7ec60..6346f871b4 100644
--- a/src/templates/form_textarea.js
+++ b/src/templates/form_textarea.js
@@ -1,5 +1,5 @@
import { html } from "lit";
-import u from '@converse/headless/utils/core.js';
+import u from '@converse/headless/utils/index.js';
export default (o) => {
const id = u.getUniqueId();
diff --git a/src/utils/html.js b/src/utils/html.js
index f4d792a6e7..b5952a5f80 100644
--- a/src/utils/html.js
+++ b/src/utils/html.js
@@ -15,7 +15,7 @@ import tplFormUrl from '../templates/form_url.js';
import tplFormUsername from '../templates/form_username.js';
import tplHyperlink from 'templates/hyperlink.js';
import tplVideo from 'templates/video.js';
-import u from '../headless/utils/core';
+import u from '../headless/utils/index.js';
import { converse, log } from '@converse/headless';
import { getURI, isAudioURL, isImageURL, isVideoURL } from '@converse/headless/utils/url.js';
import { render } from 'lit';