diff --git a/package-lock.json b/package-lock.json index 790b83f822..c75e53f8b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1834,8 +1834,8 @@ }, "node_modules/@converse/skeletor": { "version": "0.0.8", - "resolved": "git+ssh://git@github.com/conversejs/skeletor.git#0cc6669bb2e5852caa1ef29892ce3822eadbff9a", - "integrity": "sha512-rnonEzuZPckk9OGv0WOTeB67suaDS0MIEWD/xg/SdXlZVfWEdHeAAasSUU5VQ+VcOdnx2axgVDK8UBq4qm6Y3A==", + "resolved": "git+ssh://git@github.com/conversejs/skeletor.git#26e12674e71475406b000b597dafc92dc6ce7737", + "integrity": "sha512-UCYgs8veAu24A4b5nQ1LaLYss8/J1IwKIaaeU61AFjCs+VVmHZCvqIECvy9V0ozbJvQrsEUf84V/qX4vjLj/Eg==", "license": "MIT", "dependencies": { "@converse/localforage-getitems": "1.4.3", @@ -10127,8 +10127,8 @@ }, "node_modules/strophe.js": { "version": "2.0.0", - "resolved": "git+ssh://git@github.com/strophe/strophejs.git#6b24a2a2121884b2d02aeb5756142f7dcaf05d9e", - "integrity": "sha512-YIK1PUyJEwZgiPk30cEtxhN5ifLzyLOnch9T6tw7812pO2yuaGdBEQcqGd8NQjH3LzdcoS/DZJnXFxj+QKyviQ==", + "resolved": "git+ssh://git@github.com/strophe/strophejs.git#d683aba6f5f0cd7aaa7fcb82b8a37792a3d5143c", + "integrity": "sha512-BHhBLPkVcKIDW9cA1uHVQWsSOuzppEm1jTqLez8n0BZF68M2gJhca1GbYDKAS/LOMnaia0LFyaY1nx22blqHhA==", "license": "MIT", "dependencies": { "abab": "^2.0.3" @@ -11419,7 +11419,7 @@ "license": "MPL-2.0", "dependencies": { "@converse/openpromise": "^0.0.1", - "@converse/skeletor": "conversejs/skeletor#0cc6669bb2e5852caa1ef29892ce3822eadbff9a", + "@converse/skeletor": "conversejs/skeletor#26e12674e71475406b000b597dafc92dc6ce7737", "dayjs": "^1.11.8", "dompurify": "^2.3.1", "filesize": "^10.0.7", @@ -11428,7 +11428,7 @@ "pluggable.js": "3.0.1", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "strophe/strophejs#6b24a2a2121884b2d02aeb5756142f7dcaf05d9e", + "strophe.js": "strophe/strophejs#d683aba6f5f0cd7aaa7fcb82b8a37792a3d5143c", "urijs": "^1.19.10" }, "devDependencies": {} diff --git a/src/headless/package.json b/src/headless/package.json index 346586b390..b8f26809dc 100644 --- a/src/headless/package.json +++ b/src/headless/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@converse/openpromise": "^0.0.1", - "@converse/skeletor": "conversejs/skeletor#0cc6669bb2e5852caa1ef29892ce3822eadbff9a", + "@converse/skeletor": "conversejs/skeletor#26e12674e71475406b000b597dafc92dc6ce7737", "dayjs": "^1.11.8", "dompurify": "^2.3.1", "filesize": "^10.0.7", @@ -41,7 +41,7 @@ "pluggable.js": "3.0.1", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "strophe/strophejs#6b24a2a2121884b2d02aeb5756142f7dcaf05d9e", + "strophe.js": "strophe/strophejs#d683aba6f5f0cd7aaa7fcb82b8a37792a3d5143c", "urijs": "^1.19.10" }, "devDependencies": {} diff --git a/src/headless/plugins/chat/message.js b/src/headless/plugins/chat/message.js index d988024758..001f347a05 100644 --- a/src/headless/plugins/chat/message.js +++ b/src/headless/plugins/chat/message.js @@ -70,7 +70,6 @@ class Message extends ModelWithContact { /** * Sets an auto-destruct timer for this message, if it's is_ephemeral. - * @private * @method _converse.Message#setTimerForEphemeralMessage */ setTimerForEphemeralMessage () { diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js index 2b62ff5f16..ddbbe6f351 100644 --- a/src/headless/plugins/chat/model.js +++ b/src/headless/plugins/chat/model.js @@ -38,18 +38,20 @@ class ChatBox extends ModelWithContact { defaults () { return { 'bookmarked': false, - 'chat_state': undefined, 'hidden': isUniView() && !api.settings.get('singleton'), 'message_type': 'chat', - 'nickname': undefined, 'num_unread': 0, 'time_opened': this.get('time_opened') || (new Date()).getTime(), 'time_sent': (new Date(0)).toISOString(), 'type': PRIVATE_CHAT_TYPE, - 'url': '' } } + constructor (attrs, options) { + super(attrs, options); + this.disable_mam = false; + } + async initialize () { super.initialize(); this.initialized = getOpenPromise(); @@ -70,7 +72,8 @@ class ChatBox extends ModelWithContact { this.initMessages(); if (this.get('type') === PRIVATE_CHAT_TYPE) { - this.presence = _converse.presences.get(jid) || _converse.presences.create({ jid }); + const { presences } = _converse.state; + this.presence = presences.get(jid) || presences.create({ jid }); await this.setRosterContact(jid); this.presence.on('change:show', item => this.onPresenceChanged(item)); } @@ -89,7 +92,7 @@ class ChatBox extends ModelWithContact { } getMessagesCollection () { - return new _converse.Messages(); + return new _converse.exports.Messages(); } getMessagesCacheKey () { @@ -514,11 +517,11 @@ class ChatBox extends ModelWithContact { } /** - * @private * @method ChatBox#shouldShowErrorMessage - * @returns {boolean} + * @param {object} attrs + * @returns {Promise} */ - shouldShowErrorMessage (attrs) { + async shouldShowErrorMessage (attrs) { const msg = this.getMessageReferencedByError(attrs); if (!msg && attrs.chat_state) { // If the error refers to a message not included in our store, @@ -528,7 +531,7 @@ class ChatBox extends ModelWithContact { return; } // Gets overridden in MUC - return true; + return Promise.resolve(true); } isSameUser (jid1, jid2) { @@ -567,7 +570,6 @@ class ChatBox extends ModelWithContact { /** * Handles message retraction based on the passed in attributes. - * @private * @method ChatBox#handleRetraction * @param {object} attrs - Attributes representing a received * message, as returned by {@link parseMessage} @@ -609,7 +611,7 @@ class ChatBox extends ModelWithContact { * @method ChatBox#getDuplicateMessage * @param {object} attrs - Attributes representing a received * message, as returned by {@link parseMessage} - * @returns {Promise} + * @returns {Message} */ getDuplicateMessage (attrs) { const queries = [ @@ -849,7 +851,7 @@ class ChatBox extends ModelWithContact { const body = text ? u.shortnamesToUnicode(text) : undefined; attrs = Object.assign({}, attrs, { 'from': _converse.session.get('bare_jid'), - 'fullname': _converse.xmppstatus.get('fullname'), + 'fullname': _converse.state.xmppstatus.get('fullname'), 'id': origin_id, 'is_only_emojis': text ? u.isOnlyEmojis(text) : false, 'jid': this.get('jid'), @@ -1056,9 +1058,12 @@ class ChatBox extends ModelWithContact { }); } + /** + * @param {boolean} force + */ maybeShow (force) { if (isUniView()) { - const filter = c => !c.get('hidden') && + const filter = (c) => !c.get('hidden') && c.get('jid') !== this.get('jid') && c.get('id') !== 'controlbox'; const other_chats = _converse.state.chatboxes.filter(filter); @@ -1088,7 +1093,6 @@ class ChatBox extends ModelWithContact { /** * Given a newly received {@link Message} instance, * update the unread counter if necessary. - * @private * @method ChatBox#handleUnreadMessage * @param {Message} message */ @@ -1111,6 +1115,9 @@ class ChatBox extends ModelWithContact { } } + /** + * @param {Message} message + */ incrementUnreadMsgsCounter (message) { const settings = { 'num_unread': this.get('num_unread') + 1 diff --git a/src/headless/plugins/chat/parsers.js b/src/headless/plugins/chat/parsers.js index 653b8fd149..b7b8cdc7a4 100644 --- a/src/headless/plugins/chat/parsers.js +++ b/src/headless/plugins/chat/parsers.js @@ -45,17 +45,19 @@ export async function parseMessage (stanza) { let to_jid = stanza.getAttribute('to'); const to_resource = Strophe.getResourceFromJid(to_jid); - if (api.settings.get('filter_by_resource') && to_resource && to_resource !== _converse.resource) { + const resource = _converse.session.get('resource'); + if (api.settings.get('filter_by_resource') && to_resource && to_resource !== resource) { return new StanzaParseError( `Ignoring incoming message intended for a different resource: ${to_jid}`, stanza ); } + const bare_jid = _converse.session.get('bare_jid'); const original_stanza = stanza; - let from_jid = stanza.getAttribute('from') || _converse.bare_jid; + let from_jid = stanza.getAttribute('from') || bare_jid; if (isCarbon(stanza)) { - if (from_jid === _converse.bare_jid) { + if (from_jid === bare_jid) { const selector = `[xmlns="${Strophe.NS.CARBONS}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`; stanza = sizzle(selector, stanza).pop(); to_jid = stanza.getAttribute('to'); @@ -69,7 +71,7 @@ export async function parseMessage (stanza) { const is_archived = isArchived(stanza); if (is_archived) { - if (from_jid === _converse.bare_jid) { + if (from_jid === bare_jid) { const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`; stanza = sizzle(selector, stanza).pop(); to_jid = stanza.getAttribute('to'); @@ -83,7 +85,7 @@ export async function parseMessage (stanza) { } const from_bare_jid = Strophe.getBareJidFromJid(from_jid); - const is_me = from_bare_jid === _converse.bare_jid; + const is_me = from_bare_jid === bare_jid; if (is_me && to_jid === null) { return new StanzaParseError( `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`, @@ -190,12 +192,12 @@ export async function parseMessage (stanza) { getCorrectionAttributes(stanza, original_stanza), getStanzaIDs(stanza, original_stanza), getRetractionAttributes(stanza, original_stanza), - getEncryptionAttributes(stanza, _converse) + getEncryptionAttributes(stanza) ); if (attrs.is_archived) { const from = original_stanza.getAttribute('from'); - if (from && from !== _converse.bare_jid) { + if (from && from !== bare_jid) { return new StanzaParseError(`Invalid Stanza: Forged MAM message from ${from}`, stanza); } } diff --git a/src/headless/plugins/chat/utils.js b/src/headless/plugins/chat/utils.js index 6dca04d188..c7fc90c336 100644 --- a/src/headless/plugins/chat/utils.js +++ b/src/headless/plugins/chat/utils.js @@ -1,3 +1,8 @@ +/** + * @typedef {import('./model.js').default} ChatBox + * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes + * @typedef {import('strophe.js').Builder} Builder + */ import sizzle from "sizzle"; import { Model } from '@converse/skeletor'; import _converse from '../../shared/_converse.js'; @@ -24,18 +29,23 @@ export function routeToChat (event) { export async function onClearSession () { if (shouldClearCache()) { + const { chatboxes } = _converse.state; await Promise.all( - _converse.chatboxes.map(c => c.messages && c.messages.clearStore({ 'silent': true })) + chatboxes.map(/** @param {ChatBox} c */(c) => c.messages?.clearStore({ 'silent': true })) ); - const filter = o => o.get('type') !== CONTROLBOX_TYPE; - _converse.chatboxes.clearStore({ 'silent': true }, filter); + chatboxes.clearStore( + { 'silent': true }, + /** @param {Model} o */(o) => o.get('type') !== CONTROLBOX_TYPE); } } + +/** + * Given a stanza, determine whether it's a new + * message, i.e. not a MAM archived one. + * @param {Element|Model|object} message + */ export function isNewMessage (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 && @@ -48,9 +58,13 @@ export function isNewMessage (message) { } +/** + * @param {Element} stanza + */ async function handleErrorMessage (stanza) { const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from')); - if (u.isSameBareJID(from_jid, _converse.bare_jid)) { + const bare_jid = _converse.session.get('bare_jid'); + if (u.isSameBareJID(from_jid, bare_jid)) { return; } const chatbox = await api.chatboxes.get(from_jid); @@ -62,8 +76,8 @@ async function handleErrorMessage (stanza) { export function autoJoinChats () { // Automatically join private chats, based on the // "auto_join_private_chats" configuration setting. - api.settings.get('auto_join_private_chats').forEach(jid => { - if (_converse.chatboxes.where({ 'jid': jid }).length) { + api.settings.get('auto_join_private_chats').forEach(/** @param {string} jid */(jid) => { + if (_converse.state.chatboxes.where({ 'jid': jid }).length) { return; } if (typeof jid === 'string') { @@ -85,7 +99,8 @@ export function autoJoinChats () { export function registerMessageHandlers () { api.connection.get().addHandler( - stanza => { + /** @param {Element} stanza */ + (stanza) => { if ( ['groupchat', 'error'].includes(stanza.getAttribute('type')) || isHeadline(stanza) || @@ -94,14 +109,15 @@ export function registerMessageHandlers () { ) { return true; } - return _converse.handleMessageStanza(stanza) || true; + return _converse.exports.handleMessageStanza(stanza) || true; }, null, 'message', ); api.connection.get().addHandler( - stanza => handleErrorMessage(stanza) || true, + /** @param {Element} stanza */ + (stanza) => handleErrorMessage(stanza) || true, null, 'message', 'error' @@ -111,10 +127,10 @@ export function registerMessageHandlers () { /** * Handler method for all incoming single-user chat "message" stanzas. - * @param { MessageAttributes } attrs - The message attributes + * @param {Element|Builder} stanza */ export async function handleMessageStanza (stanza) { - stanza = stanza.tree?.() ?? stanza; + stanza = (stanza instanceof Element) ? stanza : stanza.tree(); if (isServerMessage(stanza)) { // Prosody sends headline messages with type `chat`, so we need to filter them out here. @@ -136,19 +152,18 @@ export async function handleMessageStanza (stanza) { const chatbox = await api.chats.get(attrs.contact_jid, { 'nickname': attrs.nick }, has_body); await chatbox?.queueMessage(attrs); /** - * @typedef { Object } MessageData + * @typedef {Object} MessageData * An object containing the original message stanza, as well as the * parsed attributes. - * @property { Element } stanza - * @property { MessageAttributes } stanza - * @property { ChatBox } chatbox + * @property {Element} stanza + * @property {MessageAttributes} stanza + * @property {ChatBox} chatbox */ const data = { stanza, attrs, chatbox }; /** * Triggered when a message stanza is been received and processed. * @event _converse#message - * @type { object } - * @property { module:converse-chat~MessageData } data + * @type {MessageData} data */ api.trigger('message', data); } @@ -156,10 +171,10 @@ export async function handleMessageStanza (stanza) { /** * Ask the XMPP server to enable Message Carbons * See [XEP-0280](https://xmpp.org/extensions/xep-0280.html#enabling) - * @param { Boolean } reconnecting */ export async function enableCarbons () { - const domain = Strophe.getDomainFromJid(_converse.bare_jid); + const bare_jid = _converse.session.get('bare_jid'); + const domain = Strophe.getDomainFromJid(bare_jid); const supported = await api.disco.supports(Strophe.NS.CARBONS, domain); if (!supported) { diff --git a/src/headless/plugins/chatboxes/api.js b/src/headless/plugins/chatboxes/api.js index d79aff6e40..0f5e3842d8 100644 --- a/src/headless/plugins/chatboxes/api.js +++ b/src/headless/plugins/chatboxes/api.js @@ -1,10 +1,13 @@ +/** + * @typedef {import('@converse/skeletor').Model} Model + * @typedef {import('../chat/model.js').default} ChatBox + */ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import { createChatBox } from './utils.js'; const _chatBoxTypes = {}; -/** @typedef {import('@converse/skeletor').Model} Model */ /** * The "chatboxes" namespace. @@ -17,7 +20,7 @@ export default { * @method api.chatboxes.create * @param {string|string[]} jids - A JID or array of JIDs * @param {Object} attrs An object containing configuration attributes - * @param {Model} model - The type of chatbox that should be created + * @param {new (attrs: object, options: object) => ChatBox} model - The type of chatbox that should be created */ async create (jids=[], attrs={}, model) { await api.waitUntil('chatBoxesFetched'); @@ -34,13 +37,14 @@ export default { */ async get (jids) { await api.waitUntil('chatBoxesFetched'); + const { chatboxes } = _converse.state; if (jids === undefined) { - return _converse.chatboxes.models; + return chatboxes.models; } else if (typeof jids === 'string') { - return _converse.chatboxes.get(jids.toLowerCase()); + return chatboxes.get(jids.toLowerCase()); } else { jids = jids.map(j => j.toLowerCase()); - return _converse.chatboxes.models.filter(m => jids.includes(m.get('jid'))); + return chatboxes.models.filter(m => jids.includes(m.get('jid'))); } }, diff --git a/src/headless/plugins/chatboxes/chatboxes.js b/src/headless/plugins/chatboxes/chatboxes.js index e0640ac442..a7a5c61e9d 100644 --- a/src/headless/plugins/chatboxes/chatboxes.js +++ b/src/headless/plugins/chatboxes/chatboxes.js @@ -1,13 +1,24 @@ +/** + * @typedef {import('@converse/skeletor').Model} Model + */ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import { Collection } from "@converse/skeletor"; import { initStorage } from '../../utils/storage.js'; class ChatBoxes extends Collection { - get comparator () { - return 'time_opened'; + + /** + * @param {Model[]} models + * @param {object} options + */ + constructor (models, options) { + super(models, Object.assign({ comparator: 'time_opened' }, options)); } + /** + * @param {Collection} collection + */ onChatBoxesFetched (collection) { collection.filter(c => !c.isValid()).forEach(c => c.destroy()); /** @@ -22,15 +33,24 @@ class ChatBoxes extends Collection { api.trigger('chatBoxesFetched'); } + /** + * @param {boolean} reconnecting + */ onConnected (reconnecting) { - if (reconnecting) { return; } - initStorage(this, `converse.chatboxes-${_converse.bare_jid}`); + if (reconnecting) return; + + const bare_jid = _converse.session.get('bare_jid'); + initStorage(this, `converse.chatboxes-${bare_jid}`); this.fetch({ 'add': true, 'success': c => this.onChatBoxesFetched(c) }); } + /** + * @param {object} attrs + * @param {object} options + */ createModel (attrs, options) { if (!attrs.type) { throw new Error("You need to specify a type of chatbox to be created"); diff --git a/src/headless/plugins/chatboxes/utils.js b/src/headless/plugins/chatboxes/utils.js index 5f8d71c63e..351328df97 100644 --- a/src/headless/plugins/chatboxes/utils.js +++ b/src/headless/plugins/chatboxes/utils.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('../chat/model.js').default} ChatBox + */ import _converse from '../../shared/_converse.js'; import { converse } from '../../shared/api/index.js'; import log from "../../log"; @@ -5,12 +8,17 @@ import log from "../../log"; const { Strophe } = converse.env; +/** + * @param {string} jid + * @param {object} attrs + * @param {new (attrs: object, options: object) => ChatBox} Model + */ export async function createChatBox (jid, attrs, Model) { jid = Strophe.getBareJidFromJid(jid.toLowerCase()); Object.assign(attrs, {'jid': jid, 'id': jid}); let chatbox; try { - chatbox = new Model(attrs, {'collection': _converse.chatboxes}); + chatbox = new Model(attrs, {'collection': _converse.state.chatboxes}); } catch (e) { log.error(e); return null; @@ -20,6 +28,6 @@ export async function createChatBox (jid, attrs, Model) { chatbox.destroy(); return null; } - _converse.chatboxes.add(chatbox); + _converse.state.chatboxes.add(chatbox); return chatbox; } diff --git a/src/headless/plugins/disco/api.js b/src/headless/plugins/disco/api.js index 7dea0e5d6f..610681ef6d 100644 --- a/src/headless/plugins/disco/api.js +++ b/src/headless/plugins/disco/api.js @@ -30,16 +30,18 @@ export default { */ async getFeature (name, xmlns) { await api.waitUntil('streamFeaturesAdded'); + + const { stream_features } = _converse.state; if (!name || !xmlns) { throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature"); } - if (_converse.stream_features === undefined && !api.connection.connected()) { + if (stream_features === undefined && !api.connection.connected()) { // Happens during tests when disco lookups happen asynchronously after teardown. - const msg = `Tried to get feature ${name} ${xmlns} but _converse.stream_features has been torn down`; + const msg = `Tried to get feature ${name} ${xmlns} but stream_features has been torn down`; log.warn(msg); return; } - return _converse.stream_features.findWhere({'name': name, 'xmlns': xmlns}); + return stream_features.findWhere({'name': name, 'xmlns': xmlns}); } }, @@ -65,15 +67,16 @@ export default { * @example _converse.api.disco.own.identities.clear(); */ add (category, type, name, lang) { - for (var i=0; i<_converse.disco._identities.length; i++) { - if (_converse.disco._identities[i].category == category && - _converse.disco._identities[i].type == type && - _converse.disco._identities[i].name == name && - _converse.disco._identities[i].lang == lang) { + const { disco } = _converse.state; + for (var i=0; i e.get('parent_jids')?.includes(jid)); + return _converse.state.disco_entities.filter(e => e.get('parent_jids')?.includes(jid)); }, /** @@ -235,7 +240,7 @@ export default { * @example _converse.api.disco.entities.create({ jid }, {'ignore_cache': true}); */ create (data, options) { - return _converse.disco_entities.create(data, options); + return _converse.state.disco_entities.create(data, options); } }, @@ -266,7 +271,7 @@ export default { const entity = await api.disco.entities.get(jid, true); - if (_converse.disco_entities === undefined && !api.connection.connected()) { + if (_converse.state.disco_entities === undefined && !api.connection.connected()) { // Happens during tests when disco lookups happen asynchronously after teardown. log.warn(`Tried to get feature ${feature} for ${jid} but _converse.disco_entities has been torn down`); return []; @@ -300,7 +305,7 @@ export default { const entity = await api.disco.entities.get(jid, true); - if (_converse.disco_entities === undefined && !api.connection.connected()) { + if (_converse.state.disco_entities === undefined && !api.connection.connected()) { // Happens during tests when disco lookups happen asynchronously after teardown. log.warn(`Tried to check if ${jid} supports feature ${feature}`); return false; @@ -424,15 +429,15 @@ export default { * XEP-0163: https://xmpp.org/extensions/xep-0163.html#support * * @method api.disco.getIdentity - * @param { string } The identity category. + * @param {string} category -The identity category. * In the XML stanza, this is the `category` * attribute of the `` element. * For example: 'pubsub' - * @param { string } type The identity type. + * @param {string} type - The identity type. * In the XML stanza, this is the `type` * attribute of the `` element. * For example: 'pep' - * @param { string } jid The JID of the entity which might have the identity + * @param {string} jid - The JID of the entity which might have the identity * @returns {promise} A promise which resolves with a map indicating * whether an identity with a given type is provided by the entity. * @example diff --git a/src/headless/plugins/disco/entity.js b/src/headless/plugins/disco/entity.js index 69f95aa20d..3ed10fe906 100644 --- a/src/headless/plugins/disco/entity.js +++ b/src/headless/plugins/disco/entity.js @@ -2,8 +2,7 @@ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import log from '../../log.js'; import sizzle from 'sizzle'; -import { Collection } from '@converse/skeletor'; -import { Model } from '@converse/skeletor'; +import { Collection, Model } from '@converse/skeletor'; import { getOpenPromise } from '@converse/openpromise'; import { createStore } from '../../utils/storage.js'; @@ -50,7 +49,6 @@ class DiscoEntity extends Model { /** * Returns a Promise which resolves with a map indicating * whether a given identity is provided by this entity. - * @private * @method _converse.DiscoEntity#getIdentity * @param { String } category - The identity category * @param { String } type - The identity type @@ -66,7 +64,6 @@ class DiscoEntity extends Model { /** * Returns a Promise which resolves with a map indicating * whether a given feature is supported. - * @private * @method _converse.DiscoEntity#getFeature * @param { String } feature - The feature that might be supported. */ @@ -133,6 +130,9 @@ class DiscoEntity extends Model { this.onInfo(stanza); } + /** + * @param {Element} stanza + */ onDiscoItems (stanza) { sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"] item`, stanza).forEach(item => { if (item.getAttribute('node')) { @@ -141,7 +141,7 @@ class DiscoEntity extends Model { return; } const jid = item.getAttribute('jid'); - const entity = _converse.disco_entities.get(jid); + const entity = _converse.state.disco_entities.get(jid); if (entity) { entity.set({ parent_jids: [this.get('jid')] }); } else { @@ -155,7 +155,7 @@ class DiscoEntity extends Model { } async queryForItems () { - if (this.identities.where({ 'category': 'server' }).length === 0) { + if (this.identities.where({ category: 'server' }).length === 0) { // Don't fetch features and items if this is not a // server or a conference component. return; @@ -164,6 +164,9 @@ class DiscoEntity extends Model { this.onDiscoItems(stanza); } + /** + * @param {Element} stanza + */ async onInfo (stanza) { Array.from(stanza.querySelectorAll('identity')).forEach(identity => { this.identities.create({ diff --git a/src/headless/plugins/disco/index.js b/src/headless/plugins/disco/index.js index 2b45bf583f..d1e0113204 100644 --- a/src/headless/plugins/disco/index.js +++ b/src/headless/plugins/disco/index.js @@ -25,19 +25,23 @@ converse.plugins.add('converse-disco', { api.promises.add('discoInitialized'); api.promises.add('streamFeaturesAdded'); - _converse.DiscoEntity = DiscoEntity; - _converse.DiscoEntities = DiscoEntities; + const exports = { DiscoEntity, DiscoEntities }; - _converse.disco = { + Object.assign(_converse, exports); // XXX: DEPRECATED + Object.assign(_converse.exports, exports); + + const disco = { _identities: [], _features: [] }; + Object.assign(_converse, { disco }); // XXX: DEPRECATED + Object.assign(_converse.state, { disco }); api.listen.on('userSessionInitialized', async () => { initStreamFeatures(); - if (_converse.connfeedback.get('connection_status') === Strophe.Status.ATTACHED) { + if (_converse.state.connfeedback.get('connection_status') === Strophe.Status.ATTACHED) { // When re-attaching to a BOSH session, we fetch the stream features from the cache. - await new Promise((success, error) => _converse.stream_features.fetch({ success, error })); + await new Promise((success, error) => _converse.state.stream_features.fetch({ success, error })); notifyStreamFeaturesAdded(); } }); @@ -47,9 +51,12 @@ converse.plugins.add('converse-disco', { api.listen.on('beforeTearDown', async () => { api.promises.add('streamFeaturesAdded'); - if (_converse.stream_features) { - await _converse.stream_features.clearStore(); - delete _converse.stream_features; + + const { stream_features } = _converse.state; + if (stream_features) { + await stream_features.clearStore(); + delete _converse.state.stream_features; + Object.assign(_converse, { stream_features: undefined }); // XXX: DEPRECATED } }); diff --git a/src/headless/plugins/disco/utils.js b/src/headless/plugins/disco/utils.js index a9526fb9b1..c252b07736 100644 --- a/src/headless/plugins/disco/utils.js +++ b/src/headless/plugins/disco/utils.js @@ -18,7 +18,7 @@ function onDiscoInfoRequest (stanza) { } iqresult.c('query', attrs); - _converse.disco._identities.forEach(identity => { + _converse.state.disco._identities.forEach(identity => { const attrs = { 'category': identity.category, 'type': identity.type @@ -31,7 +31,7 @@ function onDiscoInfoRequest (stanza) { } iqresult.c('identity', attrs).up(); }); - _converse.disco._features.forEach(f => iqresult.c('feature', {'var': f}).up()); + _converse.state.disco._features.forEach(f => iqresult.c('feature', {'var': f}).up()); api.send(iqresult.tree()); return true; } @@ -64,14 +64,22 @@ export async function initializeDisco () { 'iq', 'get', null, null ); - _converse.disco_entities = new _converse.DiscoEntities(); - const id = `converse.disco-entities-${_converse.bare_jid}`; - _converse.disco_entities.browserStorage = createStore(id, 'session'); + const disco_entities = new _converse.exports.DiscoEntities(); - const collection = await _converse.disco_entities.fetchEntities(); - if (collection.length === 0 || !collection.get(_converse.domain)) { + Object.assign(_converse, { disco_entities }); // XXX: DEPRECATED + Object.assign(_converse.state, { disco_entities }); + + const bare_jid = _converse.session.get('bare_jid'); + const id = `converse.disco-entities-${bare_jid}`; + + disco_entities.browserStorage = createStore(id, 'session'); + const collection = await disco_entities.fetchEntities(); + + const domain = _converse.session.get('domain'); + + if (collection.length === 0 || !collection.get(domain)) { // If we don't have an entity for our own XMPP server, create one. - api.disco.entities.create({'jid': _converse.domain}, {'ignore_cache': true}); + api.disco.entities.create({'jid': domain}, {'ignore_cache': true}); } /** * Triggered once the `converse-disco` plugin has been initialized and the @@ -89,12 +97,15 @@ export function initStreamFeatures () { // features from cache. // Otherwise the features will be created once we've received them // from the server (see populateStreamFeatures). - if (!_converse.stream_features) { - const bare_jid = Strophe.getBareJidFromJid(_converse.jid); + if (!_converse.state.stream_features) { + const bare_jid = _converse.session.get('bare_jid'); const id = `converse.stream-features-${bare_jid}`; api.promises.add('streamFeaturesAdded'); - _converse.stream_features = new Collection(); - _converse.stream_features.browserStorage = createStore(id, "session"); + + const stream_features = new Collection(); + stream_features.browserStorage = createStore(id, "session"); + Object.assign(_converse, { stream_features }); // XXX: DEPRECATED + Object.assign(_converse.state, { stream_features }); } } @@ -113,11 +124,11 @@ export function populateStreamFeatures () { // Strophe.js sets the element on the // Strophe.Connection instance. // - // Once this is we populate the _converse.stream_features collection + // Once this is we populate the stream_features collection // and trigger streamFeaturesAdded. initStreamFeatures(); Array.from(api.connection.get().features.childNodes).forEach(feature => { - _converse.stream_features.create({ + _converse.state.stream_features.create({ 'name': feature.nodeName, 'xmlns': feature.getAttribute('xmlns') }); @@ -126,10 +137,12 @@ export function populateStreamFeatures () { } export function clearSession () { - _converse.disco_entities?.forEach(e => e.features.clearStore()); - _converse.disco_entities?.forEach(e => e.identities.clearStore()); - _converse.disco_entities?.forEach(e => e.dataforms.clearStore()); - _converse.disco_entities?.forEach(e => e.fields.clearStore()); - _converse.disco_entities?.clearStore(); - delete _converse.disco_entities; + const { disco_entities } = _converse.state; + disco_entities?.forEach(e => e.features.clearStore()); + disco_entities?.forEach(e => e.identities.clearStore()); + disco_entities?.forEach(e => e.dataforms.clearStore()); + disco_entities?.forEach(e => e.fields.clearStore()); + disco_entities?.clearStore(); + delete _converse.state.disco_entities; + Object.assign(_converse, { disco_entities: undefined }); } diff --git a/src/headless/plugins/emoji/index.js b/src/headless/plugins/emoji/index.js index 26a0148827..1367a27e3c 100644 --- a/src/headless/plugins/emoji/index.js +++ b/src/headless/plugins/emoji/index.js @@ -73,7 +73,9 @@ converse.plugins.add('converse-emoji', { } } - _converse.EmojiPicker = EmojiPicker; + const exports = { EmojiPicker }; + Object.assign(_converse, exports); // XXX: DEPRECATED + Object.assign(_converse.exports, exports); // We extend the default converse.js API to add methods specific to MUC groupchats. Object.assign(api, { diff --git a/src/headless/plugins/emoji/utils.js b/src/headless/plugins/emoji/utils.js index 4900562d73..7ca2ce62fe 100644 --- a/src/headless/plugins/emoji/utils.js +++ b/src/headless/plugins/emoji/utils.js @@ -56,27 +56,33 @@ function fromCodePoint (codepoint) { } +/** + * Converts unicode code points and code pairs to their respective characters + * @param {string} unicode + */ function convert (unicode) { - // Converts unicode code points and code pairs to their respective characters if (unicode.indexOf("-") > -1) { - const parts = [], - s = unicode.split('-'); + const parts = []; + const s = unicode.split('-'); + for (let i = 0; i < s.length; i++) { - let part = parseInt(s[i], 16); + const part = parseInt(s[i], 16); if (part >= 0x10000 && part <= 0x10FFFF) { const hi = Math.floor((part - 0x10000) / 0x400) + 0xD800; const lo = ((part - 0x10000) % 0x400) + 0xDC00; - part = (String.fromCharCode(hi) + String.fromCharCode(lo)); + parts.push(String.fromCharCode(hi) + String.fromCharCode(lo)); } else { - part = String.fromCharCode(part); + parts.push(String.fromCharCode(part)); } - parts.push(part); } return parts.join(''); } return fromCodePoint(unicode); } +/** + * @param {string} str + */ export function convertASCII2Emoji (str) { // Replace ASCII smileys return str.replace(ASCII_REPLACE_REGEX, (entire, _, m2, m3) => { @@ -90,6 +96,9 @@ export function convertASCII2Emoji (str) { }); } +/** + * @param {string} text + */ export function getShortnameReferences (text) { if (!converse.emojis.initialized) { throw new Error( @@ -111,16 +120,24 @@ export function getShortnameReferences (text) { } +/** + * @param {string} str + * @param {Function} callback + */ function parseStringForEmojis(str, callback) { const UFE0Fg = /\uFE0F/g; const U200D = String.fromCharCode(0x200D); return String(str).replace(CODEPOINTS_REGEX, (emoji, _, offset) => { const icon_id = toCodePoint(emoji.indexOf(U200D) < 0 ? emoji.replace(UFE0Fg, '') : emoji); if (icon_id) callback(icon_id, emoji, offset); + return emoji; }); } +/** + * @param {string} text + */ export function getCodePointReferences (text) { const references = []; parseStringForEmojis(text, (icon_id, emoji, offset) => { diff --git a/src/headless/plugins/headlines/api.js b/src/headless/plugins/headlines/api.js index 1d020355bb..251b9645ba 100644 --- a/src/headless/plugins/headlines/api.js +++ b/src/headless/plugins/headlines/api.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('./feed.js').default} HeadlinesFeed + */ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import { HEADLINES_TYPE } from '../../shared/constants.js'; @@ -19,13 +22,18 @@ export default { * @param {String|String[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com'] * @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model. * @param { Boolean } [create=false] - Whether the chat should be created if it's not found. - * @returns { Promise<_converse.HeadlinesFeed> } + * @returns { Promise } */ async get (jids, attrs={}, create=false) { + /** + * @param {string} jid + * @returns {Promise} + */ async function _get (jid) { let model = await api.chatboxes.get(jid); if (!model && create) { - model = await api.chatboxes.create(jid, attrs, _converse.HeadlinesFeed); + const { HeadlinesFeed } = _converse.exports; + model = await api.chatboxes.create(jid, attrs, HeadlinesFeed); } else { model = (model && model.get('type') === HEADLINES_TYPE) ? model : null; if (model && Object.keys(attrs).length) { @@ -34,6 +42,7 @@ export default { } return model; } + if (jids === undefined) { const chats = await api.chatboxes.get(); return chats.filter(c => (c.get('type') === HEADLINES_TYPE)); diff --git a/src/headless/plugins/headlines/feed.js b/src/headless/plugins/headlines/feed.js index cd9123ed3f..d383ea53f5 100644 --- a/src/headless/plugins/headlines/feed.js +++ b/src/headless/plugins/headlines/feed.js @@ -3,6 +3,12 @@ import api from "../../shared/api/index.js"; import { HEADLINES_TYPE } from '../../shared/constants.js'; +/** + * Shows headline messages + * @class + * @namespace _converse.HeadlinesFeed + * @memberOf _converse + */ export default class HeadlinesFeed extends ChatBox { defaults () { @@ -12,10 +18,16 @@ export default class HeadlinesFeed extends ChatBox { 'message_type': 'headline', 'num_unread': 0, 'time_opened': this.get('time_opened') || (new Date()).getTime(), + 'time_sent': undefined, 'type': HEADLINES_TYPE } } + constructor (attrs, options) { + super(attrs, options); + this.disable_mam = true; // Don't do MAM queries for this box + } + async initialize () { super.initialize(); this.set({'box_id': `box-${this.get('jid')}`}); diff --git a/src/headless/plugins/headlines/index.js b/src/headless/plugins/headlines/index.js index 149a35f110..6e274cd1a7 100644 --- a/src/headless/plugins/headlines/index.js +++ b/src/headless/plugins/headlines/index.js @@ -13,13 +13,9 @@ converse.plugins.add('converse-headlines', { dependencies: ["converse-chat"], initialize () { - /** - * Shows headline messages - * @class - * @namespace _converse.HeadlinesFeed - * @memberOf _converse - */ - _converse.HeadlinesFeed = HeadlinesFeed; + const exports = { HeadlinesFeed }; + Object.assign(_converse, exports); // XXX: DEPRECATED + Object.assign(_converse.exports, exports); function registerHeadlineHandler () { api.connection.get()?.addHandler(m => { diff --git a/src/headless/plugins/headlines/utils.js b/src/headless/plugins/headlines/utils.js index 5a5a6175f2..c20979775d 100644 --- a/src/headless/plugins/headlines/utils.js +++ b/src/headless/plugins/headlines/utils.js @@ -15,7 +15,7 @@ export async function onHeadlineMessage (stanza) { await api.waitUntil('rosterInitialized') if (from_jid.includes('@') && - !_converse.roster.get(from_jid) && + !_converse.state.roster.get(from_jid) && !api.settings.get("allow_non_roster_messaging")) { return; } diff --git a/src/headless/plugins/mam/api.js b/src/headless/plugins/mam/api.js index c8eb1300c1..f26f9840d7 100644 --- a/src/headless/plugins/mam/api.js +++ b/src/headless/plugins/mam/api.js @@ -1,3 +1,6 @@ +/** + * @typedef {module:converse-rsm.RSMQueryParameters} RSMQueryParameters + */ import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import dayjs from 'dayjs'; @@ -26,11 +29,11 @@ export default { */ archive: { /** - * @typedef { module:converse-rsm~RSMQueryParameters } MAMFilterParameters - * Filter parameters which can be used to filter a MAM XEP-0313 archive - * @property { String } [end] - A date string in ISO-8601 format, before which messages should be returned. Implies backward paging. - * @property { String } [start] - A date string in ISO-8601 format, after which messages should be returned. Implies forward paging. - * @property { String } [with] - A JID against which to match messages, according to either their `to` or `from` attributes. + * @typedef {RSMQueryParameters} MAMFilterParameters + * Filter parmeters which can be used to filter a MAM XEP-0313 archive + * @property String} [end] - A date string in ISO-8601 format, before which messages should be returned. Implies backward paging. + * @property {String} [start] - A date string in ISO-8601 format, after which messages should be returned. Implies forward paging. + * @property {String} [with] - A JID against which to match messages, according to either their `to` or `from` attributes. * An item in a MUC archive matches if the publisher of the item matches the JID. * If `with` is omitted, all messages that match the rest of the query will be returned, regardless of to/from * addresses of each message. @@ -38,8 +41,8 @@ export default { /** * The options that can be passed in to the {@link _converse.api.archive.query } method - * @typedef { module:converse-mam~MAMFilterParameters } ArchiveQueryOptions - * @property { Boolean } [groupchat=false] - Whether the MAM archive is for a groupchat. + * @typedef {MAMFilterParameters} ArchiveQueryOptions + * @property {boolean} [groupchat=false] - Whether the MAM archive is for a groupchat. */ /** @@ -49,10 +52,9 @@ export default { * RSM to enable easy querying between results pages. * * @method _converse.api.archive.query - * @param { module:converse-mam~ArchiveQueryOptions } options - An object containing query parameters + * @param {ArchiveQueryOptions} options - An object containing query parameters * @throws {Error} An error is thrown if the XMPP server responds with an error. - * @returns { Promise } A promise which resolves - * to a {@link module:converse-mam~MAMQueryResult } object. + * @returns {Promise} A promise which resolves to a {@link MAMQueryResult} object. * * @example * // Requesting all archived messages @@ -211,7 +213,8 @@ export default { attrs.to = options['with']; } - const jid = attrs.to || _converse.bare_jid; + const bare_jid = _converse.session.get('bare_jid'); + const jid = attrs.to || bare_jid; const supported = await api.disco.supports(NS.MAM, jid); if (!supported) { log.warn(`Did not fetch MAM archive for ${jid} because it doesn't support ${NS.MAM}`); @@ -249,18 +252,18 @@ export default { const connection = api.connection.get(); const messages = []; - const message_handler = connection.addHandler(stanza => { + const message_handler = connection.addHandler(/** @param {Element} stanza */(stanza) => { const result = sizzle(`message > result[xmlns="${NS.MAM}"]`, stanza).pop(); if (result === undefined || result.getAttribute('queryid') !== queryid) { return true; } - const from = stanza.getAttribute('from') || _converse.bare_jid; + const from = stanza.getAttribute('from') || bare_jid; if (options.groupchat) { if (from !== options['with']) { log.warn(`Ignoring alleged groupchat MAM message from ${stanza.getAttribute('from')}`); return true; } - } else if (from !== _converse.bare_jid) { + } else if (from !== bare_jid) { log.warn(`Ignoring alleged MAM message from ${stanza.getAttribute('from')}`); return true; } @@ -296,14 +299,14 @@ export default { rsm = new RSM({...options, 'xml': set}); } /** - * @typedef { Object } MAMQueryResult - * @property { Array } messages - * @property { RSM } [rsm] - An instance of {@link RSM}. + * @typedef {Object} MAMQueryResult + * @property {Array} messages + * @property {RSM} [rsm] - An instance of {@link RSM}. * You can call `next()` or `previous()` on this instance, * to get the RSM query parameters for the next or previous * page in the result set. - * @property { Boolean } complete - * @property { Error } [error] + * @property {boolean} [complete] + * @property {Error} [error] */ return { messages, rsm, complete }; } diff --git a/src/headless/plugins/mam/index.js b/src/headless/plugins/mam/index.js index 6e4651f46d..995fde8d64 100644 --- a/src/headless/plugins/mam/index.js +++ b/src/headless/plugins/mam/index.js @@ -34,7 +34,9 @@ converse.plugins.add('converse-mam', { Object.assign(api, mam_api); // This is mainly done to aid with tests - Object.assign(_converse, { onMAMError, onMAMPreferences, handleMAMResult, MAMPlaceholderMessage }); + const exports = { onMAMError, onMAMPreferences, handleMAMResult, MAMPlaceholderMessage }; + Object.assign(_converse, exports); // XXX DEPRECATED + Object.assign(_converse.exports, exports); /************************ Event Handlers ************************/ api.listen.on('addClientFeatures', () => api.disco.own.features.add(NS.MAM)); diff --git a/src/headless/plugins/mam/utils.js b/src/headless/plugins/mam/utils.js index e960480ce9..c5b8ef3e73 100644 --- a/src/headless/plugins/mam/utils.js +++ b/src/headless/plugins/mam/utils.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('../muc/muc.js').default} MUC + * @typedef {import('../chat/model.js').default} ChatBox + */ import MAMPlaceholderMessage from './placeholder.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; @@ -11,6 +15,9 @@ import { CHATROOMS_TYPE } from '../../shared/constants.js'; const { NS } = Strophe; const u = converse.env.utils; +/** + * @param {Element} iq + */ export function onMAMError (iq) { if (iq?.querySelectorAll('feature-not-implemented').length) { log.warn(`Message Archive Management (XEP-0313) not supported by ${iq.getAttribute('from')}`); @@ -46,7 +53,7 @@ export function onMAMPreferences (iq, feature) { // but Prosody doesn't do this, so we don't rely on it. api.sendIQ(stanza) .then(() => feature.save({ 'preferences': { 'default': api.settings.get('message_archiving') } })) - .catch(_converse.onMAMError); + .catch(_converse.exports.onMAMError); } else { feature.save({ 'preferences': { 'default': api.settings.get('message_archiving') } }); } @@ -59,8 +66,8 @@ export function getMAMPrefsFromFeature (feature) { } if (prefs['default'] !== api.settings.get('message_archiving')) { api.sendIQ($iq({ 'type': 'get' }).c('prefs', { 'xmlns': NS.MAM })) - .then(iq => _converse.onMAMPreferences(iq, feature)) - .catch(_converse.onMAMError); + .then(iq => _converse.exports.onMAMPreferences(iq, feature)) + .catch(_converse.exports.onMAMError); } } @@ -100,28 +107,28 @@ export async function handleMAMResult (model, result, query, options, should_pag } /** - * @typedef { Object } MAMOptions + * @typedef {Object} MAMOptions * A map of MAM related options that may be passed to fetchArchivedMessages - * @param { number } [options.max] - The maximum number of items to return. + * @param {number} [options.max] - The maximum number of items to return. * Defaults to "archived_messages_page_size" - * @param { string } [options.after] - The XEP-0359 stanza ID of a message + * @param {string} [options.after] - The XEP-0359 stanza ID of a message * after which messages should be returned. Implies forward paging. - * @param { string } [options.before] - The XEP-0359 stanza ID of a message + * @param {string} [options.before] - The XEP-0359 stanza ID of a message * before which messages should be returned. Implies backward paging. - * @param { string } [options.end] - A date string in ISO-8601 format, + * @param {string} [options.end] - A date string in ISO-8601 format, * before which messages should be returned. Implies backward paging. - * @param { string } [options.start] - A date string in ISO-8601 format, + * @param {string} [options.start] - A date string in ISO-8601 format, * after which messages should be returned. Implies forward paging. - * @param { string } [options.with] - The JID of the entity with + * @param {string} [options.with] - The JID of the entity with * which messages were exchanged. - * @param { boolean } [options.groupchat] - True if archive in groupchat. + * @param {boolean} [options.groupchat] - True if archive in groupchat. */ /** * Fetch XEP-0313 archived messages based on the passed in criteria. - * @param { ChatBox | ChatRoom } model - * @param { MAMOptions } [options] - * @param { ('forwards'|'backwards'|null)} [should_page=null] - Determines whether + * @param {ChatBox} model + * @param {MAMOptions} [options] + * @param {('forwards'|'backwards'|null)} [should_page=null] - Determines whether * this function should recursively page through the entire result set if a limited * number of results were returned. */ @@ -130,7 +137,8 @@ export async function fetchArchivedMessages (model, options = {}, should_page = return; } const is_muc = model.get('type') === CHATROOMS_TYPE; - const mam_jid = is_muc ? model.get('jid') : _converse.bare_jid; + const bare_jid = _converse.session.get('bare_jid'); + const mam_jid = is_muc ? model.get('jid') : bare_jid; if (!(await api.disco.supports(NS.MAM, mam_jid))) { return; } @@ -163,9 +171,9 @@ export async function fetchArchivedMessages (model, options = {}, should_page = /** * Create a placeholder message which is used to indicate gaps in the history. - * @param { _converse.ChatBox | _converse.ChatRoom } model - * @param { MAMOptions } options - * @param { object } result - The RSM result object + * @param {ChatBox} model + * @param {MAMOptions} options + * @param {object} result - The RSM result object */ async function createPlaceholder (model, options, result) { if (options.before == '' && (model.messages.length === 0 || !options.start)) { @@ -186,9 +194,11 @@ async function createPlaceholder (model, options, result) { const { rsm } = result; const key = `stanza_id ${model.get('jid')}`; const adjacent_message = msgs.find(m => m[key] === rsm.result.first); + const adjacent_message_date = new Date(adjacent_message['time']); + const msg_data = { 'template_hook': 'getMessageTemplate', - 'time': new Date(new Date(adjacent_message['time']) - 1).toISOString(), + 'time': new Date(adjacent_message_date.getTime() - 1).toISOString(), 'before': rsm.result.first, 'start': options.start } @@ -198,7 +208,7 @@ async function createPlaceholder (model, options, result) { /** * Fetches messages that might have been archived *after* * the last archived message in our local cache. - * @param { _converse.ChatBox | _converse.ChatRoom } + * @param {ChatBox} model */ export function fetchNewestMessages (model) { if (model.disable_mam) { diff --git a/src/headless/plugins/muc/affiliations/api.js b/src/headless/plugins/muc/affiliations/api.js index 5f5c2f5c1c..25543538c3 100644 --- a/src/headless/plugins/muc/affiliations/api.js +++ b/src/headless/plugins/muc/affiliations/api.js @@ -1,3 +1,6 @@ +/** + * @module:plugin-muc-affiliations-api + */ import { setAffiliations } from './utils.js'; export default { @@ -11,15 +14,16 @@ export default { affiliations: { /** * Set the given affliation for the given JIDs in the specified MUCs + * @typedef {Object} User + * @property {string} User.jid - The JID of the user whose affiliation will change + * @property {Array} User.affiliation - The new affiliation for this user + * @property {string} [User.reason] - An optional reason for the affiliation change * - * @param { String|Array } muc_jids - The JIDs of the MUCs in + * @param {String|Array} muc_jids - The JIDs of the MUCs in * which the affiliation should be set. - * @param { Object[] } users - An array of objects representing users + * @param {User[]} users - An array of objects representing users * for whom the affiliation is to be set. - * @param { String } users[].jid - The JID of the user whose affiliation will change - * @param { ('outcast'|'member'|'admin'|'owner') } users[].affiliation - The new affiliation for this user - * @param { String } [users[].reason] - An optional reason for the affiliation change - * @returns { Promise } + * @returns {Promise} * * @example * api.rooms.affiliations.set( diff --git a/src/headless/plugins/muc/affiliations/utils.js b/src/headless/plugins/muc/affiliations/utils.js index ce0b203593..e670c8dc04 100644 --- a/src/headless/plugins/muc/affiliations/utils.js +++ b/src/headless/plugins/muc/affiliations/utils.js @@ -1,6 +1,10 @@ /** * @copyright The Converse.js contributors * @license Mozilla Public License (MPLv2) + * + * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem + * @typedef {module:plugin-muc-affiliations-api.User} User + * @typedef {import('@converse/skeletor').Model} Model */ import _converse from '../../../shared/_converse.js'; import api, { converse } from '../../../shared/api/index.js'; @@ -15,9 +19,9 @@ const { Strophe, $iq, u } = converse.env; * Returns an array of {@link MemberListItem} objects, representing occupants * that have the given affiliation. * See: https://xmpp.org/extensions/xep-0045.html#modifymember - * @param { ("admin"|"owner"|"member") } affiliation - * @param { String } muc_jid - The JID of the MUC for which the affiliation list should be fetched - * @returns { Promise } + * @param {("admin"|"owner"|"member")} affiliation + * @param {string} muc_jid - The JID of the MUC for which the affiliation list should be fetched + * @returns {Promise} */ export async function getAffiliationList (affiliation, muc_jid) { const { __ } = _converse; @@ -45,8 +49,8 @@ export async function getAffiliationList (affiliation, muc_jid) { /** * Given an occupant model, see which affiliations may be assigned by that user - * @param { Model } occupant - * @returns { Array<('owner'|'admin'|'member'|'outcast'|'none')> } - An array of assignable affiliations + * @param {Model} occupant + * @returns {typeof AFFILIATIONS} An array of assignable affiliations */ export function getAssignableAffiliations (occupant) { let disabled = api.settings.get('modtools_disable_assign'); @@ -62,17 +66,12 @@ export function getAssignableAffiliations (occupant) { } } -// Necessary for tests -_converse.getAssignableAffiliations = getAssignableAffiliations; - /** * Send IQ stanzas to the server to modify affiliations for users in this groupchat. * See: https://xmpp.org/extensions/xep-0045.html#modifymember - * @param { Array } users - * @param { string } users[].jid - The JID of the user whose affiliation will change - * @param { Array } users[].affiliation - The new affiliation for this user - * @param { string } [users[].reason] - An optional reason for the affiliation change - * @returns { Promise } + * @param {String|Array} muc_jid - The JID(s) of the MUCs in which the + * @param {Array} users + * @returns {Promise} */ export function setAffiliations (muc_jid, users) { const affiliations = [...new Set(users.map(u => u.affiliation))]; @@ -89,13 +88,13 @@ export function setAffiliations (muc_jid, users) { * a separate stanza for each JID. * Related ticket: https://issues.prosody.im/345 * - * @param { ('outcast'|'member'|'admin'|'owner') } affiliation - The affiliation to be set - * @param { String|Array } muc_jids - The JID(s) of the MUCs in which the + * @param {typeof AFFILIATIONS} affiliation - The affiliation to be set + * @param {String|Array} muc_jids - The JID(s) of the MUCs in which the * affiliations need to be set. - * @param { object } members - A map of jids, affiliations and + * @param {object} members - A map of jids, affiliations and * optionally reasons. Only those entries with the * same affiliation as being currently set will be considered. - * @returns { Promise } A promise which resolves and fails depending on the XMPP server response. + * @returns {Promise} A promise which resolves and fails depending on the XMPP server response. */ export function setAffiliation (affiliation, muc_jids, members) { if (!Array.isArray(muc_jids)) { @@ -109,10 +108,9 @@ export function setAffiliation (affiliation, muc_jids, members) { /** * Send an IQ stanza specifying an affiliation change. - * @private - * @param { String } affiliation: affiliation (could also be stored on the member object). - * @param { String } muc_jid: The JID of the MUC in which the affiliation should be set. - * @param { Object } member: Map containing the member's jid and optionally a reason and affiliation. + * @param {typeof AFFILIATIONS} affiliation: affiliation (could also be stored on the member object). + * @param {string} muc_jid: The JID of the MUC in which the affiliation should be set. + * @param {object} member: Map containing the member's jid and optionally a reason and affiliation. */ function sendAffiliationIQ (affiliation, muc_jid, member) { const iq = $iq({ to: muc_jid, type: 'set' }) diff --git a/src/headless/plugins/muc/api.js b/src/headless/plugins/muc/api.js index 6794c73bf7..3ae0041321 100644 --- a/src/headless/plugins/muc/api.js +++ b/src/headless/plugins/muc/api.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('./muc.js').default} MUC + */ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import log from '../../log'; @@ -22,15 +25,16 @@ export default { * the chatroom in the background (i.e. doesn't cause a view to open). * * @method api.rooms.create - * @param {(string[]|string)} jid|jids The JID or array of + * @param {(string[]|string)} jids The JID or array of * JIDs of the chatroom(s) to create - * @param { object } [attrs] attrs The room attributes - * @returns {Promise} Promise which resolves with the Model representing the chat. + * @param {object} [attrs] attrs The room attributes + * @returns {Promise[]} Promise which resolves with the Model representing the chat. */ create (jids, attrs = {}) { attrs = typeof attrs === 'string' ? { 'nick': attrs } : attrs || {}; if (!attrs.nick && api.settings.get('muc_nickname_from_jid')) { - attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid); + const bare_jid = _converse.session.get('bare_jid'); + attrs.nick = Strophe.getNodeFromJid(bare_jid); } if (jids === undefined) { throw new TypeError('rooms.create: You need to provide at least one JID'); @@ -46,30 +50,31 @@ export default { * Similar to {@link api.chats.open}, but for groupchats. * * @method api.rooms.open - * @param { string } jid The room JID or JIDs (if not specified, all + * @param {string|string[]} jids The room JID or JIDs (if not specified, all * currently open rooms will be returned). - * @param { string } attrs A map containing any extra room attributes. - * @param { string } [attrs.nick] The current user's nickname for the MUC - * @param { boolean } [attrs.auto_configure] A boolean, indicating + * @param {object} attrs A map containing any extra room attributes. + * @param {string} [attrs.nick] The current user's nickname for the MUC + * @param {boolean} [attrs.hidden] + * @param {boolean} [attrs.auto_configure] A boolean, indicating * whether the room should be configured automatically or not. * If set to `true`, then it makes sense to pass in configuration settings. - * @param { object } [attrs.roomconfig] A map of configuration settings to be used when the room gets + * @param {object} [attrs.roomconfig] A map of configuration settings to be used when the room gets * configured automatically. Currently it doesn't make sense to specify * `roomconfig` values if `auto_configure` is set to `false`. * For a list of configuration values that can be passed in, refer to these values * in the [XEP-0045 MUC specification](https://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner). * The values should be named without the `muc#roomconfig_` prefix. - * @param { boolean } [attrs.minimized] A boolean, indicating whether the room should be opened minimized or not. - * @param { boolean } [attrs.bring_to_foreground] A boolean indicating whether the room should be + * @param {boolean} [attrs.minimized] A boolean, indicating whether the room should be opened minimized or not. + * @param {boolean} [attrs.bring_to_foreground] A boolean indicating whether the room should be * brought to the foreground and therefore replace the currently shown chat. * If there is no chat currently open, then this option is ineffective. - * @param { Boolean } [force=false] - By default, a minimized + * @param {boolean} [force=false] - By default, a minimized * room won't be maximized (in `overlayed` view mode) and in * `fullscreen` view mode a newly opened room won't replace * another chat already in the foreground. * Set `force` to `true` if you want to force the room to be * maximized or shown. - * @returns {Promise} Promise which resolves with the Model representing the chat. + * @returns {Promise} Promise which resolves with the Model representing the chat. * * @example * api.rooms.open('group@muc.example.com') @@ -120,14 +125,14 @@ export default { * Fetches the object representing a MUC chatroom (aka groupchat) * * @method api.rooms.get - * @param { String } [jid] The room JID (if not specified, all rooms will be returned). - * @param { Object } [attrs] A map containing any extra room attributes + * @param {string|string[]} [jids] The room JID (if not specified, all rooms will be returned). + * @param {object} [attrs] A map containing any extra room attributes * to be set if `create` is set to `true` - * @param { String } [attrs.nick] Specify the nickname - * @param { String } [attrs.password ] Specify a password if needed to enter a new room - * @param { Boolean } create A boolean indicating whether the room should be created + * @param {string} [attrs.nick] Specify the nickname + * @param {string} [attrs.password ] Specify a password if needed to enter a new room + * @param {boolean} create A boolean indicating whether the room should be created * if not found (default: `false`) - * @returns { Promise<_converse.ChatRoom> } + * @returns {Promise} * @example * api.waitUntil('roomsAutoJoined').then(() => { * const create_if_not_found = true; @@ -145,7 +150,7 @@ export default { jid = getJIDFromURI(jid); let model = await api.chatboxes.get(jid); if (!model && create) { - model = await api.chatboxes.create(jid, attrs, _converse.ChatRoom); + model = await api.chatboxes.create(jid, attrs, _converse.exports.MUC); } else { model = model && model.get('type') === CHATROOMS_TYPE ? model : null; if (model && Object.keys(attrs).length) { diff --git a/src/headless/plugins/muc/index.js b/src/headless/plugins/muc/index.js index de3dd46e31..eaf90e491d 100644 --- a/src/headless/plugins/muc/index.js +++ b/src/headless/plugins/muc/index.js @@ -30,7 +30,7 @@ import { registerDirectInvitationHandler, routeToRoom, } from './utils.js'; -import { computeAffiliationsDelta } from './affiliations/utils.js'; +import { computeAffiliationsDelta, getAssignableAffiliations } from './affiliations/utils.js'; import { AFFILIATION_CHANGES, AFFILIATION_CHANGES_LIST, @@ -145,7 +145,7 @@ converse.plugins.add('converse-muc', { * 322 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member * 332 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of a system shutdown */ - _converse.muc = { + const MUC_FEEDBACK_MESSAGES = { info_messages: { 100: __('This groupchat is not anonymous'), 102: __('This groupchat now shows unavailable members'), @@ -177,28 +177,36 @@ converse.plugins.add('converse-muc', { }, }; + const labels = { muc: MUC_FEEDBACK_MESSAGES }; + Object.assign(_converse.labels, labels); + Object.assign(_converse, labels); // XXX DEPRECATED + routeToRoom(); addEventListener('hashchange', routeToRoom); // TODO: DEPRECATED - _converse.ChatRoom = MUC; - _converse.ChatRoomMessage = MUCMessage; - _converse.ChatRoomOccupants = ChatRoomOccupants; - _converse.ChatRoomOccupant = ChatRoomOccupant; - - const exports = { MUC, MUCMessage, ChatRoomOccupants, ChatRoomOccupant }; - Object.assign(_converse.exports, exports); - - /** @type {module:shared-api.APIEndpoint} */(api.chatboxes.registry).add(CHATROOMS_TYPE, MUC); - - Object.assign(_converse, { + const legacy_exports = { + ChatRoom: MUC, + ChatRoomMessage: MUCMessage, + } + Object.assign(_converse, legacy_exports); + + const exports = { + MUC, + MUCMessage, + ChatRoomOccupants, + ChatRoomOccupant, + getAssignableAffiliations, getDefaultMUCNickname, isInfoVisible, onDirectMUCInvitation, ChatRoomMessages: MUCMessages, - }); + }; + Object.assign(_converse.exports, exports); + Object.assign(_converse, exports); // XXX DEPRECATED + + /** @type {module:shared-api.APIEndpoint} */(api.chatboxes.registry).add(CHATROOMS_TYPE, MUC); - /************************ BEGIN Event Handlers ************************/ if (api.settings.get('allow_muc_invitations')) { api.listen.on('connected', registerDirectInvitationHandler); diff --git a/src/headless/plugins/muc/message.js b/src/headless/plugins/muc/message.js index c429088a7a..2fb0c65abf 100644 --- a/src/headless/plugins/muc/message.js +++ b/src/headless/plugins/muc/message.js @@ -3,15 +3,11 @@ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import { Strophe } from 'strophe.js'; -/** - * @namespace _converse.ChatRoomMessage - * @memberOf _converse - */ + class MUCMessage extends Message { - initialize () { + async initialize () { // eslint-disable-line require-await this.chatbox = this.collection?.chatbox; - debugger; if (!this.checkValidity()) return; if (this.get('file')) { @@ -24,9 +20,9 @@ class MUCMessage extends Message { this.setTimerForEphemeralMessage(); this.setOccupant(); /** - * Triggered once a { @link _converse.ChatRoomMessage } has been created and initialized. + * Triggered once a { @link MUCMessage} has been created and initialized. * @event _converse#chatRoomMessageInitialized - * @type { _converse.ChatRoomMessages} + * @type {MUCMessage} * @example _converse.api.listen.on('chatRoomMessageInitialized', model => { ... }); */ api.trigger('chatRoomMessageInitialized', this); @@ -41,7 +37,7 @@ class MUCMessage extends Message { * based on configuration settings and server support. * @async * @method _converse.ChatRoomMessages#mayBeModerated - * @returns { Boolean } + * @returns {boolean} */ mayBeModerated () { if (typeof this.get('from_muc') === 'undefined') { @@ -57,7 +53,7 @@ class MUCMessage extends Message { } checkValidity () { - const result = _converse.Message.prototype.checkValidity.call(this); + const result = _converse.exports.Message.prototype.checkValidity.call(this); !result && this.chatbox.debouncedRejoin(); return result; } diff --git a/src/headless/plugins/muc/messages.js b/src/headless/plugins/muc/messages.js index 134d170ee5..ea811d2a62 100644 --- a/src/headless/plugins/muc/messages.js +++ b/src/headless/plugins/muc/messages.js @@ -3,17 +3,11 @@ import { Collection } from '@converse/skeletor'; /** * Collection which stores MUC messages - * @namespace _converse.ChatRoomMessages - * @memberOf _converse */ class MUCMessages extends Collection { - get comparator () { - return 'time'; - } - - constructor () { - super(); + constructor (attrs, options={}) { + super(attrs, Object.assign({ comparator: 'time' }, options)); this.model = MUCMessage; } } diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js index 2964302b34..6873b511e4 100644 --- a/src/headless/plugins/muc/muc.js +++ b/src/headless/plugins/muc/muc.js @@ -1,5 +1,6 @@ /** * @typedef {import('../muc/message.js').default} MUCMessage + * @typedef {import('../muc/occupant.js').default} ChatRoomOccupant * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder */ @@ -13,7 +14,7 @@ import pick from 'lodash-es/pick'; import sizzle from 'sizzle'; import { Model } from '@converse/skeletor'; import { ROOMSTATUS } from './constants.js'; -import { CHATROOMS_TYPE } from '../../shared/constants.js'; +import { CHATROOMS_TYPE, GONE } from '../../shared/constants.js'; import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js'; import { TimeoutError } from '../../shared/errors.js'; import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js'; @@ -464,6 +465,9 @@ class MUC extends ChatBox { } } + /** + * @param {Element} stanza + */ async handleErrorMessageStanza (stanza) { const { __ } = _converse; const attrs = await parseMUCMessage(stanza, this); @@ -624,7 +628,10 @@ class MUC extends ChatBox { this.removeHandlers(); const connection = api.connection.get(); this.presence_handler = connection.addHandler( - stanza => this.onPresence(stanza) || true, + /** @param {Element} stanza */(stanza) => { + this.onPresence(stanza); + return true; + }, null, 'presence', null, @@ -634,7 +641,10 @@ class MUC extends ChatBox { ); this.domain_presence_handler = connection.addHandler( - stanza => this.onPresenceFromMUCHost(stanza) || true, + /** @param {Element} stanza */(stanza) => { + this.onPresenceFromMUCHost(stanza); + return true; + }, null, 'presence', null, @@ -643,7 +653,10 @@ class MUC extends ChatBox { ); this.message_handler = connection.addHandler( - stanza => !!this.handleMessageStanza(stanza) || true, + /** @param {Element} stanza */(stanza) => { + this.handleMessageStanza(stanza); + return true; + }, null, 'message', null, @@ -653,7 +666,10 @@ class MUC extends ChatBox { ); this.domain_message_handler = connection.addHandler( - stanza => this.handleMessageFromMUCHost(stanza) || true, + /** @param {Element} stanza */(stanza) => { + this.handleMessageFromMUCHost(stanza); + return true; + }, null, 'message', null, @@ -662,7 +678,10 @@ class MUC extends ChatBox { ); this.affiliation_message_handler = connection.addHandler( - stanza => this.handleAffiliationChangedMessage(stanza) || true, + (stanza) => { + this.handleAffiliationChangedMessage(stanza); + return true; + }, Strophe.NS.MUC_USER, 'message', null, @@ -721,18 +740,17 @@ class MUC extends ChatBox { * or error message within a specific timeout period. * @private * @method MUC#sendTimedMessage - * @param { _converse.Message|Element } message + * @param {Strophe.Builder|Element } message * @returns { Promise|Promise } Returns a promise - * which resolves with the reflected message stanza or with an error stanza or {@link TimeoutError}. + * which resolves with the reflected message stanza or with an error stanza or + * {@link TimeoutError}. */ - sendTimedMessage (el) { - if (typeof el.tree === 'function') { - el = el.tree(); - } + sendTimedMessage (message) { + const el = message instanceof Element ? message : message.tree(); let id = el.getAttribute('id'); if (!id) { // inject id if not found - id = this.getUniqueId('sendIQ'); + id = getUniqueId('sendIQ'); el.setAttribute('id', id); } const promise = getOpenPromise(); @@ -755,9 +773,8 @@ class MUC extends ChatBox { /** * Retract one of your messages in this groupchat - * @private * @method MUC#retractOwnMessage - * @param { _converse.Message } message - The message which we're retracting. + * @param {MUCMessage} message - The message which we're retracting. */ async retractOwnMessage (message) { const __ = _converse.__; @@ -805,10 +822,9 @@ class MUC extends ChatBox { /** * Retract someone else's message in this groupchat. - * @private * @method MUC#retractOtherMessage - * @param { _converse.ChatRoomMessage } message - The message which we're retracting. - * @param { string } [reason] - The reason for retracting the message. + * @param {MUCMessage} message - The message which we're retracting. + * @param {string} [reason] - The reason for retracting the message. * @example * const room = await api.rooms.get(jid); * const message = room.messages.findWhere({'body': 'Get rich quick!'}); @@ -816,10 +832,11 @@ class MUC extends ChatBox { */ async retractOtherMessage (message, reason) { const editable = message.get('editable'); + const bare_jid = _converse.session.get('bare_jid'); // Optimistic save message.save({ 'moderated': 'retracted', - 'moderated_by': _converse.bare_jid, + 'moderated_by': bare_jid, 'moderated_id': message.get('msgid'), 'moderation_reason': reason, 'editable': false @@ -842,8 +859,8 @@ class MUC extends ChatBox { * Sends an IQ stanza to the XMPP server to retract a message in this groupchat. * @private * @method MUC#sendRetractionIQ - * @param { _converse.ChatRoomMessage } message - The message which we're retracting. - * @param { string } [reason] - The reason for retracting the message. + * @param {MUCMessage} message - The message which we're retracting. + * @param {string} [reason] - The reason for retracting the message. */ sendRetractionIQ (message, reason) { const iq = $iq({ 'to': this.get('jid'), 'type': 'set' }) @@ -1355,7 +1372,8 @@ class MUC extends ChatBox { if (this.config.get('changesubject') || ['owner', 'admin'].includes(this.getOwnAffiliation())) { allowed_commands = [...allowed_commands, ...['subject', 'topic']]; } - const occupant = this.occupants.findWhere({ 'jid': _converse.bare_jid }); + const bare_jid = _converse.session.get('bare_jid'); + const occupant = this.occupants.findWhere({ 'jid': bare_jid }); if (this.verifyAffiliations(['owner'], occupant, false)) { allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS); } else if (this.verifyAffiliations(['admin'], occupant, false)) { @@ -1383,7 +1401,8 @@ class MUC extends ChatBox { if (!affiliations.length) { return true; } - occupant = occupant || this.occupants.findWhere({ 'jid': _converse.bare_jid }); + const bare_jid = _converse.session.get('bare_jid'); + occupant = occupant || this.occupants.findWhere({ 'jid': bare_jid }); if (occupant) { const a = occupant.get('affiliation'); if (affiliations.includes(a)) { @@ -1405,7 +1424,8 @@ class MUC extends ChatBox { if (!roles.length) { return true; } - occupant = occupant || this.occupants.findWhere({ 'jid': _converse.bare_jid }); + const bare_jid = _converse.session.get('bare_jid'); + occupant = occupant || this.occupants.findWhere({ 'jid': bare_jid }); if (occupant) { const role = occupant.get('role'); if (roles.includes(role)) { @@ -1426,7 +1446,7 @@ class MUC extends ChatBox { * @returns { ('none'|'visitor'|'participant'|'moderator') } */ getOwnRole () { - return this.getOwnOccupant()?.attributes?.role; + return this.getOwnOccupant()?.get('role'); } /** @@ -1436,14 +1456,14 @@ class MUC extends ChatBox { * @returns { ('none'|'outcast'|'member'|'admin'|'owner') } */ getOwnAffiliation () { - return this.getOwnOccupant()?.attributes?.affiliation || 'none'; + return this.getOwnOccupant()?.get('affiliation') || 'none'; } /** - * Get the {@link _converse.ChatRoomOccupant} instance which + * Get the {@link ChatRoomOccupant} instance which * represents the current user. * @method MUC#getOwnOccupant - * @returns { _converse.ChatRoomOccupant } + * @returns {ChatRoomOccupant} */ getOwnOccupant () { return this.occupants.getOwnOccupant(); @@ -1483,13 +1503,12 @@ class MUC extends ChatBox { /** * Send an IQ stanza to modify an occupant's role - * @private * @method MUC#setRole - * @param { _converse.ChatRoomOccupant } occupant - * @param { String } role - * @param { String } reason - * @param { function } onSuccess - callback for a succesful response - * @param { function } onError - callback for an error response + * @param {ChatRoomOccupant} occupant + * @param {string} role + * @param {string} reason + * @param {function} onSuccess - callback for a succesful response + * @param {function} onError - callback for an error response */ setRole (occupant, role, reason, onSuccess, onError) { const item = $build('item', { @@ -1512,10 +1531,9 @@ class MUC extends ChatBox { } /** - * @private * @method MUC#getOccupant - * @param { String } nickname_or_jid - The nickname or JID of the occupant to be returned - * @returns { _converse.ChatRoomOccupant } + * @param {string} nickname_or_jid - The nickname or JID of the occupant to be returned + * @returns {ChatRoomOccupant} */ getOccupant (nickname_or_jid) { return u.isValidJID(nickname_or_jid) @@ -1525,38 +1543,36 @@ class MUC extends ChatBox { /** * Return an array of occupant models that have the required role - * @private * @method MUC#getOccupantsWithRole - * @param { String } role - * @returns { _converse.ChatRoomOccupant[] } + * @param {string} role + * @returns {{jid: string, nick: string, role: string}[]} */ getOccupantsWithRole (role) { return this.getOccupantsSortedBy('nick') .filter(o => o.get('role') === role) .map(item => { return { - 'jid': item.get('jid'), - 'nick': item.get('nick'), - 'role': item.get('role') + jid: /** @type {string} */item.get('jid'), + nick: /** @type {string} */item.get('nick'), + role: /** @type {string} */item.get('role') }; }); } /** * Return an array of occupant models that have the required affiliation - * @private * @method MUC#getOccupantsWithAffiliation - * @param { String } affiliation - * @returns { _converse.ChatRoomOccupant[] } + * @param {string} affiliation + * @returns {{jid: string, nick: string, affiliation: string}[]} */ getOccupantsWithAffiliation (affiliation) { return this.getOccupantsSortedBy('nick') .filter(o => o.get('affiliation') === affiliation) .map(item => { return { - 'jid': item.get('jid'), - 'nick': item.get('nick'), - 'affiliation': item.get('affiliation') + jid: /** @type {string} */item.get('jid'), + nick: /** @type {string} */item.get('nick'), + affiliation: /** @type {string} */item.get('affiliation') }; }); } @@ -1565,8 +1581,8 @@ class MUC extends ChatBox { * Return an array of occupant models, sorted according to the passed-in attribute. * @private * @method MUC#getOccupantsSortedBy - * @param { String } attr - The attribute to sort the returned array by - * @returns { _converse.ChatRoomOccupant[] } + * @param {string} attr - The attribute to sort the returned array by + * @returns {ChatRoomOccupant[]} */ getOccupantsSortedBy (attr) { return Array.from(this.occupants.models).sort((a, b) => @@ -1600,12 +1616,12 @@ class MUC extends ChatBox { * Given a nick name, save it to the model state, otherwise, look * for a server-side reserved nickname or default configured * nickname and if found, persist that to the model state. - * @private * @method MUC#getAndPersistNickname - * @returns { Promise } A promise which resolves with the nickname + * @param {string} nick + * @returns {Promise} A promise which resolves with the nickname */ async getAndPersistNickname (nick) { - nick = nick || this.get('nick') || (await this.getReservedNick()) || _converse.getDefaultMUCNickname(); + nick = nick || this.get('nick') || (await this.getReservedNick()) || _converse.exports.getDefaultMUCNickname(); if (nick) safeSave(this, { nick }, { 'silent': true }); return nick; } @@ -1794,7 +1810,6 @@ class MUC extends ChatBox { /** * Given two JIDs, which can be either user JIDs or MUC occupant JIDs, * determine whether they belong to the same user. - * @private * @method MUC#isSameUser * @param { String } jid1 * @param { String } jid2 @@ -1915,14 +1930,14 @@ class MUC extends ChatBox { * Determines whether the message is from ourselves by checking * the `from` attribute. Doesn't check the `type` attribute. * @method MUC#isOwnMessage - * @param {Object|Element|_converse.Message} msg + * @param {Object|Element|MUCMessage} msg * @returns {boolean} */ isOwnMessage (msg) { let from; if (msg instanceof Element) { from = msg.getAttribute('from'); - } else if (msg instanceof _converse.Message) { + } else if (msg instanceof _converse.exports.MUCMessage) { from = msg.get('from'); } else { from = msg.from; @@ -1932,7 +1947,7 @@ class MUC extends ChatBox { getUpdatedMessageAttributes (message, attrs) { const new_attrs = { - ..._converse.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, attrs), + ..._converse.exports.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, attrs), ...pick(attrs, ['from_muc', 'occupant_id']), } @@ -1975,7 +1990,7 @@ class MUC extends ChatBox { */ async sendStatusPresence (type, status, child_nodes) { if (this.session.get('connection_status') === ROOMSTATUS.ENTERED) { - const presence = await _converse.xmppstatus.constructPresence(type, this.getRoomJIDAndNick(), status); + const presence = await _converse.exports.xmppstatus.constructPresence(type, this.getRoomJIDAndNick(), status); child_nodes?.map(c => c?.tree() ?? c).forEach(c => presence.cnode(c).up()); api.send(presence); } @@ -1998,8 +2013,8 @@ class MUC extends ChatBox { } /** - * @private * @method MUC#shouldShowErrorMessage + * @param {object} attrs * @returns {Promise} */ async shouldShowErrorMessage (attrs) { @@ -2013,7 +2028,7 @@ class MUC extends ChatBox { } else if (attrs.error_condition === 'not-acceptable' && (await this.rejoinIfNecessary())) { return false; } - return _converse.ChatBox.prototype.shouldShowErrorMessage.call(this, attrs); + return _converse.exports.ChatBox.prototype.shouldShowErrorMessage.call(this, attrs); } /** @@ -2025,7 +2040,7 @@ class MUC extends ChatBox { * @method MUC#findDanglingModeration * @param { object } attrs - Attributes representing a received * message, as returned by {@link parseMUCMessage} - * @returns { _converse.ChatRoomMessage } + * @returns {MUCMessage} */ findDanglingModeration (attrs) { if (!this.messages.length) { @@ -2112,7 +2127,7 @@ class MUC extends ChatBox { return `${result}${__('%1$s is typing', actors[0])}\n`; } else if (state === 'paused') { return `${result}${__('%1$s has stopped typing', actors[0])}\n`; - } else if (state === _converse.GONE) { + } else if (state === GONE) { return `${result}${__('%1$s has gone away', actors[0])}\n`; } else if (state === 'entered') { return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`; @@ -2142,7 +2157,7 @@ class MUC extends ChatBox { return `${result}${__('%1$s are typing', actors_str)}\n`; } else if (state === 'paused') { return `${result}${__('%1$s have stopped typing', actors_str)}\n`; - } else if (state === _converse.GONE) { + } else if (state === GONE) { return `${result}${__('%1$s have gone away', actors_str)}\n`; } else if (state === 'entered') { return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`; @@ -2236,7 +2251,7 @@ class MUC extends ChatBox { /** * Given {@link MessageAttributes} look for XEP-0316 Room Notifications and create info * messages for them. - * @param { Element } stanza + * @param {MessageAttributes} attrs */ handleMEPNotification (attrs) { if (attrs.from !== this.get('jid') || !attrs.activities) { @@ -2255,15 +2270,15 @@ class MUC extends ChatBox { * Returns an already cached message (if it exists) based on the * passed in attributes map. * @method MUC#getDuplicateMessage - * @param { object } attrs - Attributes representing a received + * @param {object} attrs - Attributes representing a received * message, as returned by {@link parseMUCMessage} - * @returns {Promise<_converse.Message>} + * @returns {MUCMessage} */ getDuplicateMessage (attrs) { if (attrs.activities?.length) { return this.messages.findWhere({'type': 'mep', 'msgid': attrs.msgid}); } else { - return _converse.ChatBox.prototype.getDuplicateMessage.call(this, attrs); + return _converse.exports.ChatBox.prototype.getDuplicateMessage.call(this, attrs); } } @@ -2315,6 +2330,9 @@ class MUC extends ChatBox { } } + /** + * @param {Element} pres + */ handleModifyError (pres) { const text = pres.querySelector('error text')?.textContent; if (text) { @@ -2341,7 +2359,7 @@ class MUC extends ChatBox { if (!x) { return; } - const disconnection_codes = Object.keys(_converse.muc.disconnect_messages); + const disconnection_codes = Object.keys(_converse.labels.muc.disconnect_messages); const codes = sizzle('status', x) .map(s => s.getAttribute('code')) .filter(c => disconnection_codes.includes(c)); @@ -2356,7 +2374,7 @@ class MUC extends ChatBox { const item = x.querySelector('item'); const reason = item ? item.querySelector('reason')?.textContent : undefined; const actor = item ? item.querySelector('actor')?.getAttribute('nick') : undefined; - const message = _converse.muc.disconnect_messages[codes[0]]; + const message = _converse.labels.muc.disconnect_messages[codes[0]]; const status = codes.includes('301') ? ROOMSTATUS.BANNED : ROOMSTATUS.DISCONNECTED; this.setDisconnectionState(message, reason, actor, status); } @@ -2474,20 +2492,22 @@ class MUC extends ChatBox { createInfoMessage (code, stanza, is_self) { const __ = _converse.__; const data = { 'type': 'info', 'is_ephemeral': true }; + const { info_messages, new_nickname_messages } = _converse.labels.muc; + if (!isInfoVisible(code)) { return; } if (code === '110' || (code === '100' && !is_self)) { return; - } else if (code in _converse.muc.info_messages) { - data.message = _converse.muc.info_messages[code]; + } else if (code in info_messages) { + data.message = info_messages[code]; } else if (!is_self && ACTION_INFO_CODES.includes(code)) { const nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, stanza).pop(); data.actor = item ? item.querySelector('actor')?.getAttribute('nick') : undefined; data.reason = item ? item.querySelector('reason')?.textContent : undefined; data.message = this.getActionInfoMessage(code, nick, data.actor); - } else if (is_self && code in _converse.muc.new_nickname_messages) { + } else if (is_self && code in new_nickname_messages) { // XXX: Side-effect of setting the nick. Should ideally be refactored out of this method let nick; if (code === '210') { @@ -2496,7 +2516,7 @@ class MUC extends ChatBox { nick = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, stanza).pop().getAttribute('nick'); } this.save('nick', nick); - data.message = __(_converse.muc.new_nickname_messages[code], nick); + data.message = __(new_nickname_messages[code], nick); } if (data.message) { @@ -2526,11 +2546,11 @@ class MUC extends ChatBox { /** * Set parameters regarding disconnection from this room. This helps to * communicate to the user why they were disconnected. - * @param { String } message - The disconnection message, as received from (or + * @param {string} message - The disconnection message, as received from (or * implied by) the server. - * @param { String } reason - The reason provided for the disconnection - * @param { String } actor - The person (if any) responsible for this disconnection - * @param { number } status - The status code (see `ROOMSTATUS`) + * @param {string} [reason] - The reason provided for the disconnection + * @param {string} [actor] - The person (if any) responsible for this disconnection + * @param {number} [status] - The status code (see `ROOMSTATUS`) */ setDisconnectionState (message, reason, actor, status=ROOMSTATUS.DISCONNECTED) { this.session.save({ @@ -2541,11 +2561,14 @@ class MUC extends ChatBox { }); } + /** + * @param {Element} presence + */ onNicknameClash (presence) { const __ = _converse.__; if (api.settings.get('muc_nickname_from_jid')) { const nick = presence.getAttribute('from').split('/')[1]; - if (nick === _converse.getDefaultMUCNickname()) { + if (nick === _converse.exports.getDefaultMUCNickname()) { this.join(nick + '-2'); } else { const del = nick.lastIndexOf('-'); @@ -2587,7 +2610,7 @@ class MUC extends ChatBox { this.setDisconnectionState(message, reason); } else if (error.querySelector('forbidden')) { this.setDisconnectionState( - _converse.muc.disconnect_messages[301], + _converse.labels.muc.disconnect_messages[301], reason, null, ROOMSTATUS.BANNED @@ -2664,7 +2687,7 @@ class MUC extends ChatBox { this.getOwnRole() !== 'none' && this.session.get('connection_status') === ROOMSTATUS.CONNECTING ) { - this.session.save('connection_status', ROOMSTATUS.CONNECTED); + this.session.save('connection_status', ROOMSTATUS.CONNECTED.toString()); } } else { this.updateOccupantsOnPresence(stanza); @@ -2684,7 +2707,7 @@ class MUC extends ChatBox { * user is the groupchat's owner. * @private * @method MUC#onOwnPresence - * @param { Element } pres - The stanza + * @param {Element} stanza - The stanza */ async onOwnPresence (stanza) { await this.occupants.fetched; @@ -2701,7 +2724,7 @@ class MUC extends ChatBox { // Set connection_status before creating the occupant, but // only trigger afterwards, so that plugins can access the // occupant in their event handlers. - this.session.save('connection_status', ROOMSTATUS.ENTERED, { 'silent': true }); + this.session.save('connection_status', ROOMSTATUS.ENTERED.toString(), { 'silent': true }); this.updateOccupantsOnPresence(stanza); this.session.trigger('change:connection_status', this.session, old_status); } else { diff --git a/src/headless/plugins/muc/occupants.js b/src/headless/plugins/muc/occupants.js index 79b320969b..c09c387ca0 100644 --- a/src/headless/plugins/muc/occupants.js +++ b/src/headless/plugins/muc/occupants.js @@ -117,10 +117,10 @@ class ChatRoomOccupants extends Collection { } /** - * Get the {@link _converse.ChatRoomOccupant} instance which + * Get the {@link ChatRoomOccupant} instance which * represents the current user. * @method _converse.ChatRoomOccupants#getOwnOccupant - * @returns { _converse.ChatRoomOccupant } + * @returns {ChatRoomOccupant} */ getOwnOccupant () { return this.findOccupant({ diff --git a/src/headless/plugins/muc/parsers.js b/src/headless/plugins/muc/parsers.js index c9a96789f5..1d5c3b7bbc 100644 --- a/src/headless/plugins/muc/parsers.js +++ b/src/headless/plugins/muc/parsers.js @@ -1,3 +1,7 @@ +/** + * @module:plugin-muc-parsers + * @typedef {import('../muc/muc.js').default} MUC + */ import dayjs from 'dayjs'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; @@ -132,7 +136,8 @@ function getSender (attrs, chatbox) { if (own_occupant_id) { is_me = attrs.occupant_id === own_occupant_id; } else if (attrs.from_real_jid) { - is_me = Strophe.getBareJidFromJid(attrs.from_real_jid) === _converse.bare_jid; + const bare_jid = _converse.session.get('bare_jid'); + is_me = Strophe.getBareJidFromJid(attrs.from_real_jid) === bare_jid; } else { is_me = attrs.nick === chatbox.get('nick') } @@ -141,12 +146,9 @@ function getSender (attrs, chatbox) { /** * Parses a passed in message stanza and returns an object of attributes. - * @param { Element } stanza - The message stanza - * @param { Element } original_stanza - The original stanza, that contains the - * message stanza, if it was contained, otherwise it's the message stanza itself. - * @param { _converse.ChatRoom } chatbox - * @param { _converse } _converse - * @returns { Promise } + * @param {Element} stanza - The message stanza + * @param {MUC} chatbox + * @returns {Promise} */ export async function parseMUCMessage (stanza, chatbox) { throwErrorIfInvalidForward(stanza); @@ -257,7 +259,7 @@ export async function parseMUCMessage (stanza, chatbox) { getOpenGraphMetadata(stanza), getRetractionAttributes(stanza, original_stanza), getModerationAttributes(stanza), - getEncryptionAttributes(stanza, _converse), + getEncryptionAttributes(stanza), ); await api.emojis.initialize(); @@ -303,20 +305,19 @@ export async function parseMUCMessage (stanza, chatbox) { /** * Given an IQ stanza with a member list, create an array of objects containing * known member data (e.g. jid, nick, role, affiliation). - * @private + * + * @typedef {Object} MemberListItem + * Either the JID or the nickname (or both) will be available. + * @property {string} affiliation + * @property {string} [role] + * @property {string} [jid] + * @property {string} [nick] + * * @method muc_utils#parseMemberListIQ * @returns { MemberListItem[] } */ export function parseMemberListIQ (iq) { return sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq).map(item => { - /** - * @typedef {Object} MemberListItem - * Either the JID or the nickname (or both) will be available. - * @property {string} affiliation - * @property {string} [role] - * @property {string} [jid] - * @property {string} [nick] - */ const data = { 'affiliation': item.getAttribute('affiliation') }; @@ -343,21 +344,28 @@ export function parseMemberListIQ (iq) { /** * Parses a passed in MUC presence stanza and returns an object of attributes. * @method parseMUCPresence - * @param { Element } stanza - The presence stanza - * @param { _converse.ChatRoom } chatbox - * @returns { MUCPresenceAttributes } + * @param {Element} stanza - The presence stanza + * @param {MUC} chatbox + * @returns {MUCPresenceAttributes} */ export function parseMUCPresence (stanza, chatbox) { /** - * @typedef { Object } MUCPresenceAttributes + * Object representing a XEP-0371 Hat + * @typedef {Object} MUCHat + * @property {string} title + * @property {string} uri + * * The object which {@link parseMUCPresence} returns - * @property { ("offline|online") } show - * @property { Array } hats - An array of XEP-0317 hats - * @property { Array } states - * @property { String } from - The sender JID (${muc_jid}/${nick}) - * @property { String } nick - The nickname of the sender - * @property { String } occupant_id - The XEP-0421 occupant ID - * @property { String } type - The type of presence + * @typedef {Object} MUCPresenceAttributes + * @property {string} show + * @property {Array} hats - An array of XEP-0317 hats + * @property {Array} states + * @property {String} from - The sender JID (${muc_jid}/${nick}) + * @property {String} nick - The nickname of the sender + * @property {String} occupant_id - The XEP-0421 occupant ID + * @property {String} type - The type of presence + * @property {String} [jid] + * @property {boolean} [is_me] */ const from = stanza.getAttribute('from'); const type = stanza.getAttribute('type'); @@ -391,12 +399,6 @@ export function parseMUCPresence (stanza, chatbox) { } else if (child.matches('x') && child.getAttribute('xmlns') === Strophe.NS.VCARDUPDATE) { data.image_hash = child.querySelector('photo')?.textContent; } else if (child.matches('hats') && child.getAttribute('xmlns') === Strophe.NS.MUC_HATS) { - /** - * @typedef { Object } MUCHat - * Object representing a XEP-0371 Hat - * @property { String } title - * @property { String } uri - */ data['hats'] = Array.from(child.children).map( c => c.matches('hat') && { diff --git a/src/headless/plugins/roster/index.js b/src/headless/plugins/roster/index.js index 4dfc226124..00a70a3089 100644 --- a/src/headless/plugins/roster/index.js +++ b/src/headless/plugins/roster/index.js @@ -36,16 +36,19 @@ converse.plugins.add('converse-roster', { Object.assign(_converse.api, roster_api); const { __ } = _converse; - _converse.HEADER_CURRENT_CONTACTS = __('My contacts'); - _converse.HEADER_PENDING_CONTACTS = __('Pending contacts'); - _converse.HEADER_REQUESTING_CONTACTS = __('Contact requests'); - _converse.HEADER_UNGROUPED = __('Ungrouped'); - _converse.HEADER_UNREAD = __('New messages'); - - _converse.Presence = Presence; - _converse.Presences = Presences; - _converse.RosterContact = RosterContact; - _converse.RosterContacts = RosterContacts; + const labels = { + HEADER_CURRENT_CONTACTS: __('My contacts'), + HEADER_PENDING_CONTACTS: __('Pending contacts'), + HEADER_REQUESTING_CONTACTS: __('Contact requests'), + HEADER_UNGROUPED: __('Ungrouped'), + HEADER_UNREAD: __('New messages'), + } + Object.assign(_converse, labels); // XXX DEPRECATED + Object.assign(_converse.labels, labels); + + const exports = { Presence, Presences, RosterContact, RosterContacts }; + Object.assign(_converse, exports); // XXX DEPRECATED + Object.assign(_converse.exports, exports); api.listen.on('beforeTearDown', () => unregisterPresenceHandler()); api.listen.on('chatBoxesInitialized', onChatBoxesInitialized); diff --git a/src/headless/plugins/roster/utils.js b/src/headless/plugins/roster/utils.js index 92e3c547c1..f90fd3c69d 100644 --- a/src/headless/plugins/roster/utils.js +++ b/src/headless/plugins/roster/utils.js @@ -13,16 +13,23 @@ const { $pres } = converse.env; function initRoster () { // Initialize the collections that represent the roster contacts and groups - const roster = _converse.roster = new _converse.RosterContacts(); - let id = `converse.contacts-${_converse.bare_jid}`; + const roster = new _converse.exports.RosterContacts(); + Object.assign(_converse, { roster }); // XXX Deprecated + Object.assign(_converse.state, { roster }); + + const bare_jid = _converse.session.get('bare_jid'); + let id = `converse.contacts-${bare_jid}`; initStorage(roster, id); - const filter = _converse.roster_filter = new RosterFilter(); - filter.id = `_converse.rosterfilter-${_converse.bare_jid}`; - initStorage(filter, filter.id); - filter.fetch(); + const roster_filter = new RosterFilter(); + Object.assign(_converse, { roster_filter }); // XXX Deprecated + Object.assign(_converse.state, { roster_filter }); + + roster_filter.id = `_converse.rosterfilter-${bare_jid}`; + initStorage(roster_filter, roster_filter.id); + roster_filter.fetch(); - id = `converse-roster-model-${_converse.bare_jid}`; + id = `converse-roster-model-${bare_jid}`; roster.data = new Model(); roster.data.id = id; initStorage(roster.data, id); @@ -42,8 +49,7 @@ function initRoster () { /** * Fetch all the roster groups, and then the roster contacts. * Emit an event after fetching is done in each case. - * @private - * @param { Bool } ignore_cache - If set to to true, the local cache + * @param {boolean} ignore_cache - If set to to true, the local cache * will be ignored it's guaranteed that the XMPP server * will be queried for the roster. */ @@ -85,7 +91,7 @@ export function unregisterPresenceHandler () { } async function clearPresences () { - await _converse.presences?.clearStore(); + await _converse.state.presences?.clearStore(); } @@ -125,7 +131,7 @@ export function onPresencesInitialized (reconnecting) { } else { initRoster(); } - _converse.roster.onConnected(); + _converse.state.roster.onConnected(); registerPresenceHandler(); populateRoster(!api.connection.get().restored); } @@ -142,12 +148,17 @@ export async function onStatusInitialized (reconnecting) { // and we'll receive new presence updates !api.connection.get().hasResumed() && (await clearPresences()); } else { - _converse.presences = new _converse.Presences(); - const id = `converse.presences-${_converse.bare_jid}`; - initStorage(_converse.presences, id, 'session'); + const presences = new _converse.exports.Presences(); + Object.assign(_converse, { presences }); + Object.assign(_converse.state, { presences }); + + const bare_jid = _converse.session.get('bare_jid'); + const id = `converse.presences-${bare_jid}`; + + initStorage(presences, id, 'session'); // We might be continuing an existing session, so we fetch // cached presence data. - _converse.presences.fetch(); + presences.fetch(); } /** * Triggered once the _converse.Presences collection has been @@ -155,7 +166,7 @@ export async function onStatusInitialized (reconnecting) { * Returns a boolean indicating whether this event has fired due to * Converse having reconnected. * @event _converse#presencesInitialized - * @type { bool } + * @type {boolean} * @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... }); */ api.trigger('presencesInitialized', reconnecting); diff --git a/src/headless/plugins/status/index.js b/src/headless/plugins/status/index.js index b67e297eb2..0d22d81203 100644 --- a/src/headless/plugins/status/index.js +++ b/src/headless/plugins/status/index.js @@ -35,28 +35,21 @@ converse.plugins.add('converse-status', { }); api.promises.add(['statusInitialized']); - _converse.XMPPStatus = XMPPStatus; - _converse.onUserActivity = onUserActivity; - _converse.onEverySecond = onEverySecond; - _converse.sendCSI = sendCSI; - _converse.registerIntervalHandler = registerIntervalHandler; - + const exports = { XMPPStatus, onUserActivity, onEverySecond, sendCSI, registerIntervalHandler }; + Object.assign(_converse, exports); // Deprecated + Object.assign(_converse.exports, exports); Object.assign(_converse.api.user, status_api); if (api.settings.get("idle_presence_timeout") > 0) { api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.IDLE)); } - api.listen.on('presencesInitialized', (reconnecting) => { - if (!reconnecting) { - _converse.registerIntervalHandler(); - } - }); + api.listen.on('presencesInitialized', (reconnecting) => (!reconnecting && registerIntervalHandler())); api.listen.on('clearSession', () => { - if (shouldClearCache() && _converse.xmppstatus) { - _converse.xmppstatus.destroy(); - delete _converse.xmppstatus; + if (shouldClearCache() && _converse.state.xmppstatus) { + _converse.state.xmppstatus.destroy(); + delete _converse.state.xmppstatus; api.promises.add(['statusInitialized']); } }); diff --git a/src/headless/plugins/status/utils.js b/src/headless/plugins/status/utils.js index bc5e20b641..029abc9e69 100644 --- a/src/headless/plugins/status/utils.js +++ b/src/headless/plugins/status/utils.js @@ -17,14 +17,15 @@ function onStatusInitialized (reconnecting) { export function initStatus (reconnecting) { // If there's no xmppstatus obj, then we were never connected to // begin with, so we set reconnecting to false. - reconnecting = _converse.xmppstatus === undefined ? false : reconnecting; + reconnecting = _converse.state.xmppstatus === undefined ? false : reconnecting; if (reconnecting) { onStatusInitialized(reconnecting); } else { - const id = `converse.xmppstatus-${_converse.bare_jid}`; - _converse.xmppstatus = new _converse.XMPPStatus({ id }); - initStorage(_converse.xmppstatus, id, 'session'); - _converse.xmppstatus.fetch({ + const id = `converse.xmppstatus-${_converse.session.get('bare_jid')}`; + _converse.state.xmppstatus = new _converse.exports.XMPPStatus({ id }); + Object.assign(_converse, { xmppstatus: _converse.state.xmppstatus }); + initStorage(_converse.state.xmppstatus, id, 'session'); + _converse.state.xmppstatus.fetch({ 'success': () => onStatusInitialized(reconnecting), 'error': () => onStatusInitialized(reconnecting), 'silent': true diff --git a/src/headless/shared/_converse.js b/src/headless/shared/_converse.js index 11e1296916..89e8cde201 100644 --- a/src/headless/shared/_converse.js +++ b/src/headless/shared/_converse.js @@ -97,6 +97,15 @@ class ConversePrivateGlobal extends EventEmitter(Object) { this.api = /** @type {module:shared-api.APIEndpoint} */ null; + /** + * Namespace for storing translated strings. + */ + this.labels = + /** + * @typedef {Record} UserMessage + * @typedef {Record} UserMessage + * @type {UserMessages} */{}; + /** * Namespace for storing code that might be useful to 3rd party * plugins. We want to make it possible for 3rd party plugins to have diff --git a/src/headless/shared/api/public.js b/src/headless/shared/api/public.js index 5e604f2f84..a06d96a184 100644 --- a/src/headless/shared/api/public.js +++ b/src/headless/shared/api/public.js @@ -84,7 +84,9 @@ export const converse = Object.assign(window.converse || {}, { setLogLevelFromRoute(); addEventListener('hashchange', setLogLevelFromRoute); - _converse.connfeedback = new ConnectionFeedback(); + const connfeedback = new ConnectionFeedback(); + Object.assign(_converse, { connfeedback }); // XXX: DEPRECATED + Object.assign(_converse.state, { connfeedback }); /* When reloading the page: * For new sessions, we need to send out a presence stanza to notify diff --git a/src/headless/shared/errors.js b/src/headless/shared/errors.js index 3174fa3104..59ebe4c165 100644 --- a/src/headless/shared/errors.js +++ b/src/headless/shared/errors.js @@ -2,4 +2,13 @@ * Custom error for indicating timeouts * @namespace converse.env */ -export class TimeoutError extends Error {} +export class TimeoutError extends Error { + + /** + * @param {string} message + */ + constructor (message) { + super(message); + this.retry_event_id = null; + } +} diff --git a/src/headless/shared/parsers.js b/src/headless/shared/parsers.js index 503b0ce0b1..61f41650cf 100644 --- a/src/headless/shared/parsers.js +++ b/src/headless/shared/parsers.js @@ -337,10 +337,9 @@ export function throwErrorIfInvalidForward (stanza) { /** * Determines whether the passed in stanza is a XEP-0333 Chat Marker - * @private * @method getChatMarker - * @param { Element } stanza - The message stanza - * @returns { Boolean } + * @param {Element} stanza - The message stanza + * @returns {Element} */ export function getChatMarker (stanza) { // If we receive more than one marker (which shouldn't happen), we take diff --git a/src/headless/shared/rsm.js b/src/headless/shared/rsm.js index 1639197eac..467331ccaa 100644 --- a/src/headless/shared/rsm.js +++ b/src/headless/shared/rsm.js @@ -6,7 +6,6 @@ * Some code taken from the Strophe RSM plugin, licensed under the MIT License * Copyright 2006-2017 Strophe (https://github.com/strophe/strophejs) */ -import _converse from './_converse.js'; import { converse } from './api/index.js'; import pick from 'lodash-es/pick'; @@ -16,12 +15,12 @@ Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm'); /** - * @typedef { Object } RSMQueryParameters + * @typedef {Object} RSMQueryParameters * [XEP-0059 RSM](https://xmpp.org/extensions/xep-0059.html) Attributes that can be used to filter query results - * @property { String } [after] - The XEP-0359 stanza ID of a message after which messages should be returned. Implies forward paging. - * @property { String } [before] - The XEP-0359 stanza ID of a message before which messages should be returned. Implies backward paging. - * @property { number } [index=0] - The index of the results page to return. - * @property { number } [max] - The maximum number of items to return. + * @property {String} [after] - The XEP-0359 stanza ID of a message after which messages should be returned. Implies forward paging. + * @property {String} [before] - The XEP-0359 stanza ID of a message before which messages should be returned. Implies backward paging. + * @property {number} [index=0] - The index of the results page to return. + * @property {number} [max] - The maximum number of items to return. */ const RSM_QUERY_PARAMETERS = ['after', 'before', 'index', 'max']; diff --git a/src/headless/shared/settings/constants.js b/src/headless/shared/settings/constants.js index e8738888f2..5933592cbc 100644 --- a/src/headless/shared/settings/constants.js +++ b/src/headless/shared/settings/constants.js @@ -38,17 +38,18 @@ export const DEFAULT_SETTINGS = { assets_path: '/dist', authentication: 'login', // Available values are "login", "prebind", "anonymous" and "external". auto_login: false, // Currently only used in connection with anonymous login - reuse_scram_keys: false, auto_reconnect: true, blacklisted_plugins: [], clear_cache_on_logout: false, connection_options: {}, credentials_url: null, // URL from where login credentials can be fetched + disable_effects: false, // Disabled UI transition effects. Mainly used for tests. discover_connection_methods: true, geouri_regex: /https\:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([\-0-9.]+)\/([\-0-9.]+)\S*/g, geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2', i18n: undefined, jid: undefined, + reuse_scram_keys: false, keepalive: true, loglevel: 'info', locales: [ diff --git a/src/headless/utils/index.js b/src/headless/utils/index.js index fec8e8319b..3baa31298e 100644 --- a/src/headless/utils/index.js +++ b/src/headless/utils/index.js @@ -51,6 +51,7 @@ import { /** * The utils object * @namespace u + * @type {Record} */ const u = {}; diff --git a/src/headless/utils/jid.js b/src/headless/utils/jid.js index 0c4afe28c4..8989fe5e4a 100644 --- a/src/headless/utils/jid.js +++ b/src/headless/utils/jid.js @@ -25,6 +25,9 @@ export function isSameDomain (jid1, jid2) { return Strophe.getDomainFromJid(jid1).toLowerCase() === Strophe.getDomainFromJid(jid2).toLowerCase(); } +/** + * @param {string} jid + */ export function getJIDFromURI (jid) { return jid.startsWith('xmpp:') && jid.endsWith('?join') ? jid.replace(/^xmpp:/, '').replace(/\?join$/, '') diff --git a/src/plugins/chatboxviews/index.js b/src/plugins/chatboxviews/index.js index edd2ec0ad7..4bd2f82d25 100644 --- a/src/plugins/chatboxviews/index.js +++ b/src/plugins/chatboxviews/index.js @@ -24,7 +24,9 @@ converse.plugins.add('converse-chatboxviews', { // configuration settings. api.settings.extend({ 'animate': true }); - _converse.chatboxviews = new ChatBoxViews(); + const chatboxviews = new ChatBoxViews(); + Object.assign(_converse, { chatboxviews }); // XXX DEPRECATED + Object.assign(_converse.state, { chatboxviews }); /************************ BEGIN Event Handlers ************************/ api.listen.on('chatBoxesInitialized', () => { diff --git a/src/plugins/chatview/tests/receipts.js b/src/plugins/chatview/tests/receipts.js index c3f41f1747..5ab02f1165 100644 --- a/src/plugins/chatview/tests/receipts.js +++ b/src/plugins/chatview/tests/receipts.js @@ -130,7 +130,7 @@ describe("A delivery receipt", function () { await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 1); // Also handle receipts with type 'chat'. See #1353 - spyOn(_converse, 'handleMessageStanza').and.callThrough(); + spyOn(_converse.exports, 'handleMessageStanza').and.callThrough(); textarea.value = 'Another message'; message_form.onKeyDown({ target: textarea, @@ -149,6 +149,6 @@ describe("A delivery receipt", function () { }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree(); api.connection.get()._dataRecv(mock.createRequest(msg)); await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 2); - expect(_converse.handleMessageStanza.calls.count()).toBe(1); + expect(_converse.exports.handleMessageStanza.calls.count()).toBe(1); })); }); diff --git a/src/plugins/headlines-view/view.js b/src/plugins/headlines-view/view.js index 424103de8b..2ee064f9e7 100644 --- a/src/plugins/headlines-view/view.js +++ b/src/plugins/headlines-view/view.js @@ -9,7 +9,6 @@ class HeadlinesFeedView extends BaseChatView { _converse.chatboxviews.add(this.jid, this); this.model = _converse.chatboxes.get(this.jid); - this.model.disable_mam = true; // Don't do MAM queries for this box this.listenTo(this.model, 'change:hidden', () => this.afterShown()); this.listenTo(this.model, 'destroy', this.remove); this.listenTo(this.model.messages, 'add', () => this.requestUpdate()); diff --git a/src/plugins/mam-views/tests/mam.js b/src/plugins/mam-views/tests/mam.js index 368cc90006..51721f64e4 100644 --- a/src/plugins/mam-views/tests/mam.js +++ b/src/plugins/mam-views/tests/mam.js @@ -577,7 +577,7 @@ describe("Message Archive Management", function () { const { api } = _converse; const entity = await _converse.api.disco.entities.get(_converse.domain); - spyOn(_converse, 'onMAMPreferences').and.callThrough(); + spyOn(_converse.exports, 'onMAMPreferences').and.callThrough(); api.settings.set('message_archiving', 'never'); const feature = new Model({ @@ -609,8 +609,8 @@ describe("Message Archive Management", function () { .c('never').c('jid').t('montague@montague.lit'); _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); - await u.waitUntil(() => _converse.onMAMPreferences.calls.count()); - expect(_converse.onMAMPreferences).toHaveBeenCalled(); + await u.waitUntil(() => _converse.exports.onMAMPreferences.calls.count()); + expect(_converse.exports.onMAMPreferences).toHaveBeenCalled(); sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(s => sizzle('iq[type="set"] prefs[xmlns="urn:xmpp:mam:2"]', s).length).pop()); expect(Strophe.serialize(sent_stanza)).toBe( diff --git a/src/plugins/muc-views/muc.js b/src/plugins/muc-views/muc.js index fe792c37f7..d3a59ac642 100644 --- a/src/plugins/muc-views/muc.js +++ b/src/plugins/muc-views/muc.js @@ -9,7 +9,7 @@ export default class MUCView extends BaseChatView { async initialize () { this.model = await api.rooms.get(this.jid); - _converse.chatboxviews.add(this.jid, this); + _converse.state.chatboxviews.add(this.jid, this); this.setAttribute('id', this.model.get('box_id')); this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged); diff --git a/src/plugins/muc-views/tests/modtools.js b/src/plugins/muc-views/tests/modtools.js index 3162ac18ed..d31500f414 100644 --- a/src/plugins/muc-views/tests/modtools.js +++ b/src/plugins/muc-views/tests/modtools.js @@ -462,16 +462,16 @@ describe("The groupchat moderator tool", function () { await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal')); const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid}); - expect(_converse.getAssignableAffiliations(occupant)).toEqual(['owner', 'admin', 'member', 'outcast', 'none']); + expect(_converse.exports.getAssignableAffiliations(occupant)).toEqual(['owner', 'admin', 'member', 'outcast', 'none']); _converse.api.settings.set('modtools_disable_assign', ['owner']); - expect(_converse.getAssignableAffiliations(occupant)).toEqual(['admin', 'member', 'outcast', 'none']); + expect(_converse.exports.getAssignableAffiliations(occupant)).toEqual(['admin', 'member', 'outcast', 'none']); _converse.api.settings.set('modtools_disable_assign', ['owner', 'admin']); - expect(_converse.getAssignableAffiliations(occupant)).toEqual(['member', 'outcast', 'none']); + expect(_converse.exports.getAssignableAffiliations(occupant)).toEqual(['member', 'outcast', 'none']); _converse.api.settings.set('modtools_disable_assign', ['owner', 'admin', 'outcast']); - expect(_converse.getAssignableAffiliations(occupant)).toEqual(['member', 'none']); + expect(_converse.exports.getAssignableAffiliations(occupant)).toEqual(['member', 'none']); expect(_converse.getAssignableRoles(occupant)).toEqual(['moderator', 'participant', 'visitor']); diff --git a/src/shared/chat/utils.js b/src/shared/chat/utils.js index 95b4c86ed0..9b4a9e9e9d 100644 --- a/src/shared/chat/utils.js +++ b/src/shared/chat/utils.js @@ -172,6 +172,14 @@ export function getTonedEmojis () { return converse.emojis.toned; } +/** + * @typedef {object} EmojiMarkupOptions + * @property {boolean} [unicode_only=false] + * @property {boolean} [add_title_wrapper=false] + * + * @param {object} data + * @param {EmojiMarkupOptions} options + */ export function getEmojiMarkup (data, options={unicode_only: false, add_title_wrapper: false}) { const emoji = data.emoji; const shortname = data.shortname; diff --git a/src/shared/rich-text.js b/src/shared/rich-text.js index 7e1cfc9e40..95dce81c6e 100644 --- a/src/shared/rich-text.js +++ b/src/shared/rich-text.js @@ -172,14 +172,18 @@ export class RichText extends String { /** * Look for emojis (shortnames or unicode) and add templates for rendering them. - * @param { String } text - * @param { number } offset - The index of the passed in text relative to + * @param {String} text + * @param {number} offset - The index of the passed in text relative to * the start of the message body text. */ addEmojis (text, offset) { const references = [...getShortnameReferences(text.toString()), ...getCodePointReferences(text.toString())]; references.forEach(e => { - this.addTemplateResult(e.begin + offset, e.end + offset, getEmojiMarkup(e, { 'add_title_wrapper': true })); + this.addTemplateResult( + e.begin + offset, + e.end + offset, + getEmojiMarkup(e, { add_title_wrapper: true }) + ); }); } diff --git a/src/shared/tests/mock.js b/src/shared/tests/mock.js index 8f6768b5cb..cf5e8fae59 100644 --- a/src/shared/tests/mock.js +++ b/src/shared/tests/mock.js @@ -690,6 +690,7 @@ async function _initConverse (settings) { _converse = await converse.initialize(Object.assign({ animate: false, + disable_effects: true, auto_subscribe: false, bosh_service_url: 'montague.lit/http-bind', discover_connection_methods: false, @@ -713,7 +714,6 @@ async function _initConverse (settings) { if (settings?.auto_login !== false) { await _converse.api.user.login('romeo@montague.lit/orchard', 'secret'); } - window.converse_disable_effects = true; return _converse; } diff --git a/src/utils/html.js b/src/utils/html.js index bd81dabc42..a2b7c8eec6 100644 --- a/src/utils/html.js +++ b/src/utils/html.js @@ -18,7 +18,7 @@ import tplFormUsername from '../templates/form_username.js'; import tplHyperlink from 'templates/hyperlink.js'; import tplVideo from 'templates/video.js'; import u from '../headless/utils/index.js'; -import { converse, log } from '@converse/headless'; +import { api, converse, log } from '@converse/headless'; import { getURI, isAudioURL, isImageURL, isVideoURL, isValidURL } from '@converse/headless/utils/url.js'; import { render } from 'lit'; import { queryChildren } from '@converse/headless/utils/html.js'; @@ -66,7 +66,7 @@ function stripEmptyTextNodes (el) { return NodeFilter.FILTER_ACCEPT; }); while (n = walker.nextNode()) text_nodes.push(n); - text_nodes.forEach((n) => EMPTY_TEXT_REGEX.test(n.data) && n.parentElement.removeChild(n)) + text_nodes.forEach((n) => EMPTY_TEXT_REGEX.test(/** @type {Text} */(n).data) && n.parentElement.removeChild(n)) return el; } @@ -372,7 +372,7 @@ export function slideOut (el, duration = 200) { cancelAnimationFrame(Number(marker)); } const end_height = calculateElementHeight(el); - if (window.converse_disable_effects) { + if (api.settings.get('disable_effects')) { // Effects are disabled (for tests) el.style.height = end_height + 'px'; slideOutWrapup(el); @@ -424,7 +424,7 @@ export function slideIn (el, duration = 200) { return reject(new Error(err)); } else if (hasClass('collapsed', el)) { return resolve(el); - } else if (window.converse_disable_effects) { + } else if (api.settings.get('disable_effects')) { // Effects are disabled (for tests) el.classList.add('collapsed'); el.style.height = '';