From 45a18900f59ef8cbca2be1a37a825b2a6dbf8cc6 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Thu, 28 Sep 2023 23:07:40 +0200 Subject: [PATCH] More work on cleaning up src/headless/utils/index.js --- src/headless/plugins/chat/model.js | 3 +- src/headless/plugins/chat/utils.js | 18 ++++ src/headless/plugins/muc/muc.js | 3 +- src/headless/plugins/muc/utils.js | 9 ++ src/headless/shared/parsers.js | 2 +- src/headless/utils/html.js | 26 +++++ src/headless/utils/index.js | 138 ++++++------------------ src/headless/utils/stanza.js | 24 +++++ src/plugins/adhoc-views/tests/adhoc.js | 1 - src/plugins/roomslist/view.js | 5 +- src/shared/autocomplete/autocomplete.js | 7 +- 11 files changed, 123 insertions(+), 113 deletions(-) create mode 100644 src/headless/utils/stanza.js diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js index d5fc7e05a1..1591bfde70 100644 --- a/src/headless/plugins/chat/model.js +++ b/src/headless/plugins/chat/model.js @@ -15,6 +15,7 @@ import { isEmptyMessage } from '../../utils/index.js'; import { isUniView } from '../../utils/session.js'; import { parseMessage } from './parsers.js'; import { sendMarker } from '../../shared/actions.js'; +import { isNewMessage } from './utils.js'; const { Strophe, $msg } = converse.env; @@ -1094,7 +1095,7 @@ const ChatBox = ModelWithContact.extend({ if (!message?.get('body')) { return } - if (u.isNewMessage(message)) { + if (isNewMessage(message)) { if (message.get('sender') === 'me') { // We remove the "scrolled" flag so that the chat area // gets scrolled down. We always want to scroll down diff --git a/src/headless/plugins/chat/utils.js b/src/headless/plugins/chat/utils.js index e72c3580a3..8a288077aa 100644 --- a/src/headless/plugins/chat/utils.js +++ b/src/headless/plugins/chat/utils.js @@ -1,3 +1,5 @@ +import sizzle from "sizzle"; +import { Model } from '@converse/skeletor/src/model.js'; import _converse from '../../shared/_converse.js'; import api, { converse } from '../../shared/api/index.js'; import log from '../../log.js'; @@ -24,6 +26,22 @@ export async function onClearSession () { } } +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 && + sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, message).length + ); + } else if (message instanceof Model) { + message = message.attributes; + } + return !(message['is_delayed'] && message['is_archived']); +} + + async function handleErrorMessage (stanza) { const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from')); if (u.isSameBareJID(from_jid, _converse.bare_jid)) { diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js index 340516e2ba..02dbee6c69 100644 --- a/src/headless/plugins/muc/muc.js +++ b/src/headless/plugins/muc/muc.js @@ -18,6 +18,7 @@ import { getUniqueId, safeSave } from '../../utils/index.js'; import { isUniView } from '../../utils/session.js'; import { parseMUCMessage, parseMUCPresence } from './parsers.js'; import { sendMarker } from '../../shared/actions.js'; +import { shouldCreateGroupchatMessage } from './utils.js'; const { u } = converse.env; @@ -2296,7 +2297,7 @@ const ChatRoomMixin = { if (attrs['chat_state']) { this.updateNotifications(attrs.nick, attrs.chat_state); } - if (u.shouldCreateGroupchatMessage(attrs)) { + if (shouldCreateGroupchatMessage(attrs)) { const msg = await handleCorrection(this, attrs) || (await this.createMessage(attrs)); this.removeNotification(attrs.nick, ['composing', 'paused']); this.handleUnreadMessage(msg); diff --git a/src/headless/plugins/muc/utils.js b/src/headless/plugins/muc/utils.js index 91500ea9dc..74f18e2905 100644 --- a/src/headless/plugins/muc/utils.js +++ b/src/headless/plugins/muc/utils.js @@ -6,6 +6,15 @@ import { safeSave } from '../../utils/index.js'; const { Strophe, sizzle, u } = converse.env; +export function isChatRoom (model) { + return model?.get('type') === 'chatroom'; +} + +export function shouldCreateGroupchatMessage (attrs) { + return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone); +} + + export function getAutoFetchedAffiliationLists () { const affs = api.settings.get('muc_fetch_members'); return Array.isArray(affs) ? affs : affs ? ['member', 'admin', 'owner'] : []; diff --git a/src/headless/shared/parsers.js b/src/headless/shared/parsers.js index bbb6ab6bb7..ca3bce80d4 100644 --- a/src/headless/shared/parsers.js +++ b/src/headless/shared/parsers.js @@ -6,7 +6,7 @@ import log from '../log.js'; import sizzle from 'sizzle'; import { Strophe } from 'strophe.js'; import { URL_PARSE_OPTIONS } from './constants.js'; -import { decodeHTMLEntities } from '../utils/index.js'; +import { decodeHTMLEntities } from '../utils/html.js'; import { rejectMessage } from './actions'; import { isAudioURL, diff --git a/src/headless/utils/html.js b/src/headless/utils/html.js index 4a3e6ea0c9..5cd7cbed80 100644 --- a/src/headless/utils/html.js +++ b/src/headless/utils/html.js @@ -1,3 +1,4 @@ +import DOMPurify from 'dompurify'; import { Strophe } from 'strophe.js'; /** @@ -62,3 +63,28 @@ export function stringToElement (s) { export function queryChildren (el, selector) { return Array.from(el.childNodes).filter(el => (el instanceof Element) && el.matches(selector)); } + +/** + * @param {Element} el - the DOM element + * @return {number} + */ +export function siblingIndex (el) { + /* eslint-disable no-cond-assign */ + for (var i = 0; el = el.previousElementSibling; i++); + return i; +} + +const element = document.createElement('div'); + +/** + * @param {string} str + * @return {string} + */ +export function decodeHTMLEntities (str) { + if (str && typeof str === 'string') { + element.innerHTML = DOMPurify.sanitize(str); + str = element.textContent; + element.textContent = ''; + } + return str; +} diff --git a/src/headless/utils/index.js b/src/headless/utils/index.js index 913d22a62f..555016bf21 100644 --- a/src/headless/utils/index.js +++ b/src/headless/utils/index.js @@ -3,16 +3,15 @@ * @license Mozilla Public License (MPLv2) * @description This is the core utilities module. */ -import DOMPurify from 'dompurify'; -import sizzle from "sizzle"; import { Model } from '@converse/skeletor/src/model.js'; -import { Strophe, toStanza } from 'strophe.js'; +import { toStanza } from 'strophe.js'; import { getOpenPromise } from '@converse/openpromise'; import { saveWindowState, shouldClearCache } from './session.js'; import { merge, isError, isFunction } from './object.js'; import { createStore, getDefaultStore } from './storage.js'; import { waitUntil } from './promise.js'; import { isValidJID, isValidMUCJID, isSameBareJID } from './jid.js'; +import { isErrorStanza } from './stanza.js'; import { getCurrentWord, getSelectValues, @@ -52,26 +51,7 @@ import { * The utils object * @namespace u */ -const u = { - arrayBufferToBase64, - arrayBufferToHex, - arrayBufferToString, - base64ToArrayBuffer, - checkFileTypes, - getSelectValues, - getURI, - isAllowedProtocolForMedia, - isAudioURL, - isError, - isFunction, - isGIFURL, - isImageURL, - isURLWithImageExtension, - isVideoURL, - shouldRenderMediaFromURL, - stringToArrayBuffer, - webForm2xForm, -}; +const u = {}; export function isEmptyMessage (attrs) { @@ -99,7 +79,7 @@ export function prefixMentions (message) { return text; } -u.getLongestSubstring = function (string, candidates) { +function getLongestSubstring (string, candidates) { function reducer (accumulator, current_value) { if (string.startsWith(current_value)) { if (current_value.length > accumulator.length) { @@ -114,80 +94,23 @@ u.getLongestSubstring = function (string, candidates) { return candidates.reduce(reducer, ''); } -u.isNewMessage = function (message) { - /* Given a stanza, determine whether it's a new - * message, i.e. not a MAM archived one. - */ - if (message instanceof Element) { - return !( - sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length && - sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, message).length - ); - } else if (message instanceof Model) { - message = message.attributes; - } - return !(message['is_delayed'] && message['is_archived']); -}; - -u.shouldCreateMessage = function (attrs) { +function shouldCreateMessage (attrs) { return attrs['retracted'] || // Retraction received *before* the message !isEmptyMessage(attrs); } -u.shouldCreateGroupchatMessage = function (attrs) { - return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone); -} - -u.isChatRoom = function (model) { - return model && (model.get('type') === 'chatroom'); -} - export function isErrorObject (o) { return o instanceof Error; } -u.isErrorStanza = function (stanza) { - if (!isElement(stanza)) { - return false; - } - return stanza.getAttribute('type') === 'error'; -} - -u.isForbiddenError = function (stanza) { - if (!isElement(stanza)) { - return false; - } - return sizzle(`error[type="auth"] forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0; -} - -u.isServiceUnavailableError = function (stanza) { - if (!isElement(stanza)) { - return false; - } - return sizzle(`error[type="cancel"] service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0; -} - -u.getAttribute = function (key, item) { - return item.get(key); -}; - -u.isPersistableModel = function (model) { - return model.collection && model.collection.browserStorage; -}; - -u.getResolveablePromise = getOpenPromise; -u.getOpenPromise = getOpenPromise; - /** * Call the callback once all the events have been triggered - * @private - * @method u#onMultipleEvents * @param { Array } events: An array of objects, with keys `object` and * `event`, representing the event name and the object it's triggered upon. * @param { Function } callback: The function to call once all events have * been triggered. */ -u.onMultipleEvents = function (events=[], callback) { +function onMultipleEvents (events=[], callback) { let triggered = []; function handler (result) { @@ -198,24 +121,20 @@ u.onMultipleEvents = function (events=[], callback) { } } events.forEach(e => e.object.on(e.event, handler)); -}; +} +function isPersistableModel (model) { + return model.collection && model.collection.browserStorage; +} export function safeSave (model, attributes, options) { - if (u.isPersistableModel(model)) { + if (isPersistableModel(model)) { model.save(attributes, options); } else { model.set(attributes, options); } } - -u.siblingIndex = function (el) { - /* eslint-disable no-cond-assign */ - for (var i = 0; el = el.previousElementSibling; i++); - return i; -}; - /** * @param {Element} el * @param {string} name @@ -251,34 +170,41 @@ export function getUniqueId (suffix) { } } - -const element = document.createElement('div'); - -export function decodeHTMLEntities (str) { - if (str && typeof str === 'string') { - element.innerHTML = DOMPurify.sanitize(str); - str = element.textContent; - element.textContent = ''; - } - return str; -} - export default Object.assign({ + arrayBufferToBase64, + arrayBufferToHex, + arrayBufferToString, + base64ToArrayBuffer, + checkFileTypes, createStore, getCurrentWord, getDefaultStore, + getLongestSubstring, + getOpenPromise, getOuterWidth, getRandomInt, + getSelectValues, + getURI, getUniqueId, + isAllowedProtocolForMedia, + isAudioURL, isElement, isEmptyMessage, + isError, isErrorObject, + isErrorStanza, + isFunction, + isGIFURL, + isImageURL, isMentionBoundary, isSameBareJID, isTagEqual, + isURLWithImageExtension, isValidJID, isValidMUCJID, + isVideoURL, merge, + onMultipleEvents, placeCaretAtEnd, prefixMentions, queryChildren, @@ -286,8 +212,12 @@ export default Object.assign({ safeSave, saveWindowState, shouldClearCache, + shouldCreateMessage, + shouldRenderMediaFromURL, + stringToArrayBuffer, stringToElement, toStanza, triggerEvent, + webForm2xForm, waitUntil, // TODO: remove. Only the API should be used }, u); diff --git a/src/headless/utils/stanza.js b/src/headless/utils/stanza.js new file mode 100644 index 0000000000..0efe9b20eb --- /dev/null +++ b/src/headless/utils/stanza.js @@ -0,0 +1,24 @@ +import sizzle from "sizzle"; +import { Strophe } from 'strophe.js'; +import { isElement } from './html.js'; + +export function isErrorStanza (stanza) { + if (!isElement(stanza)) { + return false; + } + return stanza.getAttribute('type') === 'error'; +} + +export function isForbiddenError (stanza) { + if (!isElement(stanza)) { + return false; + } + return sizzle(`error[type="auth"] forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0; +} + +export function isServiceUnavailableError (stanza) { + if (!isElement(stanza)) { + return false; + } + return sizzle(`error[type="cancel"] service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0; +} diff --git a/src/plugins/adhoc-views/tests/adhoc.js b/src/plugins/adhoc-views/tests/adhoc.js index 0af67a7858..9e39fda960 100644 --- a/src/plugins/adhoc-views/tests/adhoc.js +++ b/src/plugins/adhoc-views/tests/adhoc.js @@ -226,7 +226,6 @@ describe("Ad-hoc commands consisting of multiple steps", function () { `)); let button = await u.waitUntil(() => modal.querySelector('input[data-action="next"]')); - debugger; button.click(); sel = `iq[to="${entity_jid}"] command[sessionid="${sessionid}"]`; diff --git a/src/plugins/roomslist/view.js b/src/plugins/roomslist/view.js index a72d6b43e9..e0a3f38e1a 100644 --- a/src/plugins/roomslist/view.js +++ b/src/plugins/roomslist/view.js @@ -5,6 +5,7 @@ import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; import { _converse, api, converse } from "@converse/headless"; import { initStorage } from '@converse/headless/utils/storage.js'; +import { isChatRoom } from '@converse/headless/plugins/muc/utils.js'; const { Strophe, u } = converse.env; @@ -30,13 +31,13 @@ export class RoomsList extends CustomElement { } renderIfChatRoom (model) { - u.isChatRoom(model) && this.requestUpdate(); + isChatRoom(model) && this.requestUpdate(); } renderIfRelevantChange (model) { const attrs = ['bookmarked', 'hidden', 'name', 'num_unread', 'num_unread_general', 'has_activity']; const changed = model.changed || {}; - if (u.isChatRoom(model) && Object.keys(changed).filter(m => attrs.includes(m)).length) { + if (isChatRoom(model) && Object.keys(changed).filter(m => attrs.includes(m)).length) { this.requestUpdate(); } } diff --git a/src/shared/autocomplete/autocomplete.js b/src/shared/autocomplete/autocomplete.js index 16f072f6fb..79f6757ab0 100644 --- a/src/shared/autocomplete/autocomplete.js +++ b/src/shared/autocomplete/autocomplete.js @@ -6,10 +6,11 @@ * @license Mozilla Public License (MPLv2) */ -import { Events } from '@converse/skeletor/src/events.js'; -import { helpers, FILTER_CONTAINS, ITEM, SORT_BY_QUERY_POSITION } from './utils.js'; import Suggestion from './suggestion.js'; +import { Events } from '@converse/skeletor/src/events.js'; import { converse } from "@converse/headless"; +import { helpers, FILTER_CONTAINS, ITEM, SORT_BY_QUERY_POSITION } from './utils.js'; +import { siblingIndex } from '@converse/headless/utils/html.js'; const u = converse.env.utils; @@ -185,7 +186,7 @@ export class AutoComplete { select (selected) { if (selected) { - this.index = u.siblingIndex(selected); + this.index = siblingIndex(selected); } else { selected = this.ul.children[this.index]; }