diff --git a/CHANGES.md b/CHANGES.md index ff9ab09498..b0ec7be097 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ - Remove call to `api.confirm` in `@converse/headless` - Generate TypeScript declaration files into `dist/types` +- New config option [stanza_timeout](https://conversejs.org/docs/html/configuration.html#stanza-timeout) + ## 10.1.2 (2023-02-17) - #1490: Busy-loop when fetching registration form fails diff --git a/Makefile b/Makefile index 178e3b60c7..780857c108 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ po: .PHONY: release release: find ./src -name "*~" -exec rm {} \; - $(SED) -i '/^_converse.VERSION_NAME =/s/=.*/= "v$(VERSION)";/' src/headless/core.js + $(SED) -i '/^export const VERSION_NAME =/s/=.*/= "v$(VERSION)";/' src/headless/shared/constants.js $(SED) -i '/Version:/s/:.*/: $(VERSION)/' COPYRIGHT $(SED) -i '/Project-Id-Version:/s/:.*/: Converse.js $(VERSION)\n"/' src/i18n/converse.pot $(SED) -i '/"version":/s/:.*/: "$(VERSION)",/' manifest.json diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index d2ee6d14a8..29f20988b7 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -2089,6 +2089,13 @@ themselves). In order to support all browsers we need both an MP3 and an Ogg file. Make sure to name your files ``msg_received.ogg`` and ``msg_received.mp3``. +stanza_timeout +-------------- + +* Default: ``20000`` (20 seconds) + +The time to wait, in milliseconds, for a response stanza (for example to an IQ +request), before a timeout error is thrown and Converse stops waiting. sticky_controlbox ----------------- diff --git a/package.json b/package.json index fe57b2781a..24332846c2 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "10.1.2", "description": "Browser based XMPP chat client", "browser": "dist/converse.js", - "module": "src/converse.js", + "module": "src/index.js", "workspaces": [ "src/headless" ], diff --git a/src/entry.js b/src/entry.js index 38a6a161db..e30a438df6 100644 --- a/src/entry.js +++ b/src/entry.js @@ -57,7 +57,7 @@ const converse = { if (settings.assets_path) { __webpack_public_path__ = settings.assets_path; // eslint-disable-line no-undef } - require('./converse.js'); + require('./index.js'); Object.keys(plugins).forEach(name => converse.plugins.add(name, plugins[name])); return converse; } diff --git a/src/headless/README.md b/src/headless/README.md index 70d2e0c0a3..8f83e4b141 100644 --- a/src/headless/README.md +++ b/src/headless/README.md @@ -13,5 +13,5 @@ It's also installable with NPM/Yarn as [@converse/headless](https://www.npmjs.co The main distribution of Converse relies on the headless build. -The file [src/headless/headless.js](https://github.com/jcbrand/converse.js/blob/master/src/headless/headless.js) +The file [src/headless/index.js](https://github.com/jcbrand/converse.js/blob/master/src/headless/index.js) is used to determine which plugins are included in the build. diff --git a/src/headless/headless.js b/src/headless/index.js similarity index 94% rename from src/headless/headless.js rename to src/headless/index.js index 54efc0c135..d5e56220b1 100644 --- a/src/headless/headless.js +++ b/src/headless/index.js @@ -18,6 +18,7 @@ import "./plugins/roster/index.js"; // RFC-6121 Contacts Roster import "./plugins/smacks/index.js"; // XEP-0198 Stream Management import "./plugins/status/index.js"; import "./plugins/vcard/index.js"; // XEP-0054 VCard-temp +import "./plugins/blocking/index.js"; // XEP-0191 Blocking Command /* END: Removable components */ import { converse } from "./core.js"; diff --git a/src/headless/log.js b/src/headless/log.js index 20bdfdba71..79bcaf97f4 100644 --- a/src/headless/log.js +++ b/src/headless/log.js @@ -22,7 +22,7 @@ const logger = Object.assign({ * The log namespace * @namespace log */ -const log = { +export default { /** * The the log-level, which determines how verbose the logging is. @@ -95,5 +95,3 @@ const log = { this.log(message, 'fatal', style); } } - -export default log; diff --git a/src/headless/package.json b/src/headless/package.json index bf85bc3a2e..c6ca240008 100644 --- a/src/headless/package.json +++ b/src/headless/package.json @@ -10,7 +10,7 @@ "homepage": "https://conversejs.org", "license": "MPL-2.0", "main": "dist/converse-headless.js", - "module": "headless.js", + "module": "index.js", "keywords": [ "converse.js", "XMPP", diff --git a/src/headless/plugins/blocking/api.js b/src/headless/plugins/blocking/api.js new file mode 100644 index 0000000000..7141629949 --- /dev/null +++ b/src/headless/plugins/blocking/api.js @@ -0,0 +1,157 @@ +import log from '@converse/headless/log.js'; +import { _converse, api, converse } from '@converse/headless/core.js'; + +const { Strophe, $iq, sizzle, u } = converse.env; + +export default { + /** + * Retrieves the blocklist held by the logged in user at a JID by sending an IQ stanza. + * Saves the model variable _converse.blocked.set + * @private + * @method api.refreshBlocklist + */ + async refreshBlocklist () { + const features = await api.disco.getFeatures(_converse.domain); + if (!features?.findWhere({ 'var': Strophe.NS.BLOCKING })) { + return false; + } + if (!_converse.connection) { + return false; + } + + const iq = $iq({ + 'type': 'get', + 'id': u.getUniqueId('blocklist'), + }).c('blocklist', { 'xmlns': Strophe.NS.BLOCKING }); + + const result = await api.sendIQ(iq).catch((e) => { + log.fatal(e); + return null; + }); + if (result === null) { + const err_msg = `An error occured while fetching the blocklist`; + const { __ } = converse.env; + api.alert('error', __('Error'), err_msg); + log(err_msg, Strophe.LogLevel.WARN); + return false; + } else if (u.isErrorStanza(result)) { + log.error(`Error while fetching blocklist`); + log.error(result); + return false; + } + + const blocklist = sizzle('item', result).map((item) => item.getAttribute('jid')); + _converse.blocked.set({ 'set': new Set(blocklist) }); + return true; + }, + + /** + * Handle incoming iq stanzas in the BLOCKING namespace. Adjusts the global blocked_set. + * @private + * @method api.handleBlockingStanza + * @param { Object } [stanza] - The incoming stanza to handle + */ + handleBlockingStanza (stanza) { + if (stanza.firstElementChild.tagName === 'block') { + const users_to_block = sizzle('item', stanza).map((item) => item.getAttribute('jid')); + users_to_block.forEach(_converse.blocked.get('set').add, _converse.blocked.get('set')); + } else if (stanza.firstElementChild.tagName === 'unblock') { + const users_to_unblock = sizzle('item', stanza).map((item) => item.getAttribute('jid')); + users_to_unblock.forEach(_converse.blocked.get('set').delete, _converse.blocked.get('set')); + } else { + log.error('Received blocklist push update but could not interpret it.'); + } + // TODO: Fix this to not use the length as an update key, and + // use a more accurate update method, like a length-extendable hash + _converse.blocked.set({ 'len': _converse.blocked.get('set').size }); + }, + + /** + * Blocks JIDs by sending an IQ stanza + * @method api.blockUser + * + * @param { Array } [jid_list] - The list of JIDs to block + */ + async blockUser (jid_list) { + if (!_converse.disco_entities.get(_converse.domain)?.features?.findWhere({ 'var': Strophe.NS.BLOCKING })) { + return false; + } + if (!_converse.connection) { + return false; + } + + const block_items = jid_list.map((jid) => Strophe.xmlElement('item', { 'jid': jid })); + const block_element = Strophe.xmlElement('block', { 'xmlns': Strophe.NS.BLOCKING }); + + block_items.forEach(block_element.appendChild, block_element); + + const iq = $iq({ + 'type': 'set', + 'id': u.getUniqueId('block'), + }).cnode(block_element); + + const result = await api.sendIQ(iq).catch((e) => { + log.fatal(e); + return false; + }); + const err_msg = `An error occured while trying to block user(s) ${jid_list}`; + if (result === null) { + api.alert('error', __('Error'), err_msg); + log(err_msg, Strophe.LogLevel.WARN); + return false; + } else if (u.isErrorStanza(result)) { + log.error(err_msg); + log.error(result); + return false; + } + return true; + }, + + /** + * Unblocks JIDs by sending an IQ stanza to the server JID specified + * @method api.unblockUser + * @param { Array } [jid_list] - The list of JIDs to unblock + */ + async unblockUser (jid_list) { + if (!_converse.disco_entities.get(_converse.domain)?.features?.findWhere({ 'var': Strophe.NS.BLOCKING })) { + return false; + } + if (!_converse.connection) { + return false; + } + + const unblock_items = jid_list.map((jid) => Strophe.xmlElement('item', { 'jid': jid })); + const unblock_element = Strophe.xmlElement('unblock', { 'xmlns': Strophe.NS.BLOCKING }); + + unblock_items.forEach(unblock_element.append, unblock_element); + + const iq = $iq({ + 'type': 'set', + 'id': u.getUniqueId('block'), + }).cnode(unblock_element); + + const result = await api.sendIQ(iq).catch((e) => { + log.fatal(e); + return false; + }); + const err_msg = `An error occured while trying to unblock user(s) ${jid_list}`; + if (result === null) { + api.alert('error', __('Error'), err_msg); + log(err_msg, Strophe.LogLevel.WARN); + return false; + } else if (u.isErrorStanza(result)) { + log.error(err_msg); + log.error(result); + return false; + } + return true; + }, + + /** + * Retrieved the blocked set + * @method api.blockedUsers + */ + blockedUsers () { + return _converse.blocked.get('set'); + }, +}; diff --git a/src/headless/plugins/blocking/index.js b/src/headless/plugins/blocking/index.js new file mode 100644 index 0000000000..e8f13fae2c --- /dev/null +++ b/src/headless/plugins/blocking/index.js @@ -0,0 +1,36 @@ +/** + * @description + * Converse.js plugin which adds support for XEP-0191: Blocking + * Allows users to block other users, which hides their messages. + */ +import blocking_api from './api.js'; +import { _converse, api, converse } from '@converse/headless/core.js'; +import { onConnected } from './utils.js'; +import { Model } from '@converse/skeletor/src/model.js'; + +const { Strophe } = converse.env; + +const SetModel = Model.extend({ + defaults: { + 'set': new Set(), + 'len': 0, + }, +}); + +Strophe.addNamespace('BLOCKING', 'urn:xmpp:blocking'); + +converse.plugins.add('converse-blocking', { + dependencies: ['converse-disco'], + + enabled () { + return !api.settings.get('blacklisted_plugins').includes('converse-blocking'); + }, + + initialize () { + _converse.blocked = new SetModel(); + Object.assign(api, blocking_api); + + api.listen.on('discoInitialized', onConnected); + api.listen.on('reconnected', onConnected); + }, +}); diff --git a/src/headless/plugins/blocking/utils.js b/src/headless/plugins/blocking/utils.js new file mode 100644 index 0000000000..f8faedb98c --- /dev/null +++ b/src/headless/plugins/blocking/utils.js @@ -0,0 +1,8 @@ +import { _converse, api, converse } from "@converse/headless/core.js"; + +const { Strophe } = converse.env; + +export function onConnected () { + api.refreshBlocklist(); + _converse.connection.addHandler(api.handleBlockingStanza, Strophe.NS.BLOCKING, 'iq', 'set', null, null); +} diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js index cbdff51f18..0e9eac7dd3 100644 --- a/src/headless/plugins/chat/model.js +++ b/src/headless/plugins/chat/model.js @@ -5,6 +5,7 @@ import isObject from "lodash-es/isObject"; import log from '@converse/headless/log'; import pick from "lodash-es/pick"; import { Model } from '@converse/skeletor/src/model.js'; +import { TimeoutError } from '../../shared/errors.js'; import { _converse, api, converse } from "../../core.js"; import { debouncedPruneHistory, handleCorrection } from '@converse/headless/shared/chat/utils.js'; import { getMediaURLsMetadata } from '@converse/headless/shared/parsers.js'; @@ -368,7 +369,7 @@ const ChatBox = ModelWithContact.extend({ }, async createMessageFromError (error) { - if (error instanceof _converse.TimeoutError) { + if (error instanceof TimeoutError) { const msg = await this.createMessage({ 'type': 'error', 'message': error.message, @@ -1089,9 +1090,10 @@ const ChatBox = ModelWithContact.extend({ * @param {_converse.Message} message */ handleUnreadMessage (message) { - if (!message?.get('body')) { - return - } + if (!message?.get('body')) return + + const { pluggable } = _converse; + if (u.isNewMessage(message)) { if (message.get('sender') === 'me') { // We remove the "scrolled" flag so that the chat area @@ -1099,7 +1101,12 @@ const ChatBox = ModelWithContact.extend({ // when the user writes a message as opposed to when a // message is received. this.ui.set('scrolled', false); - } else if (this.isHidden()) { + } else if ( + this.isHidden() || ( + pluggable.plugins['converse-blocking'] && + api.blockedUsers()?.has(message?.get('from_real_jid')) + ) + ) { this.incrementUnreadMsgsCounter(message); } else { this.sendMarkerForMessage(message); diff --git a/src/headless/plugins/mam/api.js b/src/headless/plugins/mam/api.js index e07a804cbe..0c828d860a 100644 --- a/src/headless/plugins/mam/api.js +++ b/src/headless/plugins/mam/api.js @@ -1,6 +1,7 @@ -import { RSM } from '@converse/headless/shared/rsm'; import log from '@converse/headless/log'; import sizzle from "sizzle"; +import { RSM } from '@converse/headless/shared/rsm'; +import { TimeoutError } from '../../shared/errors.js'; import { _converse, api, converse } from "@converse/headless/core"; const { Strophe, $iq, dayjs } = converse.env; @@ -270,7 +271,7 @@ export default { const { __ } = _converse; const err_msg = __("Timeout while trying to fetch archived messages."); log.error(err_msg); - error = new _converse.TimeoutError(err_msg); + error = new TimeoutError(err_msg); return { messages, error }; } else if (u.isErrorStanza(iq_result)) { diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js index a9ea9551a5..96cb8fef5f 100644 --- a/src/headless/plugins/muc/muc.js +++ b/src/headless/plugins/muc/muc.js @@ -5,17 +5,18 @@ import p from '../../utils/parse-helpers'; import pick from 'lodash-es/pick'; import sizzle from 'sizzle'; import { Model } from '@converse/skeletor/src/model.js'; +import { ROOMSTATUS } from './constants.js'; import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe'; +import { TimeoutError } from '../../shared/errors.js'; import { _converse, api, converse } from '../../core.js'; import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js'; -import { handleCorrection } from '../../shared/chat/utils.js'; import { getOpenPromise } from '@converse/openpromise'; +import { handleCorrection } from '../../shared/chat/utils.js'; import { initStorage } from '../../utils/storage.js'; import { isArchived, getMediaURLsMetadata } from '../../shared/parsers.js'; import { isUniView, getUniqueId, safeSave } from '../../utils/core.js'; import { parseMUCMessage, parseMUCPresence } from './parsers.js'; import { sendMarker } from '../../shared/actions.js'; -import { ROOMSTATUS } from './constants.js'; const { u } = converse.env; @@ -709,8 +710,8 @@ const ChatRoomMixin = { * @private * @method _converse.ChatRoom#sendTimedMessage * @param { _converse.Message|Element } message - * @returns { Promise|Promise<_converse.TimeoutError> } Returns a promise - * which resolves with the reflected message stanza or with an error stanza or {@link _converse.TimeoutError}. + * @returns { Promise|Promise } Returns a promise + * which resolves with the reflected message stanza or with an error stanza or {@link TimeoutError}. */ sendTimedMessage (el) { if (typeof el.tree === 'function') { @@ -723,9 +724,10 @@ const ChatRoomMixin = { el.setAttribute('id', id); } const promise = getOpenPromise(); - const timeoutHandler = _converse.connection.addTimedHandler(_converse.STANZA_TIMEOUT, () => { + const timeout = api.settings.get('stanza_timeout'); + const timeoutHandler = _converse.connection.addTimedHandler(timeout, () => { _converse.connection.deleteHandler(handler); - const err = new _converse.TimeoutError('Timeout Error: No response from server'); + const err = new TimeoutError('Timeout Error: No response from server'); promise.resolve(err); return false; }); @@ -775,7 +777,7 @@ const ChatRoomMixin = { if (u.isErrorStanza(result)) { log.error(result); - } else if (result instanceof _converse.TimeoutError) { + } else if (result instanceof TimeoutError) { log.error(result); message.save({ editable, @@ -1335,22 +1337,34 @@ const ChatRoomMixin = { return true; }, - getAllowedCommands () { + async getAllowedCommands () { let allowed_commands = ['clear', 'help', 'me', 'nick', 'register']; + + // Only allow blocking commands when server supports it and we also support it + if ( + await api.disco.supports(Strophe.NS.BLOCKING, _converse.domain) && + _converse.pluggable.plugins['converse-blocking']?.enabled(_converse) + ) { + allowed_commands = [...allowed_commands, ...['block', 'unblock']]; + } + 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 }); if (this.verifyAffiliations(['owner'], occupant, false)) { allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS); } else if (this.verifyAffiliations(['admin'], occupant, false)) { allowed_commands = allowed_commands.concat(ADMIN_COMMANDS); } + if (this.verifyRoles(['moderator'], occupant, false)) { allowed_commands = allowed_commands.concat(MODERATOR_COMMANDS).concat(VISITOR_COMMANDS); } else if (!this.verifyRoles(['visitor', 'participant', 'moderator'], occupant, false)) { allowed_commands = allowed_commands.concat(VISITOR_COMMANDS); } + allowed_commands.sort(); if (Array.isArray(api.settings.get('muc_disable_slash_commands'))) { diff --git a/src/headless/plugins/roster/utils.js b/src/headless/plugins/roster/utils.js index d7ceca8ec3..094a46389f 100644 --- a/src/headless/plugins/roster/utils.js +++ b/src/headless/plugins/roster/utils.js @@ -1,6 +1,7 @@ import log from "@converse/headless/log"; import { Model } from '@converse/skeletor/src/model.js'; import { RosterFilter } from '@converse/headless/plugins/roster/filter.js'; +import { STATUS_WEIGHTS } from "../../shared/constants"; import { _converse, api, converse } from "@converse/headless/core"; import { initStorage } from '@converse/headless/utils/storage.js'; import { shouldClearCache } from '@converse/headless/utils/core.js'; @@ -198,12 +199,12 @@ export function rejectPresenceSubscription (jid, message) { export function contactsComparator (contact1, contact2) { const status1 = contact1.presence.get('show') || 'offline'; const status2 = contact2.presence.get('show') || 'offline'; - if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) { + if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) { const name1 = (contact1.getDisplayName()).toLowerCase(); const name2 = (contact2.getDisplayName()).toLowerCase(); return name1 < name2 ? -1 : (name1 > name2? 1 : 0); } else { - return _converse.STATUS_WEIGHTS[status1] < _converse.STATUS_WEIGHTS[status2] ? -1 : 1; + return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1; } } diff --git a/src/headless/plugins/status/api.js b/src/headless/plugins/status/api.js index 63379e8a2a..e689538bf6 100644 --- a/src/headless/plugins/status/api.js +++ b/src/headless/plugins/status/api.js @@ -1,4 +1,5 @@ -import { _converse, api } from '@converse/headless/core'; +import { _converse, api } from '../../core'; +import { STATUS_WEIGHTS } from '../../shared/constants'; export default { @@ -32,7 +33,7 @@ export default { */ async set (value, message) { const data = {'status': value}; - if (!Object.keys(_converse.STATUS_WEIGHTS).includes(value)) { + if (!Object.keys(STATUS_WEIGHTS).includes(value)) { throw new Error( 'Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1' ); diff --git a/src/headless/shared/_converse.js b/src/headless/shared/_converse.js index 279316f6d1..3052148f6f 100644 --- a/src/headless/shared/_converse.js +++ b/src/headless/shared/_converse.js @@ -1,15 +1,37 @@ import i18n from './i18n.js'; import log from '../log.js'; import pluggable from 'pluggable.js/src/pluggable.js'; -import { CONNECTION_STATUS, VERSION_NAME } from './constants'; import { Events } from '@converse/skeletor/src/events.js'; import { Router } from '@converse/skeletor/src/router.js'; -import { TimeoutError } from './errors.js'; import { createStore, getDefaultStore } from '../utils/storage.js'; import { getInitSettings } from './settings/utils.js'; import { getOpenPromise } from '@converse/openpromise'; import { shouldClearCache } from '../utils/core.js'; +import { + ACTIVE, + ANONYMOUS, + CHATROOMS_TYPE, + CLOSED, + COMPOSING, + CONTROLBOX_TYPE, + DEFAULT_IMAGE, + DEFAULT_IMAGE_TYPE, + EXTERNAL, + FAILURE, + GONE, + HEADLINES_TYPE, + INACTIVE, + LOGIN, + LOGOUT, + OPENED, + PAUSED, + PREBIND, + PRIVATE_CHAT_TYPE, + SUCCESS, + VERSION_NAME +} from './constants'; + /** * A private, closured object containing the private api (via {@link _converse.api}) @@ -19,9 +41,8 @@ import { shouldClearCache } from '../utils/core.js'; */ const _converse = { log, - shouldClearCache, // TODO: Should be moved to utils with next major release - CONNECTION_STATUS, + shouldClearCache, // TODO: Should be moved to utils with next major release VERSION_NAME, templates: {}, @@ -29,61 +50,42 @@ const _converse = { 'initialized': getOpenPromise() }, - STATUS_WEIGHTS: { - 'offline': 6, - 'unavailable': 5, - 'xa': 4, - 'away': 3, - 'dnd': 2, - 'chat': 1, // We currently don't differentiate between "chat" and "online" - 'online': 1 - }, - ANONYMOUS: 'anonymous', - CLOSED: 'closed', - EXTERNAL: 'external', - LOGIN: 'login', - LOGOUT: 'logout', - OPENED: 'opened', - PREBIND: 'prebind', - - /** - * @constant - * @type { number } - */ - STANZA_TIMEOUT: 20000, - - SUCCESS: 'success', - FAILURE: 'failure', - - // Generated from css/images/user.svg - DEFAULT_IMAGE_TYPE: 'image/svg+xml', - DEFAULT_IMAGE: "PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+CiA8cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgZmlsbD0iIzU1NSIvPgogPGNpcmNsZSBjeD0iNjQiIGN5PSI0MSIgcj0iMjQiIGZpbGw9IiNmZmYiLz4KIDxwYXRoIGQ9Im0yOC41IDExMiB2LTEyIGMwLTEyIDEwLTI0IDI0LTI0IGgyMyBjMTQgMCAyNCAxMiAyNCAyNCB2MTIiIGZpbGw9IiNmZmYiLz4KPC9zdmc+Cg==", - + // TODO: remove constants in next major release + ANONYMOUS, + CLOSED, + EXTERNAL, + LOGIN, + LOGOUT, + OPENED, + PREBIND, + + SUCCESS, + FAILURE, + + DEFAULT_IMAGE_TYPE, + DEFAULT_IMAGE, + + INACTIVE, + ACTIVE, + COMPOSING, + PAUSED, + GONE, + + PRIVATE_CHAT_TYPE, + CHATROOMS_TYPE, + HEADLINES_TYPE, + CONTROLBOX_TYPE, + + // Set as module attr so that we can override in tests. + // TODO: replace with config settings TIMEOUTS: { - // Set as module attr so that we can override in tests. PAUSED: 10000, INACTIVE: 90000 }, - // XEP-0085 Chat states - // https://xmpp.org/extensions/xep-0085.html - INACTIVE: 'inactive', - ACTIVE: 'active', - COMPOSING: 'composing', - PAUSED: 'paused', - GONE: 'gone', - - // Chat types - PRIVATE_CHAT_TYPE: 'chatbox', - CHATROOMS_TYPE: 'chatroom', - HEADLINES_TYPE: 'headline', - CONTROLBOX_TYPE: 'controlbox', - default_connection_options: {'explicitResourceBinding': true}, router: new Router(), - TimeoutError: TimeoutError, - isTestEnv: () => { return getInitSettings()['bosh_service_url'] === 'montague.lit/http-bind'; }, diff --git a/src/headless/shared/api/public.js b/src/headless/shared/api/public.js index 743c287564..adeed037e4 100644 --- a/src/headless/shared/api/public.js +++ b/src/headless/shared/api/public.js @@ -6,13 +6,15 @@ import i18n from '../i18n'; import log from '../../log.js'; import sizzle from 'sizzle'; import u, { setUnloadEvent } from '../../utils/core.js'; -import { CHAT_STATES, KEYCODES } from '../constants.js'; +import { ANONYMOUS, CHAT_STATES, KEYCODES, VERSION_NAME } from '../constants.js'; import { Collection } from "@converse/skeletor/src/collection"; import { Model } from '@converse/skeletor/src/model.js'; import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe'; +import { TimeoutError } from '../errors.js'; import { html } from 'lit'; import { initAppSettings } from '../settings/utils.js'; import { sprintf } from 'sprintf-js'; +import { stx } from '../../utils/stanza.js'; import { cleanup, @@ -70,7 +72,7 @@ export const converse = Object.assign(window.converse || {}, { _converse.strict_plugin_dependencies = settings.strict_plugin_dependencies; // Needed by pluggable.js log.setLogLevel(api.settings.get("loglevel")); - if (api.settings.get("authentication") === _converse.ANONYMOUS) { + if (api.settings.get("authentication") === ANONYMOUS) { if (api.settings.get("auto_login") && !api.settings.get('jid')) { throw new Error("Config Error: you need to provide the server's " + "domain via the 'jid' option when using anonymous " + @@ -169,18 +171,19 @@ export const converse = Object.assign(window.converse || {}, { /** * Utility methods and globals from bundled 3rd party libraries. * @typedef ConverseEnv - * @property {function} converse.env.$build - Creates a Strophe.Builder, for creating stanza objects. - * @property {function} converse.env.$iq - Creates a Strophe.Builder with an element as the root. - * @property {function} converse.env.$msg - Creates a Strophe.Builder with an element as the root. - * @property {function} converse.env.$pres - Creates a Strophe.Builder with an element as the root. - * @property {function} converse.env.Promise - The Promise implementation used by Converse. - * @property {function} converse.env.Strophe - The [Strophe](http://strophe.im/strophejs) XMPP library used by Converse. - * @property {function} converse.env.f - And instance of Lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods. - * @property {function} converse.env.sizzle - [Sizzle](https://sizzlejs.com) CSS selector engine. - * @property {function} converse.env.sprintf - * @property {object} converse.env._ - The instance of [lodash-es](http://lodash.com) used by Converse. - * @property {object} converse.env.dayjs - [DayJS](https://github.com/iamkun/dayjs) date manipulation library. - * @property {object} converse.env.utils - Module containing common utility methods used by Converse. + * @property { Error } converse.env.TimeoutError + * @property { function } converse.env.$build - Creates a Strophe.Builder, for creating stanza objects. + * @property { function } converse.env.$iq - Creates a Strophe.Builder with an element as the root. + * @property { function } converse.env.$msg - Creates a Strophe.Builder with an element as the root. + * @property { function } converse.env.$pres - Creates a Strophe.Builder with an element as the root. + * @property { function } converse.env.Promise - The Promise implementation used by Converse. + * @property { function } converse.env.Strophe - The [Strophe](http://strophe.im/strophejs) XMPP library used by Converse. + * @property { function } converse.env.f - And instance of Lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods. + * @property { function } converse.env.sizzle - [Sizzle](https://sizzlejs.com) CSS selector engine. + * @property { function } converse.env.sprintf + * @property { object } converse.env._ - The instance of [lodash-es](http://lodash.com) used by Converse. + * @property { object } converse.env.dayjs - [DayJS](https://github.com/iamkun/dayjs) date manipulation library. + * @property { object } converse.env.utils - Module containing common utility methods used by Converse. * @memberOf converse */ 'env': { @@ -193,13 +196,15 @@ export const converse = Object.assign(window.converse || {}, { Model, Promise, Strophe, + TimeoutError, URI, + VERSION_NAME, dayjs, html, log, sizzle, sprintf, - stx: u.stx, + stx, u, } }); diff --git a/src/headless/shared/api/send.js b/src/headless/shared/api/send.js index 27b3ae4a0d..a6675918cf 100644 --- a/src/headless/shared/api/send.js +++ b/src/headless/shared/api/send.js @@ -1,4 +1,4 @@ -import _converse from '@converse/headless/shared/_converse.js'; +import _converse from '../../shared/_converse.js'; import log from '../../log.js'; import { Strophe } from 'strophe.js/src/strophe'; import { TimeoutError } from '../errors.js'; @@ -43,7 +43,8 @@ export default { * Send an IQ stanza * @method _converse.api.sendIQ * @param { Element } stanza - * @param { number } [timeout=_converse.STANZA_TIMEOUT] + * @param { number } [timeout] - The default timeout value is taken from + * the `stanza_timeout` configuration setting. * @param { Boolean } [reject=true] - Whether an error IQ should cause the promise * to be rejected. If `false`, the promise will resolve instead of being rejected. * @returns { Promise } A promise which resolves (or potentially rejected) once we @@ -51,13 +52,13 @@ export default { * If the IQ stanza being sent is of type `result` or `error`, there's * nothing to wait for, so an already resolved promise is returned. */ - sendIQ (stanza, timeout=_converse.STANZA_TIMEOUT, reject=true) { + sendIQ (stanza, timeout, reject=true) { const { api, connection } = _converse; let promise; stanza = stanza.tree?.() ?? stanza; if (['get', 'set'].includes(stanza.getAttribute('type'))) { - timeout = timeout || _converse.STANZA_TIMEOUT; + timeout = timeout || api.settings.get('stanza_timeout'); if (reject) { promise = new Promise((resolve, reject) => connection.sendIQ(stanza, resolve, reject, timeout)); promise.catch((e) => { diff --git a/src/headless/shared/api/user.js b/src/headless/shared/api/user.js index 711478a708..49ed271bef 100644 --- a/src/headless/shared/api/user.js +++ b/src/headless/shared/api/user.js @@ -4,6 +4,7 @@ import u, { replacePromise } from '../../utils/core.js'; import { attemptNonPreboundSession, initConnection, setUserJID } from '../../utils/init.js'; import { getOpenPromise } from '@converse/openpromise'; import { user_settings_api } from '../settings/api.js'; +import { LOGOUT, PREBIND } from '../constants.js'; export default { /** @@ -60,7 +61,7 @@ export default { if (bosh_plugin?.enabled()) { if (await _converse.restoreBOSHSession()) { return; - } else if (api.settings.get("authentication") === _converse.PREBIND && (!automatic || api.settings.get("auto_login"))) { + } else if (api.settings.get("authentication") === PREBIND && (!automatic || api.settings.get("auto_login"))) { return _converse.startNewPreboundBOSHSession(); } } @@ -100,7 +101,7 @@ export default { promise.resolve(); } - _converse.connection.setDisconnectionCause(_converse.LOGOUT, undefined, true); + _converse.connection.setDisconnectionCause(LOGOUT, undefined, true); if (_converse.connection !== undefined) { api.listen.once('disconnected', () => complete()); _converse.connection.disconnect(); diff --git a/src/headless/shared/connection/index.js b/src/headless/shared/connection/index.js index 231657e3e5..2528865140 100644 --- a/src/headless/shared/connection/index.js +++ b/src/headless/shared/connection/index.js @@ -1,11 +1,12 @@ import debounce from 'lodash-es/debounce'; import log from "../../log.js"; import sizzle from 'sizzle'; -import { BOSH_WAIT } from '../../shared/constants.js'; +import { ANONYMOUS, BOSH_WAIT, LOGOUT } from '../../shared/constants.js'; +import { CONNECTION_STATUS } from '../constants'; import { Strophe } from 'strophe.js/src/core.js'; import { _converse, api } from "../../core.js"; -import { getOpenPromise } from '@converse/openpromise'; import { clearSession, tearDown } from "../../utils/core.js"; +import { getOpenPromise } from '@converse/openpromise'; import { setUserJID, } from '../../utils/init.js'; const i = Object.keys(Strophe.Status).reduce((max, k) => Math.max(max, Strophe.Status[k]), 0); @@ -127,7 +128,7 @@ export class Connection extends Strophe.Connection { this._proto = new Strophe.Bosh(this); this.service = api.settings.get('bosh_service_url'); } else if (api.connection.isType('bosh') && api.settings.get("websocket_url")) { - if (api.settings.get("authentication") === _converse.ANONYMOUS) { + if (api.settings.get("authentication") === ANONYMOUS) { // When reconnecting anonymously, we need to connect with only // the domain, not the full JID that we had in our previous // (now failed) session. @@ -149,7 +150,7 @@ export class Connection extends Strophe.Connection { const conn_status = _converse.connfeedback.get('connection_status'); if (conn_status === Strophe.Status.CONNFAIL) { this.switchTransport(); - } else if (conn_status === Strophe.Status.AUTHFAIL && api.settings.get("authentication") === _converse.ANONYMOUS) { + } else if (conn_status === Strophe.Status.AUTHFAIL && api.settings.get("authentication") === ANONYMOUS) { // When reconnecting anonymously, we need to connect with only // the domain, not the full JID that we had in our previous // (now failed) session. @@ -163,7 +164,7 @@ export class Connection extends Strophe.Connection { */ api.trigger('will-reconnect'); - if (api.settings.get("authentication") === _converse.ANONYMOUS) { + if (api.settings.get("authentication") === ANONYMOUS) { await clearSession(); } return api.user.login(); @@ -264,7 +265,7 @@ export class Connection extends Strophe.Connection { if (api.settings.get("auto_reconnect")) { const reason = this.disconnection_reason; if (this.disconnection_cause === Strophe.Status.AUTHFAIL) { - if (api.settings.get("credentials_url") || api.settings.get("authentication") === _converse.ANONYMOUS) { + if (api.settings.get("credentials_url") || api.settings.get("authentication") === ANONYMOUS) { // If `credentials_url` is set, we reconnect, because we might // be receiving expirable tokens from the credentials_url. // @@ -286,7 +287,7 @@ export class Connection extends Strophe.Connection { ); return this.finishDisconnection(); } else if ( - this.disconnection_cause === _converse.LOGOUT || + this.disconnection_cause === LOGOUT || reason === Strophe.ErrorCondition.NO_AUTH_MECH || reason === "host-unknown" || reason === "remote-connection-failed" @@ -308,7 +309,7 @@ export class Connection extends Strophe.Connection { */ onConnectStatusChanged (status, message) { const { __ } = _converse; - log.debug(`Status changed to: ${_converse.CONNECTION_STATUS[status]}`); + log.debug(`Status changed to: ${CONNECTION_STATUS[status]}`); if (status === Strophe.Status.ATTACHFAIL) { this.setConnectionStatus(status); this.worker_attach_promise?.resolve(false); diff --git a/src/headless/shared/constants.js b/src/headless/shared/constants.js index b02270e5cc..c261bc652c 100644 --- a/src/headless/shared/constants.js +++ b/src/headless/shared/constants.js @@ -1,7 +1,46 @@ import { Strophe } from 'strophe.js/src/strophe'; export const BOSH_WAIT = 59; -export const VERSION_NAME = "v10.1.2"; +export const VERSION_NAME = 'v10.1.2'; + +export const STATUS_WEIGHTS = { + offline: 6, + unavailable: 5, + xa: 4, + away: 3, + dnd: 2, + chat: 1, // We don't differentiate between "chat" and "online" + online: 1, +}; + +export const ANONYMOUS = 'anonymous'; +export const CLOSED = 'closed'; +export const EXTERNAL = 'external'; +export const LOGIN = 'login'; +export const LOGOUT = 'logout'; +export const OPENED = 'opened'; +export const PREBIND = 'prebind'; +export const SUCCESS = 'success'; +export const FAILURE = 'failure'; + +// Generated from css/images/user.svg +export const DEFAULT_IMAGE_TYPE = 'image/svg+xml'; +export const DEFAULT_IMAGE = + 'PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+CiA8cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgZmlsbD0iIzU1NSIvPgogPGNpcmNsZSBjeD0iNjQiIGN5PSI0MSIgcj0iMjQiIGZpbGw9IiNmZmYiLz4KIDxwYXRoIGQ9Im0yOC41IDExMiB2LTEyIGMwLTEyIDEwLTI0IDI0LTI0IGgyMyBjMTQgMCAyNCAxMiAyNCAyNCB2MTIiIGZpbGw9IiNmZmYiLz4KPC9zdmc+Cg=='; + +// XEP-0085 Chat states +// https =//xmpp.org/extensions/xep-0085.html +export const INACTIVE = 'inactive'; +export const ACTIVE = 'active'; +export const COMPOSING = 'composing'; +export const PAUSED = 'paused'; +export const GONE = 'gone'; + +// Chat types +export const PRIVATE_CHAT_TYPE = 'chatbox'; +export const CHATROOMS_TYPE = 'chatroom'; +export const HEADLINES_TYPE = 'headline'; +export const CONTROLBOX_TYPE = 'controlbox'; export const CONNECTION_STATUS = {}; CONNECTION_STATUS[Strophe.Status.ATTACHED] = 'ATTACHED'; @@ -73,7 +112,8 @@ export const CORE_PLUGINS = [ 'converse-roster', 'converse-smacks', 'converse-status', - 'converse-vcard' + 'converse-vcard', + 'converse-blocking', ]; export const URL_PARSE_OPTIONS = { 'start': /(\b|_)(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi }; @@ -94,5 +134,5 @@ export const KEYCODES = { FORWARD_SLASH: 47, AT: 50, META: 91, - META_RIGHT: 93 -} + META_RIGHT: 93, +}; diff --git a/src/headless/shared/errors.js b/src/headless/shared/errors.js index 2dabc6cdec..3174fa3104 100644 --- a/src/headless/shared/errors.js +++ b/src/headless/shared/errors.js @@ -1,5 +1,5 @@ /** * Custom error for indicating timeouts - * @namespace _converse + * @namespace converse.env */ export class TimeoutError extends Error {} diff --git a/src/headless/shared/settings/constants.js b/src/headless/shared/settings/constants.js index 29d6f38667..e8738888f2 100644 --- a/src/headless/shared/settings/constants.js +++ b/src/headless/shared/settings/constants.js @@ -101,6 +101,7 @@ export const DEFAULT_SETTINGS = { sid: undefined, singleton: false, strict_plugin_dependencies: false, + stanza_timeout: 20000, view_mode: 'overlayed', // Choices are 'overlayed', 'fullscreen', 'mobile' websocket_url: undefined, whitelisted_plugins: [], diff --git a/src/headless/utils/core.js b/src/headless/utils/core.js index ae4b8a591f..9664b74c5c 100644 --- a/src/headless/utils/core.js +++ b/src/headless/utils/core.js @@ -8,14 +8,20 @@ import _converse from '@converse/headless/shared/_converse.js'; import compact from "lodash-es/compact"; import isObject from "lodash-es/isObject"; import last from "lodash-es/last"; -import log from '@converse/headless/log.js'; +import log from '../log.js'; import sizzle from "sizzle"; import { Model } from '@converse/skeletor/src/model.js'; import { Strophe } from 'strophe.js/src/strophe.js'; import { getOpenPromise } from '@converse/openpromise'; -import { settings_api } from '@converse/headless/shared/settings/api.js'; +import { settings_api } from '../shared/settings/api.js'; import { stx , toStanza } from './stanza.js'; +/** + * The utils object + * @namespace u + */ +const u = {}; + export function isElement (el) { return el instanceof Element || el instanceof HTMLDocument; } @@ -57,14 +63,15 @@ export function shouldClearCache () { export async function tearDown () { - await _converse.api.trigger('beforeTearDown', {'synchronous': true}); + const { api } = _converse; + await api.trigger('beforeTearDown', {'synchronous': true}); window.removeEventListener('click', _converse.onUserActivity); window.removeEventListener('focus', _converse.onUserActivity); window.removeEventListener('keypress', _converse.onUserActivity); window.removeEventListener('mousemove', _converse.onUserActivity); window.removeEventListener(_converse.unloadevent, _converse.onUserActivity); window.clearInterval(_converse.everySecondTrigger); - _converse.api.trigger('afterTearDown'); + api.trigger('afterTearDown'); return _converse; } @@ -97,13 +104,6 @@ export function prefixMentions (message) { return text; } - -/** - * The utils object - * @namespace u - */ -const u = {}; - u.isTagEqual = function (stanza, name) { if (stanza.tree?.()) { return u.isTagEqual(stanza.tree(), name); diff --git a/src/headless/utils/init.js b/src/headless/utils/init.js index 0e0d99c7bc..700f00aeb2 100644 --- a/src/headless/utils/init.js +++ b/src/headless/utils/init.js @@ -4,7 +4,7 @@ import debounce from 'lodash-es/debounce'; import localDriver from 'localforage-webextensionstorage-driver/local'; import log from '../log.js'; import syncDriver from 'localforage-webextensionstorage-driver/sync'; -import { CORE_PLUGINS } from '../shared/constants.js'; +import { ANONYMOUS, CORE_PLUGINS, EXTERNAL, LOGIN, PREBIND } from '../shared/constants.js'; import { Connection, MockConnection } from '../shared/connection/index.js'; import { Model } from '@converse/skeletor/src/model.js'; import { Strophe } from 'strophe.js/src/strophe'; @@ -43,7 +43,7 @@ export function initConnection () { const api = _converse.api; if (! api.settings.get('bosh_service_url')) { - if (api.settings.get("authentication") === _converse.PREBIND) { + if (api.settings.get("authentication") === PREBIND) { throw new Error("authentication is set to 'prebind' but we don't have a BOSH connection"); } } @@ -182,7 +182,7 @@ function initPersistentStorage (_converse, store_name) { function saveJIDtoSession (_converse, jid) { jid = _converse.session.get('jid') || jid; - if (_converse.api.settings.get("authentication") !== _converse.ANONYMOUS && !Strophe.getResourceFromJid(jid)) { + if (_converse.api.settings.get("authentication") !== ANONYMOUS && !Strophe.getResourceFromJid(jid)) { jid = jid.toLowerCase() + Connection.generateResource(); } _converse.jid = jid; @@ -387,7 +387,7 @@ async function getLoginCredentialsFromSCRAMKeys () { export async function attemptNonPreboundSession (credentials, automatic) { const { api } = _converse; - if (api.settings.get("authentication") === _converse.LOGIN) { + if (api.settings.get("authentication") === LOGIN) { // XXX: If EITHER ``keepalive`` or ``auto_login`` is ``true`` and // ``authentication`` is set to ``login``, then Converse will try to log the user in, // since we don't have a way to distinguish between wether we're @@ -417,7 +417,7 @@ export async function attemptNonPreboundSession (credentials, automatic) { if (!_converse.isTestEnv()) log.warn("attemptNonPreboundSession: Couldn't find credentials to log in with"); } else if ( - [_converse.ANONYMOUS, _converse.EXTERNAL].includes(api.settings.get("authentication")) && + [ANONYMOUS, EXTERNAL].includes(api.settings.get("authentication")) && (!automatic || api.settings.get("auto_login")) ) { connect(); @@ -446,7 +446,7 @@ export async function savedLoginInfo (jid) { async function connect (credentials) { const { api } = _converse; - if ([_converse.ANONYMOUS, _converse.EXTERNAL].includes(api.settings.get("authentication"))) { + if ([ANONYMOUS, EXTERNAL].includes(api.settings.get("authentication"))) { if (!_converse.jid) { throw new Error("Config Error: when using anonymous login " + "you need to provide the server's domain via the 'jid' option. " + @@ -457,7 +457,7 @@ async function connect (credentials) { _converse.connection.reset(); } _converse.connection.connect(_converse.jid.toLowerCase()); - } else if (api.settings.get("authentication") === _converse.LOGIN) { + } else if (api.settings.get("authentication") === LOGIN) { const password = credentials?.password ?? (_converse.connection?.pass || api.settings.get("password")); if (!password) { if (api.settings.get("auto_login")) { diff --git a/src/converse.js b/src/index.js similarity index 98% rename from src/converse.js rename to src/index.js index e93ee169a1..53d5713468 100644 --- a/src/converse.js +++ b/src/index.js @@ -4,7 +4,7 @@ * @license Mozilla Public License (MPLv2) */ -import "@converse/headless/headless"; +import "@converse/headless"; import "./i18n/index.js"; import "shared/registry.js"; import { CustomElement } from 'shared/components/element'; diff --git a/src/plugins/controlbox/controlbox.js b/src/plugins/controlbox/controlbox.js index 684fd6bed4..6611ca7240 100644 --- a/src/plugins/controlbox/controlbox.js +++ b/src/plugins/controlbox/controlbox.js @@ -1,6 +1,7 @@ import tplControlbox from './templates/controlbox.js'; import { CustomElement } from 'shared/components/element.js'; import { _converse, api, converse } from '@converse/headless/core.js'; +import { LOGOUT } from '@converse/headless/shared/constants.js'; const u = converse.env.utils; @@ -49,7 +50,7 @@ class ControlBox extends CustomElement { ev?.preventDefault?.(); if ( ev?.name === 'closeAllChatBoxes' && - (_converse.disconnection_cause !== _converse.LOGOUT || + (_converse.disconnection_cause !== LOGOUT || api.settings.get('show_controlbox_by_default')) ) { return; diff --git a/src/plugins/controlbox/loginform.js b/src/plugins/controlbox/loginform.js index a350920f4b..0636aad6f0 100644 --- a/src/plugins/controlbox/loginform.js +++ b/src/plugins/controlbox/loginform.js @@ -1,5 +1,6 @@ import bootstrap from 'bootstrap.native'; import tplLoginPanel from './templates/loginform.js'; +import { ANONYMOUS } from '@converse/headless/shared/constants'; import { CustomElement } from 'shared/components/element.js'; import { _converse, api, converse } from '@converse/headless/core.js'; import { initConnection } from '@converse/headless/utils/init.js'; @@ -36,7 +37,7 @@ class LoginForm extends CustomElement { async onLoginFormSubmitted (ev) { ev?.preventDefault(); - if (api.settings.get('authentication') === _converse.ANONYMOUS) { + if (api.settings.get('authentication') === ANONYMOUS) { return this.connect(_converse.jid); } diff --git a/src/plugins/controlbox/templates/loginform.js b/src/plugins/controlbox/templates/loginform.js index 92de74c422..e68015ca0a 100644 --- a/src/plugins/controlbox/templates/loginform.js +++ b/src/plugins/controlbox/templates/loginform.js @@ -1,31 +1,41 @@ import 'shared/components/brand-heading.js'; import tplSpinner from 'templates/spinner.js'; +import { ANONYMOUS, EXTERNAL, LOGIN, PREBIND, CONNECTION_STATUS } from '@converse/headless/shared/constants'; import { REPORTABLE_STATUSES, PRETTY_CONNECTION_STATUS, CONNECTION_STATUS_CSS_CLASS } from '../constants.js'; import { __ } from 'i18n'; -import { _converse, api } from "@converse/headless/core"; -import { html } from "lit"; - +import { _converse, api } from '@converse/headless/core'; +import { html } from 'lit'; const trust_checkbox = (checked) => { const i18n_hint_trusted = __( - 'To improve performance, we cache your data in this browser. '+ - 'Uncheck this box if this is a public computer or if you want your data to be deleted when you log out. '+ - 'It\'s important that you explicitly log out, otherwise not all cached data might be deleted. '+ - 'Please note, when using an untrusted device, OMEMO encryption is NOT available.') + 'To improve performance, we cache your data in this browser. ' + + 'Uncheck this box if this is a public computer or if you want your data to be deleted when you log out. ' + + "It's important that you explicitly log out, otherwise not all cached data might be deleted. " + + 'Please note, when using an untrusted device, OMEMO encryption is NOT available.' + ); const i18n_trusted = __('This is a trusted device'); return html` `; -} +}; const connection_url_input = () => { const i18n_connection_url = __('Connection URL'); @@ -35,53 +45,60 @@ const connection_url_input = () => {

${i18n_form_help}

- +
`; -} +}; const password_input = () => { const i18n_password = __('Password'); return html`
- + placeholder="${i18n_password}" + />
`; -} +}; const tplRegisterLink = () => { - const i18n_create_account = __("Create an account"); + const i18n_create_account = __('Create an account'); const i18n_hint_no_account = __("Don't have a chat account?"); return html`

${i18n_hint_no_account}

-

+

+ +

`; -} +}; const tplShowRegisterLink = () => { - return api.settings.get('allow_registration') && - !api.settings.get("auto_login") && - _converse.pluggable.plugins['converse-register'].enabled(_converse); -} - + return ( + api.settings.get('allow_registration') && + !api.settings.get('auto_login') && + _converse.pluggable.plugins['converse-register'].enabled(_converse) + ); +}; const auth_fields = (el) => { const authentication = api.settings.get('authentication'); const i18n_login = __('Log in'); - const i18n_xmpp_address = __("XMPP Address"); + const i18n_xmpp_address = __('XMPP Address'); const locked_domain = api.settings.get('locked_domain'); const default_domain = api.settings.get('default_domain'); const placeholder_username = ((locked_domain || default_domain) && __('Username')) || __('user@domain'); @@ -90,7 +107,8 @@ const auth_fields = (el) => { return html`
- { class="form-control" type="text" name="jid" - placeholder="${placeholder_username}"/> + placeholder="${placeholder_username}" + />
- ${ (authentication !== _converse.EXTERNAL) ? password_input() : '' } - ${ api.settings.get('show_connection_url_input') ? connection_url_input() : '' } - ${ show_trust_checkbox ? trust_checkbox(show_trust_checkbox === 'off' ? false : true) : '' } + ${authentication !== EXTERNAL ? password_input() : ''} + ${api.settings.get('show_connection_url_input') ? connection_url_input() : ''} + ${show_trust_checkbox ? trust_checkbox(show_trust_checkbox === 'off' ? false : true) : ''}
- +
- ${ tplShowRegisterLink() ? tplRegisterLink(el) : '' } + ${tplShowRegisterLink() ? tplRegisterLink() : ''} `; -} - +}; const form_fields = (el) => { const authentication = api.settings.get('authentication'); - const { ANONYMOUS, EXTERNAL, LOGIN, PREBIND } = _converse; const i18n_disconnected = __('Disconnected'); const i18n_anon_login = __('Click here to log in anonymously'); return html` - ${ (authentication == LOGIN || authentication == EXTERNAL) ? auth_fields(el) : '' } - ${ authentication == ANONYMOUS ? html`` : '' } - ${ authentication == PREBIND ? html`

${i18n_disconnected}

` : '' } + ${authentication == LOGIN || authentication == EXTERNAL ? auth_fields(el) : ''} + ${authentication == ANONYMOUS + ? html`` + : ''} + ${authentication == PREBIND ? html`

${i18n_disconnected}

` : ''} `; -} - +}; export default (el) => { const connection_status = _converse.connfeedback.get('connection_status'); @@ -132,13 +150,14 @@ export default (el) => { feedback_class = CONNECTION_STATUS_CSS_CLASS[connection_status]; } const conn_feedback_message = _converse.connfeedback.get('message'); - return html` - + return html`
-
`; diff --git a/src/plugins/register/index.js b/src/plugins/register/index.js index 28e845eaa3..89e046d1cf 100644 --- a/src/plugins/register/index.js +++ b/src/plugins/register/index.js @@ -10,6 +10,7 @@ import './panel.js'; import { __ } from 'i18n'; import { _converse, api, converse } from '@converse/headless/core'; import { setActiveForm } from './utils.js'; +import { CONNECTION_STATUS } from '@converse/headless/shared/constants'; // Strophe methods for building stanzas const { Strophe } = converse.env; @@ -35,10 +36,10 @@ converse.plugins.add('converse-register', { initialize () { const { router } = _converse; - _converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL'; - _converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED'; - _converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT'; - _converse.CONNECTION_STATUS[Strophe.Status.NOTACCEPTABLE] = 'NOTACCEPTABLE'; + CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL'; + CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED'; + CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT'; + CONNECTION_STATUS[Strophe.Status.NOTACCEPTABLE] = 'NOTACCEPTABLE'; api.settings.extend({ 'allow_registration': true, diff --git a/src/plugins/register/panel.js b/src/plugins/register/panel.js index 2ed6b31702..c05e27a84f 100644 --- a/src/plugins/register/panel.js +++ b/src/plugins/register/panel.js @@ -3,6 +3,7 @@ import tplFormInput from "templates/form_input.js"; import tplFormUrl from "templates/form_url.js"; import tplFormUsername from "templates/form_username.js"; import tplRegisterPanel from "./templates/register_panel.js"; +import { CONNECTION_STATUS } from '@converse/headless/shared/constants'; import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; import { _converse, api, converse } from "@converse/headless/core.js"; @@ -219,7 +220,7 @@ class RegisterPanel extends CustomElement { ].includes(status_code)) { log.error( - `Problem during registration: Strophe.Status is ${_converse.CONNECTION_STATUS[status_code]}` + `Problem during registration: Strophe.Status is ${CONNECTION_STATUS[status_code]}` ); this.abortRegistration(); } else if (status_code === Strophe.Status.REGISTERED) { diff --git a/src/plugins/rosterview/modals/add-contact.js b/src/plugins/rosterview/modals/add-contact.js index 268f6902c4..dcfff9919f 100644 --- a/src/plugins/rosterview/modals/add-contact.js +++ b/src/plugins/rosterview/modals/add-contact.js @@ -1,13 +1,13 @@ import 'shared/autocomplete/index.js'; import BaseModal from "plugins/modal/modal.js"; +import api from '@converse/headless/shared/api'; import compact from 'lodash-es/compact'; import debounce from 'lodash-es/debounce'; import tplAddContactModal from "./templates/add-contact.js"; +import { Strophe } from 'strophe.js/src/core.js'; import { __ } from 'i18n'; -import { _converse, api, converse } from "@converse/headless/core"; - -const { Strophe } = converse.env; -const u = converse.env.utils; +import { _converse } from "@converse/headless/core"; +import { addClass, removeClass } from 'utils/html.js'; export default class AddContactModal extends BaseModal { @@ -98,7 +98,7 @@ export default class AddContactModal extends BaseModal { if (list.length !== 1) { const el = this.querySelector('.invalid-feedback'); el.textContent = __('Sorry, could not find a contact with that name') - u.addClass('d-block', el); + addClass('d-block', el); return; } const jid = list[0].value; @@ -114,15 +114,15 @@ export default class AddContactModal extends BaseModal { validateSubmission (jid) { const el = this.querySelector('.invalid-feedback'); if (!jid || compact(jid.split('@')).length < 2) { - u.addClass('is-invalid', this.querySelector('input[name="jid"]')); - u.addClass('d-block', el); + addClass('is-invalid', this.querySelector('input[name="jid"]')); + addClass('d-block', el); return false; } else if (_converse.roster.get(Strophe.getBareJidFromJid(jid))) { el.textContent = __('This contact has already been added') - u.addClass('d-block', el); + addClass('d-block', el); return false; } - u.removeClass('d-block', el); + removeClass('d-block', el); return true; } diff --git a/src/shared/chat/message.js b/src/shared/chat/message.js index 6c0afc99a6..666a105768 100644 --- a/src/shared/chat/message.js +++ b/src/shared/chat/message.js @@ -118,6 +118,11 @@ export default class Message extends CustomElement { } renderChatMessage () { + if ( _converse.pluggable.plugins['converse-blocking']?.enabled(_converse) && + api.blockedUsers()?.has(this.getProps()?.properties?.jid) + ) { + return; + } return tplMessage(this, this.getProps()); } diff --git a/src/shared/modals/templates/user-details.js b/src/shared/modals/templates/user-details.js index 55d9a03d5f..ff651e6503 100644 --- a/src/shared/modals/templates/user-details.js +++ b/src/shared/modals/templates/user-details.js @@ -4,6 +4,17 @@ import { api } from "@converse/headless/core"; import { html } from 'lit'; import { modal_close_button } from "plugins/modal/templates/buttons.js"; + +const block_button = (o) => { + const i18n_block = __("Unblock"); + return html``; +} + +const unblock_button = (o) => { + const i18n_unblock = __("Unblock"); + return html``; +} + const remove_button = (el) => { const i18n_remove_contact = __('Remove as contact'); return html` @@ -19,6 +30,10 @@ const remove_button = (el) => { } export const tplFooter = (el) => { + const vcard = el.model?.vcard; + const vcard_json = vcard ? vcard.toJSON() : {}; + const o = { ...el.model.toJSON(), ...vcard_json }; + const is_roster_contact = el.model.contact !== undefined; const i18n_refresh = __('Refresh'); const allow_contact_removal = api.settings.get('allow_contact_removal'); @@ -33,6 +48,7 @@ export const tplFooter = (el) => { > ${i18n_refresh} ${ (allow_contact_removal && is_roster_contact) ? remove_button(el) : '' } + ${ api.blockedUsers ? ((api.blockedUsers()?.has(o.jid)) ? unblock_button(o) : block_button(o)) : '' } `; } diff --git a/src/shared/registry.js b/src/shared/registry.js index 3feb7b7b5e..c1888daa02 100644 --- a/src/shared/registry.js +++ b/src/shared/registry.js @@ -1,4 +1,4 @@ -import { api } from "@converse/headless/core"; +import api from "@converse/headless/shared/api/index.js"; const registry = {}; diff --git a/src/utils/html.js b/src/utils/html.js index 0d2fb192a8..fedfa41ad3 100644 --- a/src/utils/html.js +++ b/src/utils/html.js @@ -241,7 +241,7 @@ u.hasClass = function (className, el) { }; u.toggleClass = function (className, el) { - u.hasClass(className, el) ? u.removeClass(className, el) : u.addClass(className, el); + u.hasClass(className, el) ? removeClass(className, el) : addClass(className, el); }; /** @@ -250,10 +250,10 @@ u.toggleClass = function (className, el) { * @param { string } className * @param { Element } el */ -u.addClass = function (className, el) { +export function addClass (className, el) { el instanceof Element && el.classList.add(className); return el; -}; +} /** * Remove a class from an element. @@ -261,15 +261,20 @@ u.addClass = function (className, el) { * @param { string } className * @param { Element } el */ -u.removeClass = function (className, el) { +export function removeClass (className, el) { el instanceof Element && el.classList.remove(className); return el; -}; +} -u.removeElement = function (el) { +/** + * Remove an element from its parent + * @method u#removeElement + * @param { Element } el + */ +export function removeElement (el) { el instanceof Element && el.parentNode && el.parentNode.removeChild(el); return el; -}; +} u.getElementFromTemplateResult = function (tr) { const div = document.createElement('div'); @@ -278,8 +283,8 @@ u.getElementFromTemplateResult = function (tr) { }; u.showElement = el => { - u.removeClass('collapsed', el); - u.removeClass('hidden', el); + removeClass('collapsed', el); + removeClass('hidden', el); }; u.hideElement = function (el) { @@ -609,6 +614,15 @@ u.xForm2TemplateResult = function (field, stanza, options={}) { } }; -Object.assign(u, { getOOBURLMarkup, ancestor, slideIn, slideOut, isEqualNode }); +Object.assign(u, { + addClass, + ancestor, + getOOBURLMarkup, + isEqualNode, + removeClass, + removeElement, + slideIn, + slideOut, +}); export default u;