Bladeren bron

Fix type errors and remove unused util functions

JC Brand 1 jaar geleden
bovenliggende
commit
2d5717bd3b

+ 3 - 3
package-lock.json

@@ -1834,8 +1834,8 @@
     },
     "node_modules/@converse/skeletor": {
       "version": "0.0.8",
-      "resolved": "git+ssh://git@github.com/conversejs/skeletor.git#81b33ecd868b597a0c8b108ab8e3c9c77a5d9d05",
-      "integrity": "sha512-5miToCgbGuNKCa/4ui4dtB4Z67OKsyQhqkhx2rh7jYGZTOCbmCehOYnKQVn2TI9DitcI/8dl6G5OvEJfL7y3AQ==",
+      "resolved": "git+ssh://git@github.com/conversejs/skeletor.git#0cc6669bb2e5852caa1ef29892ce3822eadbff9a",
+      "integrity": "sha512-rnonEzuZPckk9OGv0WOTeB67suaDS0MIEWD/xg/SdXlZVfWEdHeAAasSUU5VQ+VcOdnx2axgVDK8UBq4qm6Y3A==",
       "license": "MIT",
       "dependencies": {
         "@converse/localforage-getitems": "1.4.3",
@@ -11419,7 +11419,7 @@
       "license": "MPL-2.0",
       "dependencies": {
         "@converse/openpromise": "^0.0.1",
-        "@converse/skeletor": "conversejs/skeletor#81b33ecd868b597a0c8b108ab8e3c9c77a5d9d05",
+        "@converse/skeletor": "conversejs/skeletor#0cc6669bb2e5852caa1ef29892ce3822eadbff9a",
         "dayjs": "^1.11.8",
         "dompurify": "^2.3.1",
         "filesize": "^10.0.7",

+ 1 - 1
src/headless/package.json

@@ -32,7 +32,7 @@
   },
   "dependencies": {
     "@converse/openpromise": "^0.0.1",
-    "@converse/skeletor": "conversejs/skeletor#81b33ecd868b597a0c8b108ab8e3c9c77a5d9d05",
+    "@converse/skeletor": "conversejs/skeletor#0cc6669bb2e5852caa1ef29892ce3822eadbff9a",
     "dayjs": "^1.11.8",
     "dompurify": "^2.3.1",
     "filesize": "^10.0.7",

+ 19 - 7
src/headless/plugins/bookmarks/collection.js

@@ -43,7 +43,9 @@ class Bookmarks extends Collection {
         api.trigger('bookmarksInitialized', this);
     }
 
-    // eslint-disable-next-line class-methods-use-this
+    /**
+     * @param {Bookmark} bookmark
+     */
     async openBookmarkedRoom (bookmark) {
         if ( api.settings.get('muc_respect_autojoin') && bookmark.get('autojoin')) {
             const groupchat = await api.rooms.create(
@@ -123,22 +125,31 @@ class Bookmarks extends Collection {
         );
     }
 
-    // eslint-disable-next-line class-methods-use-this
+    /**
+     * @param {Bookmark} bookmark
+     */
     markRoomAsBookmarked (bookmark) {
-        const groupchat = _converse.chatboxes.get(bookmark.get('jid'));
+        const { chatboxes } = _converse.state;
+        const groupchat = chatboxes.get(bookmark.get('jid'));
         groupchat?.save('bookmarked', true);
     }
 
-    // eslint-disable-next-line class-methods-use-this
+    /**
+     * @param {Bookmark} bookmark
+     */
     markRoomAsUnbookmarked (bookmark) {
-        const groupchat = _converse.chatboxes.get(bookmark.get('jid'));
+        const { chatboxes } = _converse.state;
+        const groupchat = chatboxes.get(bookmark.get('jid'));
         groupchat?.save('bookmarked', false);
     }
 
+    /**
+     * @param {Element} stanza
+     */
     createBookmarksFromStanza (stanza) {
         const xmlns = Strophe.NS.BOOKMARKS;
         const sel = `items[node="${xmlns}"] item storage[xmlns="${xmlns}"] conference`;
-        sizzle(sel, stanza).forEach(el => {
+        sizzle(sel, stanza).forEach(/** @type {Element} */(el) => {
             const jid = el.getAttribute('jid');
             const bookmark = this.get(jid);
             const attrs = {
@@ -186,7 +197,8 @@ class Bookmarks extends Collection {
     async getUnopenedBookmarks () {
         await api.waitUntil('bookmarksInitialized')
         await api.waitUntil('chatBoxesFetched')
-        return this.filter(b => !_converse.chatboxes.get(b.get('jid')));
+        const { chatboxes } = _converse.state;
+        return this.filter(b => !chatboxes.get(b.get('jid')));
     }
 }
 

+ 5 - 4
src/headless/plugins/bookmarks/index.js

@@ -64,10 +64,11 @@ converse.plugins.add('converse-bookmarks', {
         })
 
         api.listen.on('clearSession', () => {
-            if (_converse.bookmarks) {
-                _converse.bookmarks.clearStore({'silent': true});
-                window.sessionStorage.removeItem(_converse.bookmarks.fetched_flag);
-                delete _converse.bookmarks;
+            const { state } = _converse;
+            if (state.bookmarks) {
+                state.bookmarks.clearStore({'silent': true});
+                window.sessionStorage.removeItem(state.bookmarks.fetched_flag);
+                delete state.bookmarks;
             }
         });
 

+ 1 - 2
src/headless/plugins/bookmarks/utils.js

@@ -22,8 +22,7 @@ export async function initBookmarks () {
     }
     if (await checkBookmarksSupport()) {
         _converse.state.bookmarks = new _converse.exports.Bookmarks();
-        // TODO: DEPRECATED
-        _converse.bookmarks = _converse.state.bookmarks;
+        Object.assign(_converse, { bookmarks: _converse.state.bookmarks }); // TODO: DEPRECATED
     }
 }
 

+ 2 - 2
src/headless/plugins/bosh.js

@@ -64,7 +64,7 @@ converse.plugins.add('converse-bosh', {
             const xhr = new XMLHttpRequest();
             xhr.open('GET', api.settings.get('prebind_url'), true);
             xhr.setRequestHeader('Accept', 'application/json, text/javascript');
-            xhr.onload = async function () {
+            xhr.onload = async function (event) {
                 if (xhr.status >= 200 && xhr.status < 400) {
                     const data = JSON.parse(xhr.responseText);
                     const jid = await setUserJID(data.jid);
@@ -76,7 +76,7 @@ converse.plugins.add('converse-bosh', {
                         BOSH_WAIT
                     );
                 } else {
-                    xhr.onerror();
+                    xhr.onerror(event);
                 }
             };
             xhr.onerror = function () {

+ 4 - 1
src/headless/plugins/caps/utils.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder
+ */
 import _converse from '../../shared/_converse.js';
 import { converse } from '../../shared/api/index.js';
 import { arrayBufferToBase64, stringToArrayBuffer  } from '../../utils/arraybuffer.js';
@@ -38,7 +41,7 @@ async function createCapsNode () {
 
 /**
  * Given a stanza, adds a XEP-0115 CAPS element
- * @param { Element } stanza
+ * @param {Strophe.Builder} stanza
  */
 export async function addCapsNode (stanza) {
     const caps_el = await createCapsNode();

+ 6 - 3
src/headless/plugins/chat/api.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('./model.js').default} ChatBox
+ */
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import log from "../../log.js";
@@ -14,7 +17,7 @@ export default {
     chats: {
         /**
          * @method api.chats.create
-         * @param {string|string[]} jid|jids An jid or array of jids
+         * @param {string|string[]} jids An jid or array of jids
          * @param { object } [attrs] An object containing configuration attributes.
          */
         async create (jids, attrs) {
@@ -106,7 +109,7 @@ 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.ChatBox> }
+         * @returns { Promise<ChatBox> }
          *
          * @example
          * // To return a single chat, provide the JID of the contact you're chatting with in that chat:
@@ -127,7 +130,7 @@ export default {
             async function _get (jid) {
                 let model = await api.chatboxes.get(jid);
                 if (!model && create) {
-                    model = await api.chatboxes.create(jid, attrs, _converse.ChatBox);
+                    model = await api.chatboxes.create(jid, attrs, _converse.exports.ChatBox);
                 } else {
                     model = (model && model.get('type') === PRIVATE_CHAT_TYPE) ? model : null;
                     if (model && Object.keys(attrs).length) {

+ 11 - 6
src/headless/plugins/chatboxes/index.js

@@ -23,10 +23,10 @@ converse.plugins.add('converse-chatboxes', {
             'privateChatsAutoJoined'
         ]);
 
-        Object.assign(api, { 'chatboxes': chatboxes_api});
-
-        _converse.ChatBoxes = ChatBoxes;
+        Object.assign(api, { chatboxes: chatboxes_api});
 
+        Object.assign(_converse, { ChatBoxes }); // TODO: DEPRECATED
+        Object.assign(_converse.exports, { ChatBoxes });
 
         api.listen.on('addClientFeatures', () => {
             api.disco.own.features.add(Strophe.NS.MESSAGE_CORRECT);
@@ -34,8 +34,13 @@ converse.plugins.add('converse-chatboxes', {
             api.disco.own.features.add(Strophe.NS.OUTOFBAND);
         });
 
+        let chatboxes;
+
         api.listen.on('pluginsInitialized', () => {
-            _converse.chatboxes = new _converse.ChatBoxes();
+            chatboxes = new _converse.exports.ChatBoxes();
+            Object.assign(_converse, { chatboxes }); // TODO: DEPRECATED
+            Object.assign(_converse.state, { chatboxes });
+
             /**
              * Triggered once the _converse.ChatBoxes collection has been initialized.
              * @event _converse#chatBoxesInitialized
@@ -45,7 +50,7 @@ converse.plugins.add('converse-chatboxes', {
             api.trigger('chatBoxesInitialized');
         });
 
-        api.listen.on('presencesInitialized', (reconnecting) => _converse.chatboxes.onConnected(reconnecting));
-        api.listen.on('reconnected', () => _converse.chatboxes.forEach(m => m.onReconnection()));
+        api.listen.on('presencesInitialized', (reconnecting) => chatboxes.onConnected(reconnecting));
+        api.listen.on('reconnected', () => chatboxes.forEach(m => m.onReconnection()));
     }
 });

+ 4 - 0
src/headless/shared/parsers.js

@@ -151,6 +151,10 @@ export function getOpenGraphMetadata (stanza) {
 }
 
 
+/**
+ * @param {string} text
+ * @param {number} offset
+ */
 export function getMediaURLsMetadata (text, offset=0) {
     const objs = [];
     if (!text) {

+ 18 - 1
src/headless/utils/url.js

@@ -2,13 +2,27 @@ import URI from 'urijs';
 import log from '../log.js';
 import api from '../shared/api/index.js';
 
+/**
+ * Will return false if URL is malformed or contains disallowed characters
+ * @param {string} text
+ * @returns {boolean}
+ */
+export function isValidURL (text) {
+    try {
+        return !!(new URL(text));
+    } catch (error) {
+        log.error(error);
+        return false;
+    }
+}
+
 /**
  * Given a url, check whether the protocol being used is allowed for rendering
  * the media in the chat (as opposed to just rendering a URL hyperlink).
  * @param {string} url
  * @returns {boolean}
  */
-export function isAllowedProtocolForMedia(url) {
+export function isAllowedProtocolForMedia (url) {
     const uri = getURI(url);
     const { protocol } = window.location;
     if (['chrome-extension:','file:'].includes(protocol)) {
@@ -20,6 +34,9 @@ export function isAllowedProtocolForMedia(url) {
     );
 }
 
+/**
+ * @param {string|URI} url
+ */
 export function getURI (url) {
     try {
         return url instanceof URI ? url : new URI(url);

+ 16 - 4
src/plugins/chatview/tests/styling.js

@@ -174,10 +174,22 @@ describe("An incoming chat Message", function () {
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         expect(msg_el.innerText).toBe(msg_text);
 
-        await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
-            'Go to ~<a target="_blank" rel="noopener" href="https://conversejs.org~now/">https://conversejs.org~now</a> '+
-            '<span class="styling-directive">_</span><i>please</i><span class="styling-directive">_</span>'
-        );
+        // Chrome < 119 thinks this is not a valid URL while Chrome 119 does.
+        let valid_url = false;
+        try {
+            valid_url = !!(new URL('https://conversejs.org~now'));
+        } catch (e) {
+            valid_url = false;
+        }
+
+        if (valid_url) {
+            await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
+                'Go to ~<a target="_blank" rel="noopener" href="https://conversejs.org~now/">https://conversejs.org~now</a> '+
+                '<span class="styling-directive">_</span><i>please</i><span class="styling-directive">_</span>', 1000);
+        } else {
+            await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
+                'Go to ~https://conversejs.org~now <span class="styling-directive">_</span><i>please</i><span class="styling-directive">_</span>');
+        }
 
         msg_text = `Go to _https://converse_js.org_ _please_`;
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)

+ 2 - 0
src/shared/directives/rich-text.js

@@ -20,6 +20,8 @@ class RichTextRenderer {
         } catch (e) {
             log.error(e);
         }
+        console.warn('text.payload');
+        console.warn(text.payload);
         return text.payload;
     }
 

+ 29 - 4
src/shared/rich-text.js

@@ -5,7 +5,7 @@ import tplVideo from 'templates/video.js';
 import { api } from '@converse/headless';
 import { containsDirectives, getDirectiveAndLength, getDirectiveTemplate, isQuoteDirective } from './styling.js';
 import { getEmojiMarkup } from './chat/utils.js';
-import { getHyperlinkTemplate } from 'utils/html.js';
+import { getHyperlinkTemplate } from '../utils/html.js';
 import { getMediaURLs } from '@converse/headless/shared/chat/utils.js';
 import { getMediaURLsMetadata } from '@converse/headless/shared/parsers.js';
 import {
@@ -59,9 +59,9 @@ export class RichText extends String {
      *  from the start of the original message text. This is necessary because
      *  RichText instances can be nested when templates call directives
      *  which create new RichText instances (as happens with XEP-393 styling directives).
-     * @param { Object } options
-     * @param { String } options.nick - The current user's nickname (only relevant if the message is in a XEP-0045 MUC)
-     * @param { Boolean } options.render_styling - Whether XEP-0393 message styling should be applied to the message
+     * @param { Object } [options]
+     * @param { String } [options.nick] - The current user's nickname (only relevant if the message is in a XEP-0045 MUC)
+     * @param { Boolean } [options.render_styling] - Whether XEP-0393 message styling should be applied to the message
      * @param { Boolean } [options.embed_audio] - Whether audio URLs should be rendered as <audio> elements.
      *  If set to `true`, then audio files will always be rendered with an
      *  audio player. If set to `false`, they won't, and if not defined, then the `embed_audio` setting
@@ -123,11 +123,27 @@ export class RichText extends String {
      *  offset from the start of the original message stanza's body text).
      */
     addHyperlinks (text, local_offset) {
+        console.warn('-------------');
+        console.warn('addHyperlinks');
+        console.warn('-------------');
+
         const full_offset = local_offset + this.offset;
         const urls_meta = this.media_urls || getMediaURLsMetadata(text, local_offset).media_urls || [];
         const media_urls = getMediaURLs(urls_meta, text, full_offset);
 
+        console.warn('text');
+        console.warn(text);
+        console.warn('local_offset');
+        console.warn(local_offset);
+        console.warn('media_urls.length');
+        console.warn(media_urls.length);
+        console.warn(media_urls);
+        console.warn('-------------');
+
         media_urls.filter(o => !o.is_encrypted).forEach(url_obj => {
+            console.warn('url_obj');
+            console.warn(url_obj);
+
             const url_text = url_obj.url;
             const filtered_url = filterQueryParamsFromURL(url_text);
             let template;
@@ -146,6 +162,7 @@ export class RichText extends String {
             } else if (isAudioURL(url_text) && this.shouldRenderMedia(url_text, 'audio')) {
                 template = tplAudio(filtered_url, this.hide_media_urls);
             } else {
+                console.warn('calling getHyperlinkTemplate');
                 template = getHyperlinkTemplate(filtered_url);
             }
             this.addTemplateResult(url_obj.start + local_offset, url_obj.end + local_offset, template);
@@ -299,6 +316,13 @@ export class RichText extends String {
         this.render_styling && this.addStyling();
         this.addAnnotations(this.addMentions);
         this.addAnnotations(this.addHyperlinks);
+
+        console.warn('this.references');
+        console.warn(this.references);
+
+        console.warn('this.marshall()');
+        console.warn(this.marshall().map(item => (isString(item) ? item : item.template)));
+
         this.addAnnotations(this.addMapURLs);
 
         await api.emojis.initialize();
@@ -335,6 +359,7 @@ export class RichText extends String {
      * @param { Object } template - The lit TemplateResult instance
      */
     addTemplateResult (begin, end, template) {
+        console.log(`addTemplateResult called with ${begin}, ${end}, ${template}`);
         this.references.push({ begin, end, template });
     }
 

+ 1 - 1
src/shared/tests/mock.js

@@ -695,7 +695,7 @@ async function _initConverse (settings) {
         'discover_connection_methods': false,
         'enable_smacks': false,
         'i18n': 'en',
-        'loglevel': 'debug',
+        'loglevel': 'warn',
         'no_trimming': true,
         'persistent_store': 'localStorage',
         'play_sounds': false,

+ 92 - 128
src/utils/html.js

@@ -2,6 +2,8 @@
  * @copyright 2022, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  * @description This is the DOM/HTML utilities module.
+ * @typedef {import('lit').TemplateResult} TemplateResult
+ * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder
  */
 import tplAudio from 'templates/audio.js';
 import tplFile from 'templates/file.js';
@@ -17,7 +19,7 @@ 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 { getURI, isAudioURL, isImageURL, isVideoURL } from '@converse/headless/utils/url.js';
+import { getURI, isAudioURL, isImageURL, isVideoURL, isValidURL } from '@converse/headless/utils/url.js';
 import { render } from 'lit';
 import { queryChildren } from '@converse/headless/utils/html.js';
 
@@ -25,13 +27,6 @@ const { sizzle, Strophe } = converse.env;
 
 const APPROVED_URL_PROTOCOLS = ['http', 'https', 'xmpp', 'mailto'];
 
-function getAutoCompleteProperty (name, options) {
-    return {
-        'muc#roomconfig_lang': 'language',
-        'muc#roomconfig_roomsecret': options?.new_password ? 'new-password' : 'current-password'
-    }[name];
-}
-
 const XFORM_TYPE_MAP = {
     'text-private': 'password',
     'text-single': 'text',
@@ -56,6 +51,9 @@ const XFORM_VALIDATE_TYPE_MAP = {
 
 const EMPTY_TEXT_REGEX = /\s*\n\s*/
 
+/**
+ * @param {Element|Strophe.Builder} el
+ */
 function stripEmptyTextNodes (el) {
     el = el.tree?.() ?? el;
 
@@ -73,6 +71,13 @@ function stripEmptyTextNodes (el) {
     return el;
 }
 
+function getAutoCompleteProperty (name, options) {
+    return {
+        'muc#roomconfig_lang': 'language',
+        'muc#roomconfig_roomsecret': options?.new_password ? 'new-password' : 'current-password'
+    }[name];
+}
+
 const serializer = new XMLSerializer();
 
 /**
@@ -118,8 +123,8 @@ function isEqualNode (actual, expected) {
 
 /**
  * Given an HTMLElement representing a form field, return it's name and value.
- * @param { HTMLElement } field
- * @returns { { string, string } | null }
+ * @param {HTMLInputElement|HTMLSelectElement} field
+ * @returns {{[key:string]:string|number|string[]}|null}
  */
 export function getNameAndValue(field) {
     const name = field.getAttribute('name');
@@ -128,18 +133,17 @@ export function getNameAndValue(field) {
     }
     let value;
     if (field.getAttribute('type') === 'checkbox') {
-        value = field.checked && 1 || 0;
+        value = /** @type {HTMLInputElement} */(field).checked && 1 || 0;
     } else if (field.tagName == "TEXTAREA") {
         value = field.value.split('\n').filter(s => s.trim());
     } else if (field.tagName == "SELECT") {
-        value = u.getSelectValues(field);
+        value = u.getSelectValues(/** @type {HTMLSelectElement} */(field));
     } else {
         value = field.value;
     }
     return { name, value };
 }
 
-
 function getInputType(field) {
     const type = XFORM_TYPE_MAP[field.getAttribute('type')]
     if (type == 'text') {
@@ -174,8 +178,8 @@ export function getFileName (url) {
  * Returns the markup for a URL that points to a downloadable asset
  * (such as a video, image or audio file).
  * @method u#getOOBURLMarkup
- * @param { String } url
- * @returns { TemplateResult }
+ * @param {string} url
+ * @returns {TemplateResult|string}
  */
 export function getOOBURLMarkup (url) {
     const uri = getURI(url);
@@ -197,48 +201,25 @@ export function getOOBURLMarkup (url) {
  * Return the height of the passed in DOM element,
  * based on the heights of its children.
  * @method u#calculateElementHeight
- * @param { HTMLElement } el
+ * @param {HTMLElement} el
  * @returns {number}
  */
-u.calculateElementHeight = function (el) {
-    return Array.from(el.children).reduce((result, child) => result + child.offsetHeight, 0);
-};
+function calculateElementHeight (el) {
+    return Array.from(el.children).reduce((result, child) => {
+        if (child instanceof HTMLElement) {
+            return result + child.offsetHeight;
+        }
+        return result;
+    }, 0);
+}
 
-u.getNextElement = function (el, selector = '*') {
+function getNextElement (el, selector = '*') {
     let next_el = el.nextElementSibling;
     while (next_el !== null && !sizzle.matchesSelector(next_el, selector)) {
         next_el = next_el.nextElementSibling;
     }
     return next_el;
-};
-
-u.getPreviousElement = function (el, selector = '*') {
-    let prev_el = el.previousElementSibling;
-    while (prev_el !== null && !sizzle.matchesSelector(prev_el, selector)) {
-        prev_el = prev_el.previousElementSibling;
-    }
-    return prev_el;
-};
-
-u.getFirstChildElement = function (el, selector = '*') {
-    let first_el = el.firstElementChild;
-    while (first_el !== null && !sizzle.matchesSelector(first_el, selector)) {
-        first_el = first_el.nextElementSibling;
-    }
-    return first_el;
-};
-
-u.getLastChildElement = function (el, selector = '*') {
-    let last_el = el.lastElementChild;
-    while (last_el !== null && !sizzle.matchesSelector(last_el, selector)) {
-        last_el = last_el.previousElementSibling;
-    }
-    return last_el;
-};
-
-u.toggleClass = function (className, el) {
-    u.hasClass(className, el) ? removeClass(className, el) : addClass(className, el);
-};
+}
 
 /**
  * Has an element a class?
@@ -282,21 +263,30 @@ export function removeElement (el) {
     return el;
 }
 
-u.getElementFromTemplateResult = function (tr) {
+/**
+ * @param {TemplateResult} tr
+ */
+function getElementFromTemplateResult (tr) {
     const div = document.createElement('div');
     render(tr, div);
     return div.firstElementChild;
-};
+}
 
-u.showElement = el => {
+/**
+ * @param {Element} el
+ */
+function showElement (el) {
     removeClass('collapsed', el);
     removeClass('hidden', el);
-};
+}
 
-u.hideElement = function (el) {
+/**
+ * @param {Element} el
+ */
+function hideElement (el) {
     el instanceof Element && el.classList.add('hidden');
     return el;
-};
+}
 
 export function ancestor (el, selector) {
     let parent = el;
@@ -308,12 +298,11 @@ export function ancestor (el, selector) {
 
 /**
  * Return the element's siblings until one matches the selector.
- * @private
  * @method u#nextUntil
  * @param { HTMLElement } el
  * @param { String } selector
  */
-u.nextUntil = function (el, selector) {
+function nextUntil (el, selector) {
     const matches = [];
     let sibling_el = el.nextElementSibling;
     while (sibling_el !== null && !sibling_el.matches(selector)) {
@@ -321,64 +310,48 @@ u.nextUntil = function (el, selector) {
         sibling_el = sibling_el.nextElementSibling;
     }
     return matches;
-};
+}
 
 /**
  * Helper method that replace HTML-escaped symbols with equivalent characters
  * (e.g. transform occurrences of '&amp;' to '&')
- * @private
  * @method u#unescapeHTML
  * @param { String } string - a String containing the HTML-escaped symbols.
  */
-u.unescapeHTML = function (string) {
+function unescapeHTML (string) {
     var div = document.createElement('div');
     div.innerHTML = string;
     return div.innerText;
-};
+}
 
-u.escapeHTML = function (string) {
+/**
+ * @method u#escapeHTML
+ * @param {string} string
+ */
+function escapeHTML (string) {
     return string
         .replace(/&/g, '&amp;')
         .replace(/</g, '&lt;')
         .replace(/>/g, '&gt;')
         .replace(/"/g, '&quot;');
-};
+}
 
 function isProtocolApproved (protocol, safeProtocolsList = APPROVED_URL_PROTOCOLS) {
     return !!safeProtocolsList.includes(protocol);
 }
 
-// Will return false if URL is malformed or contains disallowed characters
-function isUrlValid (urlString) {
-    try {
-        const url = new URL(urlString);
-        return !!url;
-    } catch (error) {
-        return false;
-    }
-}
-
+/**
+ * @param {string} url
+ */
 export function getHyperlinkTemplate (url) {
     const http_url = RegExp('^w{3}.', 'ig').test(url) ? `http://${url}` : url;
     const uri = getURI(url);
-    if (uri !== null && isUrlValid(http_url) && (isProtocolApproved(uri._parts.protocol) || !uri._parts.protocol)) {
+    if (uri !== null && isValidURL(http_url) && (isProtocolApproved(uri._parts.protocol) || !uri._parts.protocol)) {
         return tplHyperlink(uri, url);
     }
     return url;
 }
 
-u.slideInAllElements = function (elements, duration = 300) {
-    return Promise.all(Array.from(elements).map(e => u.slideIn(e, duration)));
-};
-
-u.slideToggleElement = function (el, duration) {
-    if (u.hasClass('collapsed', el) || u.hasClass('hidden', el)) {
-        return u.slideOut(el, duration);
-    } else {
-        return u.slideIn(el, duration);
-    }
-};
-
 /**
  * Shows/expands an element by sliding it out of itself
  * @method slideOut
@@ -394,11 +367,11 @@ export function slideOut (el, duration = 200) {
             return;
         }
         const marker = el.getAttribute('data-slider-marker');
-        if (marker) {
+        if (marker && !Number.isNaN(Number(marker))) {
             el.removeAttribute('data-slider-marker');
-            cancelAnimationFrame(marker);
+            cancelAnimationFrame(Number(marker));
         }
-        const end_height = u.calculateElementHeight(el);
+        const end_height = calculateElementHeight(el);
         if (window.converse_disable_effects) {
             // Effects are disabled (for tests)
             el.style.height = end_height + 'px';
@@ -406,7 +379,7 @@ export function slideOut (el, duration = 200) {
             resolve();
             return;
         }
-        if (!u.hasClass('collapsed', el) && !u.hasClass('hidden', el)) {
+        if (!hasClass('collapsed', el) && !hasClass('hidden', el)) {
             resolve();
             return;
         }
@@ -424,7 +397,7 @@ export function slideOut (el, duration = 200) {
                 // browser bug where browsers don't know the correct
                 // offsetHeight beforehand.
                 el.removeAttribute('data-slider-marker');
-                el.style.height = u.calculateElementHeight(el) + 'px';
+                el.style.height = calculateElementHeight(el) + 'px';
                 el.style.overflow = '';
                 el.style.height = '';
                 resolve();
@@ -440,9 +413,8 @@ export function slideOut (el, duration = 200) {
 
 /**
  * Hides/contracts an element by sliding it into itself
- * @method slideIn
- * @param { HTMLElement } el - The HTML string
- * @param { Number } duration - The duration amount in milliseconds
+ * @param {HTMLElement} el - The HTML string
+ * @param {Number} duration - The duration amount in milliseconds
  */
 export function slideIn (el, duration = 200) {
     return new Promise((resolve, reject) => {
@@ -450,7 +422,7 @@ export function slideIn (el, duration = 200) {
             const err = 'An element needs to be passed in to slideIn';
             log.warn(err);
             return reject(new Error(err));
-        } else if (u.hasClass('collapsed', el)) {
+        } else if (hasClass('collapsed', el)) {
             return resolve(el);
         } else if (window.converse_disable_effects) {
             // Effects are disabled (for tests)
@@ -459,9 +431,9 @@ export function slideIn (el, duration = 200) {
             return resolve(el);
         }
         const marker = el.getAttribute('data-slider-marker');
-        if (marker) {
+        if (marker && !Number.isNaN(Number(marker))) {
             el.removeAttribute('data-slider-marker');
-            cancelAnimationFrame(marker);
+            cancelAnimationFrame(Number(marker));
         }
         const original_height = el.offsetHeight,
             steps = duration / 17; // We assume 17ms per animation which is ~60FPS
@@ -485,44 +457,26 @@ export function slideIn (el, duration = 200) {
     });
 }
 
-function afterAnimationEnds (el, callback) {
-    el.classList.remove('visible');
-    callback?.();
-}
-
-u.isInDOM = function (el) {
+/**
+ * @param {HTMLElement} el
+ */
+function isInDOM (el) {
     return document.querySelector('body').contains(el);
-};
+}
 
-u.isVisible = function (el) {
+/**
+ * @param {HTMLElement} el
+ */
+function isVisible (el) {
     if (el === null) {
         return false;
     }
-    if (u.hasClass('hidden', el)) {
+    if (hasClass('hidden', el)) {
         return false;
     }
     // XXX: Taken from jQuery's "visible" implementation
     return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
-};
-
-u.fadeIn = function (el, callback) {
-    if (!el) {
-        log.warn('An element needs to be passed in to fadeIn');
-    }
-    if (window.converse_disable_effects) {
-        el.classList.remove('hidden');
-        return afterAnimationEnds(el, callback);
-    }
-    if (u.hasClass('hidden', el)) {
-        el.classList.add('visible');
-        el.classList.remove('hidden');
-        el.addEventListener('webkitAnimationEnd', () => afterAnimationEnds(el, callback));
-        el.addEventListener('animationend', () => afterAnimationEnds(el, callback));
-        el.addEventListener('oanimationend', () => afterAnimationEnds(el, callback));
-    } else {
-        afterAnimationEnds(el, callback);
-    }
-};
+}
 
 /**
  * Takes an XML field in XMPP XForm (XEP-004: Data Forms) format returns a
@@ -531,12 +485,12 @@ u.fadeIn = function (el, callback) {
  * @param {HTMLElement} field - the field to convert
  * @param {Element} stanza - the containing stanza
  * @param {Object} options
- * @returns {import('lit').TemplateResult}
+ * @returns {TemplateResult}
  */
 export function xForm2TemplateResult (field, stanza, options={}) {
     if (field.getAttribute('type') === 'list-single' || field.getAttribute('type') === 'list-multi') {
         const values = queryChildren(field, 'value').map(el => el?.textContent);
-        const options = queryChildren(field, 'option').map(/** @type {HTMLElement} */(option) => {
+        const options = queryChildren(field, 'option').map((/** @type {HTMLElement} */option) => {
             const value = option.querySelector('value')?.textContent;
             return {
                 'value': value,
@@ -623,13 +577,23 @@ export function xForm2TemplateResult (field, stanza, options={}) {
 Object.assign(u, {
     addClass,
     ancestor,
+    calculateElementHeight,
+    escapeHTML,
+    getElementFromTemplateResult,
+    getNextElement,
     getOOBURLMarkup,
     hasClass,
+    hideElement,
     isEqualNode,
+    isInDOM,
+    isVisible,
+    nextUntil,
     removeClass,
     removeElement,
+    showElement,
     slideIn,
     slideOut,
+    unescapeHTML,
     xForm2TemplateResult,
 });