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';