Procházet zdrojové kódy

Avoid having two headless instances included in the build

Refactored so that all imports from `@converse/headless` are from the
index and not deeper files.

Updates #3348
JC Brand před 1 rokem
rodič
revize
df24c6910d
100 změnil soubory, kde provedl 14224 přidání a 1106 odebrání
  1. 1 0
      CHANGES.md
  2. 1 1
      Makefile
  3. 3 3
      package-lock.json
  4. 13 8
      src/headless/index.js
  5. 1 1
      src/headless/package.json
  6. 8 2
      src/headless/plugins/adhoc/utils.js
  7. 18 5
      src/headless/plugins/bookmarks/collection.js
  8. 2 81
      src/headless/plugins/bookmarks/index.js
  9. 82 0
      src/headless/plugins/bookmarks/plugin.js
  10. 2 13
      src/headless/plugins/bookmarks/utils.js
  11. 1 1
      src/headless/plugins/chat/model-with-contact.js
  12. 3 6
      src/headless/plugins/chat/model.js
  13. 1 2
      src/headless/plugins/chat/parsers.js
  14. 2 1
      src/headless/plugins/chat/plugin.js
  15. 8 4
      src/headless/plugins/chatboxes/api.js
  16. 2 94
      src/headless/plugins/emoji/index.js
  17. 96 0
      src/headless/plugins/emoji/plugin.js
  18. 5 64
      src/headless/plugins/mam/index.js
  19. 69 0
      src/headless/plugins/mam/plugin.js
  20. 2 2
      src/headless/plugins/mam/utils.js
  21. 14 1
      src/headless/plugins/muc/affiliations/api.js
  22. 3 22
      src/headless/plugins/muc/affiliations/utils.js
  23. 1 2
      src/headless/plugins/muc/api.js
  24. 5 0
      src/headless/plugins/muc/index.js
  25. 2 2
      src/headless/plugins/muc/muc.js
  26. 37 0
      src/headless/plugins/muc/occupant.js
  27. 8 2
      src/headless/plugins/muc/occupants.js
  28. 1 2
      src/headless/plugins/muc/parsers.js
  29. 1 2
      src/headless/plugins/muc/plugin.js
  30. 4 28
      src/headless/plugins/muc/utils.js
  31. 2 1
      src/headless/plugins/roster/index.js
  32. 1 86
      src/headless/plugins/roster/utils.js
  33. 0 35
      src/headless/shared/chat/utils.js
  34. 0 77
      src/headless/shared/parsers.js
  35. 1 1
      src/headless/shared/settings/utils.js
  36. 7 1
      src/headless/types/index.d.ts
  37. 9 2
      src/headless/types/plugins/adhoc/utils.d.ts
  38. 2 1
      src/headless/types/plugins/bookmarks/collection.d.ts
  39. 3 1
      src/headless/types/plugins/bookmarks/index.d.ts
  40. 2 0
      src/headless/types/plugins/bookmarks/plugin.d.ts
  41. 0 1
      src/headless/types/plugins/bookmarks/utils.d.ts
  42. 0 2
      src/headless/types/plugins/chat/model.d.ts
  43. 8 5
      src/headless/types/plugins/chatboxes/api.d.ts
  44. 2 1
      src/headless/types/plugins/emoji/index.d.ts
  45. 2 0
      src/headless/types/plugins/emoji/plugin.d.ts
  46. 2 1
      src/headless/types/plugins/mam/index.d.ts
  47. 2 0
      src/headless/types/plugins/mam/plugin.d.ts
  48. 10 0
      src/headless/types/plugins/muc/affiliations/api.d.ts
  49. 3 9
      src/headless/types/plugins/muc/affiliations/utils.d.ts
  50. 12 0
      src/headless/types/plugins/muc/occupant.d.ts
  51. 1 0
      src/headless/types/plugins/muc/occupants.d.ts
  52. 0 8
      src/headless/types/plugins/muc/utils.d.ts
  53. 2 1
      src/headless/types/plugins/roster/index.d.ts
  54. 0 11
      src/headless/types/plugins/roster/utils.d.ts
  55. 0 9
      src/headless/types/shared/chat/utils.d.ts
  56. 0 24
      src/headless/types/shared/parsers.d.ts
  57. 2 2
      src/headless/types/utils/form.d.ts
  58. 6 6
      src/headless/types/utils/html.d.ts
  59. 72 69
      src/headless/types/utils/index.d.ts
  60. 21 3
      src/headless/types/utils/stanza.d.ts
  61. 48 0
      src/headless/types/utils/url.d.ts
  62. 1 1
      src/headless/utils/form.js
  63. 5 5
      src/headless/utils/html.js
  64. 28 74
      src/headless/utils/index.js
  65. 24 0
      src/headless/utils/stanza.js
  66. 85 0
      src/headless/utils/url.js
  67. 13355 199
      src/i18n/converse.pot
  68. 2 3
      src/i18n/index.js
  69. 4 3
      src/plugins/bookmark-views/components/bookmarks-list.js
  70. 1 2
      src/plugins/bookmark-views/index.js
  71. 3 4
      src/plugins/bookmark-views/utils.js
  72. 2 4
      src/plugins/chatboxviews/index.js
  73. 2 2
      src/plugins/chatboxviews/templates/chats.js
  74. 4 4
      src/plugins/chatboxviews/view.js
  75. 3 2
      src/plugins/chatview/chat.js
  76. 6 8
      src/plugins/chatview/message-form.js
  77. 4 4
      src/plugins/chatview/templates/chat-head.js
  78. 3 2
      src/plugins/chatview/templates/chat.js
  79. 2 3
      src/plugins/controlbox/controlbox.js
  80. 3 2
      src/plugins/controlbox/index.js
  81. 2 2
      src/plugins/controlbox/loginform.js
  82. 3 2
      src/plugins/controlbox/model.js
  83. 2 2
      src/plugins/controlbox/templates/controlbox.js
  84. 4 3
      src/plugins/controlbox/templates/loginform.js
  85. 2 3
      src/plugins/fullscreen/index.js
  86. 4 3
      src/plugins/headlines-view/feed-list.js
  87. 3 1
      src/plugins/headlines-view/templates/feeds-list.js
  88. 2 3
      src/plugins/mam-views/placeholder.js
  89. 5 6
      src/plugins/mam-views/utils.js
  90. 4 3
      src/plugins/minimize/index.js
  91. 2 2
      src/plugins/minimize/utils.js
  92. 3 4
      src/plugins/minimize/view.js
  93. 2 3
      src/plugins/muc-views/affiliation-form.js
  94. 2 2
      src/plugins/muc-views/index.js
  95. 9 11
      src/plugins/muc-views/modals/muc-list.js
  96. 10 14
      src/plugins/muc-views/modtools.js
  97. 3 4
      src/plugins/muc-views/role-form.js
  98. 4 6
      src/plugins/muc-views/sidebar.js
  99. 1 2
      src/plugins/muc-views/templates/affiliation-form.js
  100. 4 2
      src/plugins/muc-views/templates/muc-chatarea.js

+ 1 - 0
CHANGES.md

@@ -29,6 +29,7 @@
 - The `windowStateChanged` event has been removed. If you used it, rely on the
   `visibilitychange` event on `document` instead.
 - `api.modal.create` no longer takes a class, instead it takes the name of a custom DOM element.
+- `getAssignableRoles` and `getAssignableAffiliations` are no longer on the `_converse` object, but on the Occupant instance.
 
 ## 10.1.6 (2023-08-31)
 

+ 1 - 1
Makefile

@@ -202,7 +202,7 @@ src/headless/dist/converse-headless.js: src webpack/webpack.common.js node_modul
 src/headless/dist/converse-headless.min.js: src webpack/webpack.common.js node_modules @converse/headless
 	npm run headless
 
-dist:: node_modules src/* | dist/website.css dist/website.min.css
+dist:: node_modules src/**/* | dist/website.css dist/website.min.css
 	npm run headless
 	# Ideally this should just be `npm run build`.
 	# The additional steps are necessary to properly generate JSON chunk files

+ 3 - 3
package-lock.json

@@ -9288,8 +9288,8 @@
     },
     "node_modules/strophe.js": {
       "version": "2.0.0",
-      "resolved": "git+ssh://git@github.com/strophe/strophejs.git#d6cb60012fd243a1c3f1033e8acd6c80264d65cf",
-      "integrity": "sha512-sS6LSjZRgagAaa42WMPppxhXEyj/KaY1xoIY5FBz/R4yGq06YjrLfsKGFkBU+FGOe5uG/hbNZCxKrbPzp+gDNg==",
+      "resolved": "git+ssh://git@github.com/strophe/strophejs.git#57ff5e731d7e83a4025492b2973b3d1524239f46",
+      "integrity": "sha512-xEzssSc46ejyWt+J+fhIXeIvgvexC8OqwhVnwfCRoZOr1Rp3lRQrGBgtPsbyp22y8/VcKcuz/uGgZNxCmyTTeQ==",
       "license": "MIT",
       "dependencies": {
         "abab": "^2.0.3"
@@ -10383,7 +10383,7 @@
         "pluggable.js": "3.0.1",
         "sizzle": "^2.3.5",
         "sprintf-js": "^1.1.2",
-        "strophe.js": "strophe/strophejs#d6cb60012fd243a1c3f1033e8acd6c80264d65cf",
+        "strophe.js": "strophe/strophejs#57ff5e731d7e83a4025492b2973b3d1524239f46",
         "urijs": "^1.19.10"
       },
       "devDependencies": {}

+ 13 - 8
src/headless/index.js

@@ -1,20 +1,22 @@
 import dayjs from 'dayjs';
 import advancedFormat from 'dayjs/plugin/advancedFormat';
 
-import './shared/constants.js';
+dayjs.extend(advancedFormat);
 
+import * as shared_constants from './shared/constants.js';
 import api from './shared/api/index.js';
 import u from './utils/index.js';
 import _converse from './shared/_converse';
 import i18n from './shared/i18n';
 import converse from './shared/api/public.js';
-
-dayjs.extend(advancedFormat);
+import log from './log.js';
 
 // START: Removable components
 // ---------------------------
 // The following components may be removed if they're not needed.
-import './plugins/bookmarks/index.js'; // XEP-0199 XMPP Ping
+
+export { EmojiPicker } from './plugins/emoji/index.js';
+export { Bookmark, Bookmarks } from './plugins/bookmarks/index.js'; // XEP-0199 XMPP Ping
 import './plugins/bosh/index.js'; // XEP-0206 BOSH
 import './plugins/caps/index.js'; // XEP-0115 Entity Capabilities
 export { ChatBox, Message, Messages } from './plugins/chat/index.js'; // RFC-6121 Instant messaging
@@ -22,7 +24,9 @@ import './plugins/chatboxes/index.js';
 import './plugins/disco/index.js'; // XEP-0030 Service discovery
 import './plugins/adhoc/index.js'; // XEP-0050 Ad Hoc Commands
 import './plugins/headlines/index.js'; // Support for headline messages
-import './plugins/mam/index.js'; // XEP-0313 Message Archive Management
+
+// XEP-0313 Message Archive Management
+export { MAMPlaceholderMessage } from './plugins/mam/index.js';
 
 // XEP-0045 Multi-user chat
 export { MUCMessage, MUCMessages, MUC, MUCOccupant, MUCOccupants } from './plugins/muc/index.js';
@@ -31,7 +35,7 @@ import './plugins/ping/index.js'; // XEP-0199 XMPP Ping
 import './plugins/pubsub.js'; // XEP-0060 Pubsub
 
 // RFC-6121 Contacts Roster
-export { RosterContact, RosterContacts, Presence, Presences } from './plugins/roster/index.js';
+export { RosterContact, RosterContacts, RosterFilter, Presence, Presences } from './plugins/roster/index.js';
 
 import './plugins/smacks/index.js'; // XEP-0198 Stream Management
 export { XMPPStatus } from './plugins/status/index.js';
@@ -39,8 +43,9 @@ export { VCard, VCards } from './plugins/vcard/index.js'; // XEP-0054 VCard-temp
 // ---------------------------
 // END: Removable components
 
+import * as muc_constants from './plugins/muc/constants.js';
+const constants = Object.assign({}, shared_constants, muc_constants);
 
-import log from './log.js';
-export { api, converse, _converse, i18n, log, u };
+export { api, converse, _converse, i18n, log, u, constants };
 
 export default converse;

+ 1 - 1
src/headless/package.json

@@ -43,7 +43,7 @@
     "pluggable.js": "3.0.1",
     "sizzle": "^2.3.5",
     "sprintf-js": "^1.1.2",
-    "strophe.js": "strophe/strophejs#d6cb60012fd243a1c3f1033e8acd6c80264d65cf",
+    "strophe.js": "strophe/strophejs#57ff5e731d7e83a4025492b2973b3d1524239f46",
     "urijs": "^1.19.10"
   },
   "devDependencies": {}

+ 8 - 2
src/headless/plugins/adhoc/utils.js

@@ -1,14 +1,20 @@
 import sizzle from 'sizzle';
 import converse from '../../shared/api/public.js';
-import { getAttributes } from '../../shared/parsers';
 
 const { Strophe, u } = converse.env;
 
+/**
+ * @param {Element} stanza
+ */
 export function parseForCommands (stanza) {
     const items = sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"][node="${Strophe.NS.ADHOC}"] item`, stanza);
-    return items.map(getAttributes)
+    return items.map(u.getAttributes)
 }
 
+/**
+ * @param {Element} iq
+ * @param {string} [jid]
+ */
 export function getCommandFields (iq, jid) {
     const cmd_el = sizzle(`command[xmlns="${Strophe.NS.ADHOC}"]`, iq).pop();
     const data = {

+ 18 - 5
src/headless/plugins/bookmarks/collection.js

@@ -13,11 +13,6 @@ const { Strophe, $iq, sizzle } = converse.env;
 
 class Bookmarks extends Collection {
 
-    constructor () {
-        super([], { comparator: (/** @type {Bookmark} */b) => b.get('name').toLowerCase() });
-        this.model = Bookmark;
-    }
-
     async initialize () {
         this.on('add', bm => this.openBookmarkedRoom(bm)
             .then(bm => this.markRoomAsBookmarked(bm))
@@ -44,6 +39,24 @@ class Bookmarks extends Collection {
         api.trigger('bookmarksInitialized', this);
     }
 
+    static async checkBookmarksSupport () {
+        const bare_jid = _converse.session.get('bare_jid');
+        if (!bare_jid) return false;
+
+        const identity = await api.disco.getIdentity('pubsub', 'pep', bare_jid);
+        if (api.settings.get('allow_public_bookmarks')) {
+            return !!identity;
+        } else {
+            return api.disco.supports(Strophe.NS.PUBSUB + '#publish-options', bare_jid);
+        }
+    }
+
+    constructor () {
+        super([], { comparator: (/** @type {Bookmark} */b) => b.get('name').toLowerCase() });
+        this.model = Bookmark;
+    }
+
+
     /**
      * @param {Bookmark} bookmark
      */

+ 2 - 81
src/headless/plugins/bookmarks/index.js

@@ -1,84 +1,5 @@
-/**
- * @description
- * Converse.js plugin which adds views for bookmarks specified in XEP-0048.
- * @copyright 2022, the Converse.js contributors
- * @license Mozilla Public License (MPLv2)
- */
-import "../..//plugins/muc/index.js";
 import Bookmark from './model.js';
 import Bookmarks from './collection.js';
-import _converse from '../../shared/_converse.js';
-import api from '../../shared/api/index.js';
-import converse from '../../shared/api/public.js';
-import { initBookmarks, getNicknameFromBookmark, handleBookmarksPush } from './utils.js';
+import './plugin.js';
 
-const { Strophe } = converse.env;
-
-Strophe.addNamespace('BOOKMARKS', 'storage:bookmarks');
-
-
-converse.plugins.add('converse-bookmarks', {
-
-    dependencies: ["converse-chatboxes", "converse-muc"],
-
-    overrides: {
-        // Overrides mentioned here will be picked up by converse.js's
-        // plugin architecture they will replace existing methods on the
-        // relevant objects or classes.
-        // New functions which don't exist yet can also be added.
-
-        ChatRoom: {
-            getDisplayName () {
-                const { _converse, getDisplayName } = this.__super__;
-                const bookmark = this.get('bookmarked') ? _converse.bookmarks?.get(this.get('jid')) : null;
-                return bookmark?.get('name') || getDisplayName.apply(this, arguments);
-            },
-
-            getAndPersistNickname (nick) {
-                nick = nick || getNicknameFromBookmark(this.get('jid'));
-                return this.__super__.getAndPersistNickname.call(this, nick);
-            }
-        }
-    },
-
-    initialize () {
-        // Configuration values for this plugin
-        // ====================================
-        // Refer to docs/source/configuration.rst for explanations of these
-        // configuration settings.
-        api.settings.extend({
-            allow_bookmarks: true,
-            allow_public_bookmarks: false,
-            muc_respect_autojoin: true
-        });
-
-        api.promises.add('bookmarksInitialized');
-
-        const exports  = { Bookmark, Bookmarks };
-        Object.assign(_converse, exports); // TODO: DEPRECATED
-        Object.assign(_converse.exports, exports);
-
-        api.listen.on('addClientFeatures', () => {
-            if (api.settings.get('allow_bookmarks')) {
-                api.disco.own.features.add(Strophe.NS.BOOKMARKS + '+notify')
-            }
-        })
-
-        api.listen.on('clearSession', () => {
-            const { state } = _converse;
-            if (state.bookmarks) {
-                state.bookmarks.clearStore({'silent': true});
-                window.sessionStorage.removeItem(state.bookmarks.fetched_flag);
-                delete state.bookmarks;
-            }
-        });
-
-        api.listen.on('connected', async () =>  {
-            // Add a handler for bookmarks pushed from other connected clients
-            const bare_jid = _converse.session.get('bare_jid');
-            api.connection.get().addHandler(handleBookmarksPush, null, 'message', 'headline', null, bare_jid);
-            await Promise.all([api.waitUntil('chatBoxesFetched')]);
-            initBookmarks();
-        });
-    }
-});
+export { Bookmark, Bookmarks };

+ 82 - 0
src/headless/plugins/bookmarks/plugin.js

@@ -0,0 +1,82 @@
+/**
+ * @copyright 2022, the Converse.js contributors
+ * @license Mozilla Public License (MPLv2)
+ */
+import "../../plugins/muc/index.js";
+import Bookmark from './model.js';
+import Bookmarks from './collection.js';
+import _converse from '../../shared/_converse.js';
+import api from '../../shared/api/index.js';
+import converse from '../../shared/api/public.js';
+import { initBookmarks, getNicknameFromBookmark, handleBookmarksPush } from './utils.js';
+
+const { Strophe } = converse.env;
+
+Strophe.addNamespace('BOOKMARKS', 'storage:bookmarks');
+
+
+converse.plugins.add('converse-bookmarks', {
+
+    dependencies: ["converse-chatboxes", "converse-muc"],
+
+    overrides: {
+        // Overrides mentioned here will be picked up by converse.js's
+        // plugin architecture they will replace existing methods on the
+        // relevant objects or classes.
+        // New functions which don't exist yet can also be added.
+
+        ChatRoom: {
+            getDisplayName () {
+                const { _converse, getDisplayName } = this.__super__;
+                const bookmark = this.get('bookmarked') ? _converse.bookmarks?.get(this.get('jid')) : null;
+                return bookmark?.get('name') || getDisplayName.apply(this, arguments);
+            },
+
+            getAndPersistNickname (nick) {
+                nick = nick || getNicknameFromBookmark(this.get('jid'));
+                return this.__super__.getAndPersistNickname.call(this, nick);
+            }
+        }
+    },
+
+    initialize () {
+        // Configuration values for this plugin
+        // ====================================
+        // Refer to docs/source/configuration.rst for explanations of these
+        // configuration settings.
+        api.settings.extend({
+            allow_bookmarks: true,
+            allow_public_bookmarks: false,
+            muc_respect_autojoin: true
+        });
+
+        api.promises.add('bookmarksInitialized');
+
+        const exports  = { Bookmark, Bookmarks };
+        Object.assign(_converse, exports); // TODO: DEPRECATED
+        Object.assign(_converse.exports, exports);
+
+        api.listen.on('addClientFeatures', () => {
+            if (api.settings.get('allow_bookmarks')) {
+                api.disco.own.features.add(Strophe.NS.BOOKMARKS + '+notify')
+            }
+        })
+
+        api.listen.on('clearSession', () => {
+            const { state } = _converse;
+            if (state.bookmarks) {
+                state.bookmarks.clearStore({'silent': true});
+                window.sessionStorage.removeItem(state.bookmarks.fetched_flag);
+                delete state.bookmarks;
+            }
+        });
+
+        api.listen.on('connected', async () =>  {
+            // Add a handler for bookmarks pushed from other connected clients
+            const bare_jid = _converse.session.get('bare_jid');
+            api.connection.get().addHandler(handleBookmarksPush, null, 'message', 'headline', null, bare_jid);
+            await Promise.all([api.waitUntil('chatBoxesFetched')]);
+            initBookmarks();
+        });
+    }
+});

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

@@ -2,26 +2,15 @@ import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
 import log from "../../log.js";
+import Bookmarks from './collection.js';
 
 const { Strophe, sizzle } = converse.env;
 
-export async function checkBookmarksSupport () {
-    const bare_jid = _converse.session.get('bare_jid');
-    if (!bare_jid) return false;
-
-    const identity = await api.disco.getIdentity('pubsub', 'pep', bare_jid);
-    if (api.settings.get('allow_public_bookmarks')) {
-        return !!identity;
-    } else {
-        return api.disco.supports(Strophe.NS.PUBSUB + '#publish-options', bare_jid);
-    }
-}
-
 export async function initBookmarks () {
     if (!api.settings.get('allow_bookmarks')) {
         return;
     }
-    if (await checkBookmarksSupport()) {
+    if (await Bookmarks.checkBookmarksSupport()) {
         _converse.state.bookmarks = new _converse.exports.Bookmarks();
         Object.assign(_converse, { bookmarks: _converse.state.bookmarks }); // TODO: DEPRECATED
     }

+ 1 - 1
src/headless/plugins/chat/model-with-contact.js

@@ -1,6 +1,6 @@
-import api from "../../shared/api/index.js";
 import { Model } from '@converse/skeletor';
 import { getOpenPromise } from '@converse/openpromise';
+import api from "../../shared/api/index.js";
 
 class ModelWithContact extends Model {
     /**

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

@@ -5,20 +5,19 @@
  * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes
  * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder
  */
+import isMatch from "lodash-es/isMatch";
 import pick from "lodash-es/pick";
+import { getOpenPromise } from '@converse/openpromise';
 import { Model } from '@converse/skeletor';
 import { ACTIVE, PRIVATE_CHAT_TYPE, COMPOSING, INACTIVE, PAUSED, SUCCESS, GONE } from '../../shared/constants.js';
 import ModelWithContact from './model-with-contact.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
-import isMatch from "lodash-es/isMatch";
 import log from '../../log.js';
 import { TimeoutError } from '../../shared/errors.js';
 import { debouncedPruneHistory, handleCorrection } from '../../shared/chat/utils.js';
 import { filesize } from "filesize";
-import { getMediaURLsMetadata } from '../../shared/parsers.js';
-import { getOpenPromise } from '@converse/openpromise';
 import { initStorage } from '../../utils/storage.js';
 import { isEmptyMessage } from '../../utils/index.js';
 import { isNewMessage } from './utils.js';
@@ -31,8 +30,6 @@ const { Strophe, $msg, u } = converse.env;
 
 /**
  * Represents an open/ongoing chat conversation.
- * @namespace ChatBox
- * @memberOf _converse
  */
 class ChatBox extends ModelWithContact {
 
@@ -866,7 +863,7 @@ class ChatBox extends ModelWithContact {
             body,
             is_spoiler,
             origin_id
-        }, getMediaURLsMetadata(text));
+        }, u.getMediaURLsMetadata(text));
 
         /**
          * *Hook* which allows plugins to update the attributes of an outgoing message.

+ 1 - 2
src/headless/plugins/chat/parsers.js

@@ -17,7 +17,6 @@ import {
     getCorrectionAttributes,
     getEncryptionAttributes,
     getErrorAttributes,
-    getMediaURLsMetadata,
     getOutOfBandAttributes,
     getReceiptId,
     getReferences,
@@ -225,5 +224,5 @@ export async function parseMessage (stanza) {
     // We call this after the hook, to allow plugins (like omemo) to decrypt encrypted
     // messages, since we need to parse the message text to determine whether
     // there are media urls.
-    return Object.assign(attrs, getMediaURLsMetadata(attrs.is_encrypted ? attrs.plaintext : attrs.body));
+    return Object.assign(attrs, u.getMediaURLsMetadata(attrs.is_encrypted ? attrs.plaintext : attrs.body));
 }

+ 2 - 1
src/headless/plugins/chat/plugin.js

@@ -4,6 +4,7 @@ import Messages from './messages.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
+import chatboxes_api from '../chatboxes/api.js';
 import chat_api from './api.js';
 import { PRIVATE_CHAT_TYPE } from '../../shared/constants.js';
 import {
@@ -43,7 +44,7 @@ converse.plugins.add('converse-chat', {
 
         Object.assign(api, chat_api);
 
-        api.chatboxes.registry.add(PRIVATE_CHAT_TYPE, ChatBox);
+        chatboxes_api.registry.add(PRIVATE_CHAT_TYPE, ChatBox);
 
         routeToChat();
         addEventListener('hashchange', routeToChat);

+ 8 - 4
src/headless/plugins/chatboxes/api.js

@@ -9,7 +9,6 @@ import { createChatBox } from './utils.js';
 const { waitUntil } = promise_api;
 const _chatBoxTypes = {};
 
-
 /**
  * The "chatboxes" namespace.
  *
@@ -17,11 +16,15 @@ const _chatBoxTypes = {};
  * @memberOf api
  */
 export default {
+    /**
+     * @typedef {new (attrs: object, options: object) => ChatBox} ModelClass
+     */
+
     /**
      * @method api.chatboxes.create
      * @param {string|string[]} jids - A JID or array of JIDs
      * @param {Object} attrs An object containing configuration attributes
-     * @param {new (attrs: object, options: object) => ChatBox} model - The type of chatbox that should be created
+     * @param {ModelClass} model - The type of chatbox that should be created
      */
     async create (jids=[], attrs={}, model) {
         await waitUntil('chatBoxesFetched');
@@ -57,6 +60,7 @@ export default {
      * @memberOf api.chatboxes
      */
     registry: {
+
         /**
          * @method api.chatboxes.registry.add
          * Add another type of chatbox that can be added to this collection.
@@ -64,7 +68,7 @@ export default {
          * chatbox class to instantiate (e.g. ChatBox, MUC, Feed etc.) based on the
          * passed in attributes.
          * @param {string} type - The type name
-         * @param {Model} model - The model which will be instantiated for the given type name.
+         * @param {ModelClass} model - The model which will be instantiated for the given type name.
          */
         add(type, model) {
             _chatBoxTypes[type] = model;
@@ -73,7 +77,7 @@ export default {
         /**
          * @method api.chatboxes.registry.get
          * @param {string} type - The type name
-         * @return {Model} model - The model which will be instantiated for the given type name.
+         * @return {ModelClass} model - The model which will be instantiated for the given type name.
          */
         get(type) {
             return _chatBoxTypes[type];

+ 2 - 94
src/headless/plugins/emoji/index.js

@@ -1,96 +1,4 @@
-/**
- * @module converse-emoji
- * @copyright 2022, the Converse.js contributors
- * @license Mozilla Public License (MPLv2)
- */
-import './utils.js';
-import _converse from '../../shared/_converse.js';
-import api from '../../shared/api/index.js';
-import converse from '../../shared/api/public.js';
-import { getOpenPromise } from '@converse/openpromise';
 import EmojiPicker from './picker.js';
+import './plugin.js';
 
-
-converse.emojis = {
-    'initialized': false,
-    'initialized_promise': getOpenPromise()
-};
-
-
-converse.plugins.add('converse-emoji', {
-
-    initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by converse.js's plugin machinery.
-         */
-        const { ___ } = _converse;
-
-        api.settings.extend({
-            'emoji_image_path': 'https://twemoji.maxcdn.com/v/12.1.6/',
-            'emoji_categories': {
-                "smileys": ":grinning:",
-                "people": ":thumbsup:",
-                "activity": ":soccer:",
-                "travel": ":motorcycle:",
-                "objects": ":bomb:",
-                "nature": ":rainbow:",
-                "food": ":hotdog:",
-                "symbols": ":musical_note:",
-                "flags": ":flag_ac:",
-                "custom": null
-            },
-            // We use the triple-underscore method which doesn't actually
-            // translate but does signify to gettext that these strings should
-            // go into the POT file. The translation then happens in the
-            // template. We do this so that users can pass in their own
-            // strings via converse.initialize, which is before __ is
-            // available.
-            'emoji_category_labels': {
-                "smileys": ___("Smileys and emotions"),
-                "people": ___("People"),
-                "activity": ___("Activities"),
-                "travel": ___("Travel"),
-                "objects": ___("Objects"),
-                "nature": ___("Animals and nature"),
-                "food": ___("Food and drink"),
-                "symbols": ___("Symbols"),
-                "flags": ___("Flags"),
-                "custom": ___("Stickers")
-            }
-        });
-
-        const exports = { EmojiPicker };
-        Object.assign(_converse, exports); // XXX: DEPRECATED
-        Object.assign(_converse.exports, exports);
-
-        // We extend the default converse.js API to add methods specific to MUC groupchats.
-        Object.assign(api, {
-            /**
-             * @namespace api.emojis
-             * @memberOf api
-             */
-            emojis: {
-                /**
-                 * Initializes Emoji support by downloading the emojis JSON (and any applicable images).
-                 * @method api.emojis.initialize
-                 * @returns {Promise}
-                 */
-                async initialize () {
-                    if (!converse.emojis.initialized) {
-                        converse.emojis.initialized = true;
-                        const module = await import(/*webpackChunkName: "emojis" */ './emoji.json');
-                        const json = converse.emojis.json = module.default;
-                        converse.emojis.by_sn = Object.keys(json).reduce((result, cat) => Object.assign(result, json[cat]), {});
-                        converse.emojis.list = Object.values(converse.emojis.by_sn);
-                        converse.emojis.list.sort((a, b) => a.sn < b.sn ? -1 : (a.sn > b.sn ? 1 : 0));
-                        converse.emojis.shortnames = converse.emojis.list.map(m => m.sn);
-                        const getShortNames = () => converse.emojis.shortnames.map(s => s.replace(/[+]/g, "\\$&")).join('|');
-                        converse.emojis.shortnames_regex = new RegExp(getShortNames(), "gi");
-                        converse.emojis.initialized_promise.resolve();
-                    }
-                    return converse.emojis.initialized_promise;
-                }
-            }
-        });
-    }
-});
+export { EmojiPicker };

+ 96 - 0
src/headless/plugins/emoji/plugin.js

@@ -0,0 +1,96 @@
+/**
+ * @module converse-emoji
+ * @copyright 2022, the Converse.js contributors
+ * @license Mozilla Public License (MPLv2)
+ */
+import _converse from '../../shared/_converse.js';
+import api from '../../shared/api/index.js';
+import converse from '../../shared/api/public.js';
+import { getOpenPromise } from '@converse/openpromise';
+import './utils.js';
+import EmojiPicker from './picker.js';
+
+
+converse.emojis = {
+    'initialized': false,
+    'initialized_promise': getOpenPromise()
+};
+
+
+converse.plugins.add('converse-emoji', {
+
+    initialize () {
+        /* The initialize function gets called as soon as the plugin is
+         * loaded by converse.js's plugin machinery.
+         */
+        const { ___ } = _converse;
+
+        api.settings.extend({
+            'emoji_image_path': 'https://twemoji.maxcdn.com/v/12.1.6/',
+            'emoji_categories': {
+                "smileys": ":grinning:",
+                "people": ":thumbsup:",
+                "activity": ":soccer:",
+                "travel": ":motorcycle:",
+                "objects": ":bomb:",
+                "nature": ":rainbow:",
+                "food": ":hotdog:",
+                "symbols": ":musical_note:",
+                "flags": ":flag_ac:",
+                "custom": null
+            },
+            // We use the triple-underscore method which doesn't actually
+            // translate but does signify to gettext that these strings should
+            // go into the POT file. The translation then happens in the
+            // template. We do this so that users can pass in their own
+            // strings via converse.initialize, which is before __ is
+            // available.
+            'emoji_category_labels': {
+                "smileys": ___("Smileys and emotions"),
+                "people": ___("People"),
+                "activity": ___("Activities"),
+                "travel": ___("Travel"),
+                "objects": ___("Objects"),
+                "nature": ___("Animals and nature"),
+                "food": ___("Food and drink"),
+                "symbols": ___("Symbols"),
+                "flags": ___("Flags"),
+                "custom": ___("Stickers")
+            }
+        });
+
+        const exports = { EmojiPicker };
+        Object.assign(_converse, exports); // XXX: DEPRECATED
+        Object.assign(_converse.exports, exports);
+
+        // We extend the default converse.js API to add methods specific to MUC groupchats.
+        Object.assign(api, {
+            /**
+             * @namespace api.emojis
+             * @memberOf api
+             */
+            emojis: {
+                /**
+                 * Initializes Emoji support by downloading the emojis JSON (and any applicable images).
+                 * @method api.emojis.initialize
+                 * @returns {Promise}
+                 */
+                async initialize () {
+                    if (!converse.emojis.initialized) {
+                        converse.emojis.initialized = true;
+                        const module = await import(/*webpackChunkName: "emojis" */ './emoji.json');
+                        const json = converse.emojis.json = module.default;
+                        converse.emojis.by_sn = Object.keys(json).reduce((result, cat) => Object.assign(result, json[cat]), {});
+                        converse.emojis.list = Object.values(converse.emojis.by_sn);
+                        converse.emojis.list.sort((a, b) => a.sn < b.sn ? -1 : (a.sn > b.sn ? 1 : 0));
+                        converse.emojis.shortnames = converse.emojis.list.map(m => m.sn);
+                        const getShortNames = () => converse.emojis.shortnames.map(s => s.replace(/[+]/g, "\\$&")).join('|');
+                        converse.emojis.shortnames_regex = new RegExp(getShortNames(), "gi");
+                        converse.emojis.initialized_promise.resolve();
+                    }
+                    return converse.emojis.initialized_promise;
+                }
+            }
+        });
+    }
+});

+ 5 - 64
src/headless/plugins/mam/index.js

@@ -1,67 +1,8 @@
-/**
- * @description XEP-0313 Message Archive Management
- * @copyright 2022, the Converse.js contributors
- * @license Mozilla Public License (MPLv2)
- */
-import '../disco/index.js';
+import u from '../../utils/index.js';
 import MAMPlaceholderMessage from './placeholder.js';
-import _converse from '../../shared/_converse.js';
-import api from '../../shared/api/index.js';
-import converse from '../../shared/api/public.js';
-import mam_api from './api.js';
-import { PRIVATE_CHAT_TYPE } from '../..//shared/constants.js';
-import { Strophe } from 'strophe.js';
-import {
-    onMAMError,
-    onMAMPreferences,
-    getMAMPrefsFromFeature,
-    preMUCJoinMAMFetch,
-    fetchNewestMessages,
-    handleMAMResult
-} from './utils.js';
+import { fetchArchivedMessages } from './utils.js';
+import './plugin.js';
 
-const { NS } = Strophe;
+Object.assign(u, { mam: { fetchArchivedMessages }});
 
-converse.plugins.add('converse-mam', {
-    dependencies: ['converse-disco', 'converse-muc'],
-
-    initialize () {
-        api.settings.extend({
-            archived_messages_page_size: '50',
-            mam_request_all_pages: true,
-            message_archiving: undefined, // Supported values are 'always', 'never', 'roster' (https://xmpp.org/extensions/xep-0313.html#prefs)
-            message_archiving_timeout: 20000 // Time (in milliseconds) to wait before aborting MAM request
-        });
-
-        Object.assign(api, mam_api);
-        // This is mainly done to aid with tests
-        const exports = { onMAMError, onMAMPreferences, handleMAMResult, MAMPlaceholderMessage };
-        Object.assign(_converse, exports); // XXX DEPRECATED
-        Object.assign(_converse.exports, exports);
-
-        /************************ Event Handlers ************************/
-        api.listen.on('addClientFeatures', () => api.disco.own.features.add(NS.MAM));
-        api.listen.on('serviceDiscovered', getMAMPrefsFromFeature);
-        api.listen.on('chatRoomViewInitialized', view => {
-            if (api.settings.get('muc_show_logs_before_join')) {
-                preMUCJoinMAMFetch(view.model);
-                // If we want to show MAM logs before entering the MUC, we need
-                // to be informed once it's clear that this MUC supports MAM.
-                view.model.features.on('change:mam_enabled', () => preMUCJoinMAMFetch(view.model));
-            }
-        });
-        api.listen.on('enteredNewRoom', muc => muc.features.get('mam_enabled') && fetchNewestMessages(muc));
-
-        api.listen.on('chatReconnected', chat => {
-            if (chat.get('type') === PRIVATE_CHAT_TYPE) {
-                fetchNewestMessages(chat);
-            }
-        });
-
-        api.listen.on('afterMessagesFetched', chat => {
-            if (chat.get('type') === PRIVATE_CHAT_TYPE) {
-                fetchNewestMessages(chat);
-            }
-        });
-    }
-});
+export { MAMPlaceholderMessage };

+ 69 - 0
src/headless/plugins/mam/plugin.js

@@ -0,0 +1,69 @@
+/**
+ * @description XEP-0313 Message Archive Management
+ * @copyright 2022, the Converse.js contributors
+ * @license Mozilla Public License (MPLv2)
+ */
+import { Strophe } from 'strophe.js';
+import _converse from '../../shared/_converse.js';
+import api from '../../shared/api/index.js';
+import converse from '../../shared/api/public.js';
+import { PRIVATE_CHAT_TYPE } from '../..//shared/constants.js';
+import '../disco/index.js';
+import mam_api from './api.js';
+import MAMPlaceholderMessage from './placeholder.js';
+import {
+    fetchNewestMessages,
+    getMAMPrefsFromFeature,
+    handleMAMResult,
+    onMAMError,
+    onMAMPreferences,
+    preMUCJoinMAMFetch,
+} from './utils.js';
+
+
+const { NS } = Strophe;
+
+converse.plugins.add('converse-mam', {
+    dependencies: ['converse-disco', 'converse-muc'],
+
+    initialize () {
+        api.settings.extend({
+            archived_messages_page_size: '50',
+            mam_request_all_pages: true,
+            message_archiving: undefined, // Supported values are 'always', 'never', 'roster'
+                                          // https://xmpp.org/extensions/xep-0313.html#prefs
+            message_archiving_timeout: 20000 // Time (in milliseconds) to wait before aborting MAM request
+        });
+
+        Object.assign(api, mam_api);
+        // This is mainly done to aid with tests
+        const exports = { onMAMError, onMAMPreferences, handleMAMResult, MAMPlaceholderMessage };
+        Object.assign(_converse, exports); // XXX DEPRECATED
+        Object.assign(_converse.exports, exports);
+
+        /************************ Event Handlers ************************/
+        api.listen.on('addClientFeatures', () => api.disco.own.features.add(NS.MAM));
+        api.listen.on('serviceDiscovered', getMAMPrefsFromFeature);
+        api.listen.on('chatRoomViewInitialized', view => {
+            if (api.settings.get('muc_show_logs_before_join')) {
+                preMUCJoinMAMFetch(view.model);
+                // If we want to show MAM logs before entering the MUC, we need
+                // to be informed once it's clear that this MUC supports MAM.
+                view.model.features.on('change:mam_enabled', () => preMUCJoinMAMFetch(view.model));
+            }
+        });
+        api.listen.on('enteredNewRoom', muc => muc.features.get('mam_enabled') && fetchNewestMessages(muc));
+
+        api.listen.on('chatReconnected', chat => {
+            if (chat.get('type') === PRIVATE_CHAT_TYPE) {
+                fetchNewestMessages(chat);
+            }
+        });
+
+        api.listen.on('afterMessagesFetched', chat => {
+            if (chat.get('type') === PRIVATE_CHAT_TYPE) {
+                fetchNewestMessages(chat);
+            }
+        });
+    }
+});

+ 2 - 2
src/headless/plugins/mam/utils.js

@@ -2,13 +2,13 @@
  * @typedef {import('../muc/muc.js').default} MUC
  * @typedef {import('../chat/model.js').default} ChatBox
  */
+import sizzle from 'sizzle';
+import { Strophe, $iq } from 'strophe.js';
 import MAMPlaceholderMessage from './placeholder.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
 import log from '../../log.js';
-import sizzle from 'sizzle';
-import { Strophe, $iq } from 'strophe.js';
 import { parseMUCMessage } from '../../plugins/muc/parsers';
 import { parseMessage } from '../../plugins/chat/parsers';
 import { CHATROOMS_TYPE } from '../../shared/constants.js';

+ 14 - 1
src/headless/plugins/muc/affiliations/api.js

@@ -1,7 +1,8 @@
 /**
  * @module:plugin-muc-affiliations-api
+ * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem
  */
-import { setAffiliations } from './utils.js';
+import { getAffiliationList, setAffiliations } from './utils.js';
 
 export default {
     /**
@@ -43,6 +44,18 @@ export default {
             users = !Array.isArray(users) ? [users] : users;
             muc_jids = !Array.isArray(muc_jids) ? [muc_jids] : muc_jids;
             return setAffiliations(muc_jids, users);
+        },
+
+        /**
+         * Returns an array of {@link MemberListItem} objects, representing occupants
+         * that have the given affiliation.
+         * @typedef {("admin"|"owner"|"member")} NonOutcastAffiliation
+         * @param {NonOutcastAffiliation} affiliation
+         * @param {string} jid - The JID of the MUC for which the affiliation list should be fetched
+         * @returns {Promise<MemberListItem[]|Error>}
+         */
+        get (affiliation, jid) {
+            return getAffiliationList(affiliation, jid);
         }
     }
 }

+ 3 - 22
src/headless/plugins/muc/affiliations/utils.js

@@ -5,12 +5,12 @@
  * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem
  * @typedef {module:plugin-muc-affiliations-api.User} User
  * @typedef {import('@converse/skeletor').Model} Model
+ * @typedef {import('../constants').AFFILIATIONS} AFFILIATIONS
  */
 import _converse from '../../../shared/_converse.js';
 import api from '../../../shared/api/index.js';
 import converse from '../../../shared/api/public.js';
 import log from '../../../log.js';
-import { AFFILIATIONS } from '../constants.js';
 import { parseMemberListIQ } from '../parsers.js';
 
 const { Strophe, $iq, u } = converse.env;
@@ -49,25 +49,6 @@ export async function getAffiliationList (affiliation, muc_jid) {
         .sort((a, b) => (a.nick < b.nick ? -1 : a.nick > b.nick ? 1 : 0));
 }
 
-/**
- * Given an occupant model, see which affiliations may be assigned by that user
- * @param {Model} occupant
- * @returns {typeof AFFILIATIONS} An array of assignable affiliations
- */
-export function getAssignableAffiliations (occupant) {
-    let disabled = api.settings.get('modtools_disable_assign');
-    if (!Array.isArray(disabled)) {
-        disabled = disabled ? AFFILIATIONS : [];
-    }
-    if (occupant?.get('affiliation') === 'owner') {
-        return AFFILIATIONS.filter(a => !disabled.includes(a));
-    } else if (occupant?.get('affiliation') === 'admin') {
-        return AFFILIATIONS.filter(a => !['owner', 'admin', ...disabled].includes(a));
-    } else {
-        return [];
-    }
-}
-
 /**
  * Send IQ stanzas to the server to modify affiliations for users in this groupchat.
  * See: https://xmpp.org/extensions/xep-0045.html#modifymember
@@ -90,7 +71,7 @@ export function setAffiliations (muc_jid, users) {
  * a separate stanza for each JID.
  * Related ticket: https://issues.prosody.im/345
  *
- * @param {typeof AFFILIATIONS[number]} affiliation - The affiliation to be set
+ * @param {AFFILIATIONS[number]} affiliation - The affiliation to be set
  * @param {String|Array<String>} muc_jids - The JID(s) of the MUCs in which the
  *  affiliations need to be set.
  * @param {object} members - A map of jids, affiliations and
@@ -110,7 +91,7 @@ export function setAffiliation (affiliation, muc_jids, members) {
 
 /**
  * Send an IQ stanza specifying an affiliation change.
- * @param {typeof AFFILIATIONS[number]} affiliation: affiliation (could also be stored on the member object).
+ * @param {AFFILIATIONS[number]} affiliation: affiliation (could also be stored on the member object).
  * @param {string} muc_jid: The JID of the MUC in which the affiliation should be set.
  * @param {object} member: Map containing the member's jid and optionally a reason and affiliation.
  */

+ 1 - 2
src/headless/plugins/muc/api.js

@@ -8,10 +8,9 @@ import promise_api from '../../shared/api/promise.js';
 import { CHATROOMS_TYPE } from '../../shared/constants.js';
 import { Strophe } from 'strophe.js';
 import { getJIDFromURI } from '../../utils/jid.js';
-import { settings_api } from '../../shared/settings/api.js';
+import { settings_api as settings } from '../../shared/settings/api.js';
 
 const { waitUntil } = promise_api;
-const { settings } = settings_api;
 
 /**
  * The "rooms" namespace groups methods relevant to chatrooms

+ 5 - 0
src/headless/plugins/muc/index.js

@@ -1,3 +1,4 @@
+import u from '../../utils/index.js';
 import MUCMessage from './message.js';
 import MUCMessages from './messages.js';
 import MUC from './muc.js';
@@ -5,4 +6,8 @@ import MUCOccupant from './occupant.js';
 import MUCOccupants from './occupants.js';
 import './plugin.js';
 
+import { isChatRoom } from './utils.js';
+import { setAffiliation } from './affiliations/utils.js';
+Object.assign(u, { muc: { isChatRoom, setAffiliation }});
+
 export { MUCMessage, MUCMessages, MUC, MUCOccupant, MUCOccupants };

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

@@ -27,7 +27,7 @@ import { computeAffiliationsDelta, setAffiliations, getAffiliationList }  from '
 import { getOpenPromise } from '@converse/openpromise';
 import { handleCorrection } from '../../shared/chat/utils.js';
 import { initStorage, createStore } from '../../utils/storage.js';
-import { isArchived, getMediaURLsMetadata } from '../../shared/parsers.js';
+import { isArchived } from '../../shared/parsers.js';
 import { getUniqueId, isErrorObject, safeSave } from '../../utils/index.js';
 import { isUniView } from '../../utils/session.js';
 import { parseMUCMessage, parseMUCPresence } from './parsers.js';
@@ -1065,7 +1065,7 @@ class MUC extends ChatBox {
             'nick': this.get('nick'),
             'sender': 'me',
             'type': 'groupchat'
-        }, getMediaURLsMetadata(text));
+        }, u.getMediaURLsMetadata(text));
 
         /**
          * *Hook* which allows plugins to update the attributes of an outgoing

+ 37 - 0
src/headless/plugins/muc/occupant.js

@@ -1,4 +1,6 @@
 import { Model } from '@converse/skeletor';
+import api from '../../shared/api/index.js';
+import { AFFILIATIONS, ROLES } from './constants.js';
 
 /**
  * Represents a participant in a MUC
@@ -42,6 +44,41 @@ class MUCOccupant extends Model {
         return this.get('nick') || this.get('jid');
     }
 
+    /**
+     * Return roles which may be assigned to this occupant
+     * @returns {typeof ROLES} - An array of assignable roles
+     */
+    getAssignableRoles () {
+        let disabled = api.settings.get('modtools_disable_assign');
+        if (!Array.isArray(disabled)) {
+            disabled = disabled ? ROLES : [];
+        }
+        if (this.get('role') === 'moderator') {
+            return ROLES.filter(r => !disabled.includes(r));
+        } else {
+            return [];
+        }
+    }
+
+    /**
+    * Return affiliations which may be assigned by this occupant
+    * @returns {typeof AFFILIATIONS} An array of assignable affiliations
+    */
+    getAssignableAffiliations () {
+        let disabled = api.settings.get('modtools_disable_assign');
+        if (!Array.isArray(disabled)) {
+            disabled = disabled ? AFFILIATIONS : [];
+        }
+        if (this.get('affiliation') === 'owner') {
+            return AFFILIATIONS.filter(a => !disabled.includes(a));
+        } else if (this.get('affiliation') === 'admin') {
+            return AFFILIATIONS.filter(a => !['owner', 'admin', ...disabled].includes(a));
+        } else {
+            return [];
+        }
+    }
+
+
     isMember () {
         return ['admin', 'owner', 'member'].includes(this.get('affiliation'));
     }

+ 8 - 2
src/headless/plugins/muc/occupants.js

@@ -9,7 +9,7 @@ import converse from '../../shared/api/public.js';
 import { Collection, Model } from '@converse/skeletor';
 import { Strophe } from 'strophe.js';
 import { getAffiliationList } from './affiliations/utils.js';
-import { getAutoFetchedAffiliationLists, occupantsComparator } from './utils.js';
+import { occupantsComparator } from './utils.js';
 import { getUniqueId } from '../../utils/index.js';
 
 const { u } = converse.env;
@@ -39,6 +39,12 @@ class MUCOccupants extends Collection {
         this.on('change:role', () => this.sort());
     }
 
+
+    static getAutoFetchedAffiliationLists () {
+        const affs = api.settings.get('muc_fetch_members');
+        return Array.isArray(affs) ? affs : affs ? ['member', 'admin', 'owner'] : [];
+    }
+
     create (attrs, options) {
         if (attrs.id || attrs instanceof Model) {
             return super.create(attrs, options);
@@ -52,7 +58,7 @@ class MUCOccupants extends Collection {
             // https://xmpp.org/extensions/xep-0045.html#affil-priv
             return;
         }
-        const affiliations = getAutoFetchedAffiliationLists();
+        const affiliations = MUCOccupants.getAutoFetchedAffiliationLists();
         if (affiliations.length === 0) {
             return;
         }

+ 1 - 2
src/headless/plugins/muc/parsers.js

@@ -14,7 +14,6 @@ import {
     getCorrectionAttributes,
     getEncryptionAttributes,
     getErrorAttributes,
-    getMediaURLsMetadata,
     getOpenGraphMetadata,
     getOutOfBandAttributes,
     getReceiptId,
@@ -300,7 +299,7 @@ export async function parseMUCMessage (stanza, chatbox) {
     // We call this after the hook, to allow plugins to decrypt encrypted
     // messages, since we need to parse the message text to determine whether
     // there are media urls.
-    return Object.assign(attrs, getMediaURLsMetadata(attrs.is_encrypted ? attrs.plaintext : attrs.body));
+    return Object.assign(attrs, u.getMediaURLsMetadata(attrs.is_encrypted ? attrs.plaintext : attrs.body));
 }
 
 /**

+ 1 - 2
src/headless/plugins/muc/plugin.js

@@ -31,7 +31,7 @@ import {
     registerDirectInvitationHandler,
     routeToRoom,
 } from './utils.js';
-import { computeAffiliationsDelta, getAssignableAffiliations } from './affiliations/utils.js';
+import { computeAffiliationsDelta } from './affiliations/utils.js';
 import {
     AFFILIATION_CHANGES,
     AFFILIATION_CHANGES_LIST,
@@ -201,7 +201,6 @@ converse.plugins.add('converse-muc', {
             MUCMessages,
             MUCOccupant,
             MUCOccupants,
-            getAssignableAffiliations,
             getDefaultMUCNickname,
             isInfoVisible,
             onDirectMUCInvitation,

+ 4 - 28
src/headless/plugins/muc/utils.js

@@ -5,7 +5,7 @@ import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
 import log from '../../log.js';
-import { ROLES, MUC_ROLE_WEIGHTS } from './constants.js';
+import { MUC_ROLE_WEIGHTS } from './constants.js';
 import { safeSave } from '../../utils/index.js';
 import { CHATROOMS_TYPE } from '../../shared/constants.js';
 import { getUnloadEvent } from '../../utils/session.js';
@@ -20,11 +20,6 @@ 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'] : [];
-}
-
 export function occupantsComparator (occupant1, occupant2) {
     const role1 = occupant1.get('role') || 'none';
     const role2 = occupant2.get('role') || 'none';
@@ -37,26 +32,9 @@ export function occupantsComparator (occupant1, occupant2) {
     }
 }
 
-/**
- * Given an occupant model, see which roles may be assigned to that user.
- * @param {Model} occupant
- * @returns {typeof ROLES} - An array of assignable roles
- */
-export function getAssignableRoles (occupant) {
-    let disabled = api.settings.get('modtools_disable_assign');
-    if (!Array.isArray(disabled)) {
-        disabled = disabled ? ROLES : [];
-    }
-    if (occupant.get('role') === 'moderator') {
-        return ROLES.filter(r => !disabled.includes(r));
-    } else {
-        return [];
-    }
-}
-
 export function registerDirectInvitationHandler () {
     api.connection.get().addHandler(
-        message => {
+        (message) => {
             _converse.exports.onDirectMUCInvitation(message);
             return true;
         },
@@ -250,7 +228,8 @@ export function onStatusInitialized () {
 
 export function onBeforeResourceBinding () {
     api.connection.get().addHandler(
-        stanza => {
+        /** @param {Element} stanza */
+        (stanza) => {
             const muc_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
             if (!_converse.state.chatboxes.get(muc_jid)) {
                 api.waitUntil('chatBoxesFetched').then(async () => {
@@ -268,6 +247,3 @@ export function onBeforeResourceBinding () {
         'groupchat'
     );
 }
-
-
-Object.assign(_converse, { getAssignableRoles });

+ 2 - 1
src/headless/plugins/roster/index.js

@@ -2,6 +2,7 @@ import RosterContact from './contact.js';
 import RosterContacts from './contacts.js';
 import Presence from './presence.js';
 import Presences from './presences.js';
+import { RosterFilter } from './filter.js';
 import './plugin.js';
 
-export { RosterContact, RosterContacts, Presence, Presences };
+export { RosterContact, RosterContacts, Presence, Presences, RosterFilter };

+ 1 - 86
src/headless/plugins/roster/utils.js

@@ -8,7 +8,7 @@ import log from "../../log.js";
 import { Strophe } from 'strophe.js';
 import { Model } from '@converse/skeletor';
 import { RosterFilter } from '../../plugins/roster/filter.js';
-import { STATUS_WEIGHTS, PRIVATE_CHAT_TYPE } from "../../shared/constants";
+import { PRIVATE_CHAT_TYPE } from "../../shared/constants";
 import { initStorage } from '../../utils/storage.js';
 import { shouldClearCache } from '../../utils/session.js';
 
@@ -221,88 +221,3 @@ export function rejectPresenceSubscription (jid, message) {
     if (message && message !== "") { pres.c("status").t(message); }
     api.send(pres);
 }
-
-export function contactsComparator (contact1, contact2) {
-    const status1 = contact1.presence.get('show') || 'offline';
-    const status2 = contact2.presence.get('show') || 'offline';
-    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 STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1;
-    }
-}
-
-export function groupsComparator (a, b) {
-    const HEADER_WEIGHTS = {};
-    const {
-        HEADER_UNREAD,
-        HEADER_REQUESTING_CONTACTS,
-        HEADER_CURRENT_CONTACTS,
-        HEADER_UNGROUPED,
-        HEADER_PENDING_CONTACTS,
-    } = _converse.labels;
-
-    HEADER_WEIGHTS[HEADER_UNREAD] = 0;
-    HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 1;
-    HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS]    = 2;
-    HEADER_WEIGHTS[HEADER_UNGROUPED]           = 3;
-    HEADER_WEIGHTS[HEADER_PENDING_CONTACTS]    = 4;
-
-    const WEIGHTS =  HEADER_WEIGHTS;
-    const special_groups = Object.keys(HEADER_WEIGHTS);
-    const a_is_special = special_groups.includes(a);
-    const b_is_special = special_groups.includes(b);
-    if (!a_is_special && !b_is_special ) {
-        return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
-    } else if (a_is_special && b_is_special) {
-        return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0);
-    } else if (!a_is_special && b_is_special) {
-        const a_header = HEADER_CURRENT_CONTACTS;
-        return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0);
-    } else if (a_is_special && !b_is_special) {
-        const b_header = HEADER_CURRENT_CONTACTS;
-        return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0);
-    }
-}
-
-export function getGroupsAutoCompleteList () {
-    const roster = /** @type {RosterContacts} */(_converse.state.roster);
-    const groups = roster.reduce((groups, contact) => groups.concat(contact.get('groups')), []);
-    return [...new Set(groups.filter(i => i))];
-}
-
-export function getJIDsAutoCompleteList () {
-    const roster = /** @type {RosterContacts} */(_converse.state.roster);
-    return [...new Set(roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))];
-}
-
-
-/**
- * @param {string} query
- */
-export async function getNamesAutoCompleteList (query) {
-    const options = {
-        'mode': /** @type {RequestMode} */('cors'),
-        'headers': {
-            'Accept': 'text/json'
-        }
-    };
-    const url = `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(query)}`;
-    let response;
-    try {
-        response = await fetch(url, options);
-    } catch (e) {
-        log.error(`Failed to fetch names for query "${query}"`);
-        log.error(e);
-        return [];
-    }
-
-    const json = response.json;
-    if (!Array.isArray(json)) {
-        log.error(`Invalid JSON returned"`);
-        return [];
-    }
-    return json.map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
-}

+ 0 - 35
src/headless/shared/chat/utils.js

@@ -4,7 +4,6 @@
  * @typedef {import('../../plugins/chat/model.js').default} ChatBox
  * @typedef {import('../../plugins/muc/muc.js').default} MUC
  * @typedef {module:headless-shared-chat-utils.MediaURLData} MediaURLData
- * @typedef {module:headless-shared-parsers.MediaURLMetadata} MediaURLMetadata
  */
 import debounce from 'lodash-es/debounce.js';
 import converse from '../api/public.js';
@@ -38,39 +37,6 @@ export function pruneHistory (model) {
     }
 }
 
-/**
- * Given an array of {@link MediaURLMetadata} objects and text, return an
- * array of {@link MediaURL} objects.
- * @param {Array<MediaURLMetadata>} arr
- * @param {String} text
- * @returns{Array<MediaURLData>}
- */
-export function getMediaURLs (arr, text, offset=0) {
-    /**
-     * @typedef {Object} MediaURLData
-     * An object representing a URL found in a chat message
-     * @property {Boolean} is_audio
-     * @property {Boolean} is_image
-     * @property {Boolean} is_video
-     * @property {String} end
-     * @property {String} start
-     * @property {String} url
-     */
-    return arr.map(o => {
-        const start = o.start - offset;
-        const end = o.end - offset;
-        if (start < 0 || start >= text.length) {
-            return null;
-        }
-        return Object.assign({}, o, {
-            start,
-            end,
-            'url': text.substring(o.start-offset, o.end-offset),
-        });
-    }).filter(o => o);
-}
-
-
 /**
  * Determines whether the given attributes of an incoming message
  * represent a XEP-0308 correction and, if so, handles it appropriately.
@@ -119,7 +85,6 @@ export async function handleCorrection (model, attrs) {
     return message;
 }
 
-
 export const debouncedPruneHistory = debounce(pruneHistory, 500, {
     maxWait: 2000
 });

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

@@ -1,24 +1,15 @@
 /**
  * @module:headless-shared-parsers
- * @typedef {module:headless-shared-parsers.MediaURLMetadata} MediaURLMetadata
  * @typedef {module:headless-shared-parsers.Reference} Reference
  */
-import URI from 'urijs';
 import _converse from './_converse.js';
 import api from './api/index.js';
 import dayjs from 'dayjs';
 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/html.js';
 import { rejectMessage } from './actions';
-import {
-    isAudioURL,
-    isEncryptedFileURL,
-    isImageURL,
-    isVideoURL
-} from '../utils/url.js';
 
 const { NS } = Strophe;
 
@@ -165,60 +156,6 @@ export function getOpenGraphMetadata (stanza) {
 }
 
 
-/**
- * @param {string} text
- * @param {number} offset
- */
-export function getMediaURLsMetadata (text, offset=0) {
-    const objs = [];
-    if (!text) {
-        return {};
-    }
-    try {
-        URI.withinString(
-            text,
-            (url, start, end) => {
-                if (url.startsWith('_')) {
-                    url = url.slice(1);
-                    start += 1;
-                }
-                if (url.endsWith('_')) {
-                    url = url.slice(0, url.length-1);
-                    end -= 1;
-                }
-                objs.push({ url, 'start': start+offset, 'end': end+offset });
-                return url;
-            },
-            URL_PARSE_OPTIONS
-        );
-    } catch (error) {
-        log.debug(error);
-    }
-
-    /**
-     * @typedef {Object} MediaURLMetadata
-     * An object representing the metadata of a URL found in a chat message
-     * The actual URL is not saved, it can be extracted via the `start` and `end` indexes.
-     * @property {Boolean} is_audio
-     * @property {Boolean} is_image
-     * @property {Boolean} is_video
-     * @property {String} end
-     * @property {String} start
-     */
-    const media_urls = objs
-        .map(o => ({
-            'end': o.end,
-            'is_audio': isAudioURL(o.url),
-            'is_image': isImageURL(o.url),
-            'is_video': isVideoURL(o.url),
-            'is_encrypted': isEncryptedFileURL(o.url),
-            'start': o.start
-
-        }));
-    return media_urls.length ? { media_urls } : {};
-}
-
-
 export function getSpoilerAttributes (stanza) {
     const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
     return {
@@ -403,17 +340,3 @@ export function isServerMessage (stanza) {
 export function isArchived (original_stanza) {
     return !!sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
 }
-
-
-/**
- * Returns an object containing all attribute names and values for a particular element.
- * @method getAttributes
- * @param {Element} stanza
- * @returns {object}
- */
-export function getAttributes (stanza) {
-    return stanza.getAttributeNames().reduce((acc, name) => {
-        acc[name] = Strophe.xmlunescape(stanza.getAttribute(name));
-        return acc;
-    }, {});
-}

+ 1 - 1
src/headless/shared/settings/utils.js

@@ -1,7 +1,7 @@
 import isEqual from "lodash-es/isEqual.js";
 import pick from 'lodash-es/pick';
-import { DEFAULT_SETTINGS } from './constants.js';
 import { EventEmitter } from '@converse/skeletor';
+import { DEFAULT_SETTINGS } from './constants.js';
 import { merge } from '../../utils/object.js';
 
 

+ 7 - 1
src/headless/types/index.d.ts

@@ -1,3 +1,5 @@
+export { EmojiPicker } from "./plugins/emoji/index.js";
+export { MAMPlaceholderMessage } from "./plugins/mam/index.js";
 export { XMPPStatus } from "./plugins/status/index.js";
 export default converse;
 import api from "./shared/api/index.js";
@@ -6,9 +8,13 @@ import _converse from "./shared/_converse";
 import i18n from "./shared/i18n";
 import log from "./log.js";
 import u from "./utils/index.js";
+export const constants: typeof shared_constants & typeof muc_constants;
+import * as shared_constants from "./shared/constants.js";
+import * as muc_constants from "./plugins/muc/constants.js";
 export { api, converse, _converse, i18n, log, u };
+export { Bookmark, Bookmarks } from "./plugins/bookmarks/index.js";
 export { ChatBox, Message, Messages } from "./plugins/chat/index.js";
 export { MUCMessage, MUCMessages, MUC, MUCOccupant, MUCOccupants } from "./plugins/muc/index.js";
-export { RosterContact, RosterContacts, Presence, Presences } from "./plugins/roster/index.js";
+export { RosterContact, RosterContacts, RosterFilter, Presence, Presences } from "./plugins/roster/index.js";
 export { VCard, VCards } from "./plugins/vcard/index.js";
 //# sourceMappingURL=index.d.ts.map

+ 9 - 2
src/headless/types/plugins/adhoc/utils.d.ts

@@ -1,5 +1,12 @@
-export function parseForCommands(stanza: any): any;
-export function getCommandFields(iq: any, jid: any): {
+/**
+ * @param {Element} stanza
+ */
+export function parseForCommands(stanza: Element): any;
+/**
+ * @param {Element} iq
+ * @param {string} [jid]
+ */
+export function getCommandFields(iq: Element, jid?: string): {
     sessionid: any;
     instructions: any;
     fields: any;

+ 2 - 1
src/headless/types/plugins/bookmarks/collection.d.ts

@@ -1,9 +1,10 @@
 export default Bookmarks;
 declare class Bookmarks extends Collection {
+    static checkBookmarksSupport(): Promise<any>;
     constructor();
-    model: typeof Bookmark;
     initialize(): Promise<void>;
     fetched_flag: string;
+    model: typeof Bookmark;
     /**
      * @param {Bookmark} bookmark
      */

+ 3 - 1
src/headless/types/plugins/bookmarks/index.d.ts

@@ -1,2 +1,4 @@
-export {};
+import Bookmark from "./model.js";
+import Bookmarks from "./collection.js";
+export { Bookmark, Bookmarks };
 //# sourceMappingURL=index.d.ts.map

+ 2 - 0
src/headless/types/plugins/bookmarks/plugin.d.ts

@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=plugin.d.ts.map

+ 0 - 1
src/headless/types/plugins/bookmarks/utils.d.ts

@@ -1,4 +1,3 @@
-export function checkBookmarksSupport(): Promise<any>;
 export function initBookmarks(): Promise<void>;
 export function getNicknameFromBookmark(jid: any): any;
 export function handleBookmarksPush(message: any): boolean;

+ 0 - 2
src/headless/types/plugins/chat/model.d.ts

@@ -8,8 +8,6 @@ export namespace Strophe {
 }
 /**
  * Represents an open/ongoing chat conversation.
- * @namespace ChatBox
- * @memberOf _converse
  */
 declare class ChatBox extends ModelWithContact {
     constructor(attrs: any, options: any);

+ 8 - 5
src/headless/types/plugins/chatboxes/api.d.ts

@@ -1,9 +1,12 @@
 declare namespace _default {
+    /**
+     * @typedef {new (attrs: object, options: object) => ChatBox} ModelClass
+     */
     /**
      * @method api.chatboxes.create
      * @param {string|string[]} jids - A JID or array of JIDs
      * @param {Object} attrs An object containing configuration attributes
-     * @param {new (attrs: object, options: object) => ChatBox} model - The type of chatbox that should be created
+     * @param {ModelClass} model - The type of chatbox that should be created
      */
     function create(jids: string | string[], attrs: any, model: new (attrs: any, options: any) => import("../chat/model.js").default): Promise<import("../chat/model.js").default | import("../chat/model.js").default[]>;
     /**
@@ -19,15 +22,15 @@ declare namespace _default {
          * chatbox class to instantiate (e.g. ChatBox, MUC, Feed etc.) based on the
          * passed in attributes.
          * @param {string} type - The type name
-         * @param {Model} model - The model which will be instantiated for the given type name.
+         * @param {ModelClass} model - The model which will be instantiated for the given type name.
          */
-        function add(type: string, model: import("@converse/skeletor").Model): void;
+        function add(type: string, model: new (attrs: any, options: any) => import("../chat/model.js").default): void;
         /**
          * @method api.chatboxes.registry.get
          * @param {string} type - The type name
-         * @return {Model} model - The model which will be instantiated for the given type name.
+         * @return {ModelClass} model - The model which will be instantiated for the given type name.
          */
-        function get(type: string): import("@converse/skeletor").Model;
+        function get(type: string): new (attrs: any, options: any) => import("../chat/model.js").default;
     }
 }
 export default _default;

+ 2 - 1
src/headless/types/plugins/emoji/index.d.ts

@@ -1,2 +1,3 @@
-export {};
+export { EmojiPicker };
+import EmojiPicker from "./picker.js";
 //# sourceMappingURL=index.d.ts.map

+ 2 - 0
src/headless/types/plugins/emoji/plugin.d.ts

@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=plugin.d.ts.map

+ 2 - 1
src/headless/types/plugins/mam/index.d.ts

@@ -1,2 +1,3 @@
-export {};
+export { MAMPlaceholderMessage };
+import MAMPlaceholderMessage from "./placeholder.js";
 //# sourceMappingURL=index.d.ts.map

+ 2 - 0
src/headless/types/plugins/mam/plugin.d.ts

@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=plugin.d.ts.map

+ 10 - 0
src/headless/types/plugins/muc/affiliations/api.d.ts

@@ -41,7 +41,17 @@ declare namespace _default {
              */
             reason?: string;
         }[]): Promise<any>;
+        /**
+         * Returns an array of {@link MemberListItem} objects, representing occupants
+         * that have the given affiliation.
+         * @typedef {("admin"|"owner"|"member")} NonOutcastAffiliation
+         * @param {NonOutcastAffiliation} affiliation
+         * @param {string} jid - The JID of the MUC for which the affiliation list should be fetched
+         * @returns {Promise<MemberListItem[]|Error>}
+         */
+        function get(affiliation: "owner" | "admin" | "member", jid: string): Promise<Error | any[]>;
     }
 }
 export default _default;
+export type MemberListItem = any;
 //# sourceMappingURL=api.d.ts.map

+ 3 - 9
src/headless/types/plugins/muc/affiliations/utils.d.ts

@@ -9,12 +9,6 @@
  * @returns {Promise<MemberListItem[]|Error>}
  */
 export function getAffiliationList(affiliation: NonOutcastAffiliation, muc_jid: string): Promise<MemberListItem[] | Error>;
-/**
- * Given an occupant model, see which affiliations may be assigned by that user
- * @param {Model} occupant
- * @returns {typeof AFFILIATIONS} An array of assignable affiliations
- */
-export function getAssignableAffiliations(occupant: Model): typeof AFFILIATIONS;
 /**
  * Send IQ stanzas to the server to modify affiliations for users in this groupchat.
  * See: https://xmpp.org/extensions/xep-0045.html#modifymember
@@ -33,7 +27,7 @@ export function setAffiliations(muc_jid: string | Array<string>, users: Array<Us
  * a separate stanza for each JID.
  * Related ticket: https://issues.prosody.im/345
  *
- * @param {typeof AFFILIATIONS[number]} affiliation - The affiliation to be set
+ * @param {AFFILIATIONS[number]} affiliation - The affiliation to be set
  * @param {String|Array<String>} muc_jids - The JID(s) of the MUCs in which the
  *  affiliations need to be set.
  * @param {object} members - A map of jids, affiliations and
@@ -41,7 +35,7 @@ export function setAffiliations(muc_jid: string | Array<string>, users: Array<Us
  *  same affiliation as being currently set will be considered.
  * @returns {Promise} A promise which resolves and fails depending on the XMPP server response.
  */
-export function setAffiliation(affiliation: (typeof AFFILIATIONS)[number], muc_jids: string | Array<string>, members: object): Promise<any>;
+export function setAffiliation(affiliation: any[][number], muc_jids: string | Array<string>, members: object): Promise<any>;
 /**
  * Given two lists of objects with 'jid', 'affiliation' and
  * 'reason' properties, return a new list containing
@@ -79,5 +73,5 @@ export type NonOutcastAffiliation = ("admin" | "owner" | "member");
 export type MemberListItem = any;
 export type User = any;
 export type Model = import('@converse/skeletor').Model;
-import { AFFILIATIONS } from "../constants.js";
+export type AFFILIATIONS = any[];
 //# sourceMappingURL=utils.d.ts.map

+ 12 - 0
src/headless/types/plugins/muc/occupant.d.ts

@@ -15,9 +15,21 @@ declare class MUCOccupant extends Model {
     };
     save(key: any, val: any, options: any): any;
     getDisplayName(): any;
+    /**
+     * Return roles which may be assigned to this occupant
+     * @returns {typeof ROLES} - An array of assignable roles
+     */
+    getAssignableRoles(): typeof ROLES;
+    /**
+    * Return affiliations which may be assigned by this occupant
+    * @returns {typeof AFFILIATIONS} An array of assignable affiliations
+    */
+    getAssignableAffiliations(): typeof AFFILIATIONS;
     isMember(): boolean;
     isModerator(): boolean;
     isSelf(): any;
 }
 import { Model } from "@converse/skeletor";
+import { ROLES } from "./constants.js";
+import { AFFILIATIONS } from "./constants.js";
 //# sourceMappingURL=occupant.d.ts.map

+ 1 - 0
src/headless/types/plugins/muc/occupants.d.ts

@@ -6,6 +6,7 @@ export type MemberListItem = any;
  * @memberOf _converse
  */
 declare class MUCOccupants extends Collection {
+    static getAutoFetchedAffiliationLists(): any[];
     constructor(attrs: any, options: any);
     chatroom: any;
     get model(): typeof MUCOccupant;

+ 0 - 8
src/headless/types/plugins/muc/utils.d.ts

@@ -1,13 +1,6 @@
 export function isChatRoom(model: any): boolean;
 export function shouldCreateGroupchatMessage(attrs: any): any;
-export function getAutoFetchedAffiliationLists(): any[];
 export function occupantsComparator(occupant1: any, occupant2: any): 1 | 0 | -1;
-/**
- * Given an occupant model, see which roles may be assigned to that user.
- * @param {Model} occupant
- * @returns {typeof ROLES} - An array of assignable roles
- */
-export function getAssignableRoles(occupant: Model): typeof ROLES;
 export function registerDirectInvitationHandler(): void;
 export function disconnectChatRooms(): any;
 export function onWindowStateChanged(): Promise<void>;
@@ -43,5 +36,4 @@ export function onBeforeTearDown(): void;
 export function onStatusInitialized(): void;
 export function onBeforeResourceBinding(): void;
 export type Model = import('@converse/skeletor').Model;
-import { ROLES } from "./constants.js";
 //# sourceMappingURL=utils.d.ts.map

+ 2 - 1
src/headless/types/plugins/roster/index.d.ts

@@ -2,5 +2,6 @@ import RosterContact from "./contact.js";
 import RosterContacts from "./contacts.js";
 import Presence from "./presence.js";
 import Presences from "./presences.js";
-export { RosterContact, RosterContacts, Presence, Presences };
+import { RosterFilter } from "./filter.js";
+export { RosterContact, RosterContacts, Presence, Presences, RosterFilter };
 //# sourceMappingURL=index.d.ts.map

+ 0 - 11
src/headless/types/plugins/roster/utils.d.ts

@@ -28,16 +28,5 @@ export function onRosterContactsFetched(): void;
  * @param { String } message - An optional message to the user
  */
 export function rejectPresenceSubscription(jid: string, message: string): void;
-export function contactsComparator(contact1: any, contact2: any): 1 | 0 | -1;
-export function groupsComparator(a: any, b: any): 1 | 0 | -1;
-export function getGroupsAutoCompleteList(): any[];
-export function getJIDsAutoCompleteList(): any[];
-/**
- * @param {string} query
- */
-export function getNamesAutoCompleteList(query: string): Promise<{
-    label: any;
-    value: any;
-}[]>;
 export type RosterContacts = import('./contacts').default;
 //# sourceMappingURL=utils.d.ts.map

+ 0 - 9
src/headless/types/shared/chat/utils.d.ts

@@ -2,14 +2,6 @@
  * @param {ChatBox|MUC} model
  */
 export function pruneHistory(model: ChatBox | MUC): void;
-/**
- * Given an array of {@link MediaURLMetadata} objects and text, return an
- * array of {@link MediaURL} objects.
- * @param {Array<MediaURLMetadata>} arr
- * @param {String} text
- * @returns{Array<MediaURLData>}
- */
-export function getMediaURLs(arr: Array<MediaURLMetadata>, text: string, offset?: number): Array<MediaURLData>;
 /**
  * Determines whether the given attributes of an incoming message
  * represent a XEP-0308 correction and, if so, handles it appropriately.
@@ -27,5 +19,4 @@ export type Message = import('../../plugins/chat/message.js').default;
 export type ChatBox = import('../../plugins/chat/model.js').default;
 export type MUC = import('../../plugins/muc/muc.js').default;
 export type MediaURLData = any;
-export type MediaURLMetadata = any;
 //# sourceMappingURL=utils.d.ts.map

+ 0 - 24
src/headless/types/shared/parsers.d.ts

@@ -30,22 +30,6 @@ export function getCorrectionAttributes(stanza: any, original_stanza: any): {
     edited?: undefined;
 };
 export function getOpenGraphMetadata(stanza: any): any;
-/**
- * @param {string} text
- * @param {number} offset
- */
-export function getMediaURLsMetadata(text: string, offset?: number): {
-    media_urls?: undefined;
-} | {
-    media_urls: {
-        end: any;
-        is_audio: boolean;
-        is_image: any;
-        is_video: boolean;
-        is_encrypted: any;
-        start: any;
-    }[];
-};
 export function getSpoilerAttributes(stanza: any): {
     is_spoiler: boolean;
     spoiler_hint: any;
@@ -125,13 +109,6 @@ export function isServerMessage(stanza: Element): boolean;
  * @returns {boolean}
  */
 export function isArchived(original_stanza: Element): boolean;
-/**
- * Returns an object containing all attribute names and values for a particular element.
- * @method getAttributes
- * @param {Element} stanza
- * @returns {object}
- */
-export function getAttributes(stanza: Element): object;
 export class StanzaParseError extends Error {
     /**
      * @param {string} message
@@ -140,6 +117,5 @@ export class StanzaParseError extends Error {
     constructor(message: string, stanza: Element);
     stanza: Element;
 }
-export type MediaURLMetadata = any;
 export type Reference = any;
 //# sourceMappingURL=parsers.d.ts.map

+ 2 - 2
src/headless/types/utils/form.d.ts

@@ -15,10 +15,10 @@ export function webForm2xForm(field: HTMLInputElement | HTMLTextAreaElement | HT
  * @param {HTMLInputElement} input - The HTMLElement in which text is being entered
  * @param {number} [index] - An optional rightmost boundary index. If given, the text
  *  value of the input element will only be considered up until this index.
- * @param {string} [delineator] - An optional string delineator to
+ * @param {string|RegExp} [delineator] - An optional string delineator to
  *  differentiate between words.
  */
-export function getCurrentWord(input: HTMLInputElement, index?: number, delineator?: string): string;
+export function getCurrentWord(input: HTMLInputElement, index?: number, delineator?: string | RegExp): string;
 /**
  * @param {string} s
  */

+ 6 - 6
src/headless/types/utils/html.d.ts

@@ -1,12 +1,12 @@
 /**
- * @param { any } el
- * @returns { boolean }
+ * @param {unknown} el
+ * @returns {boolean}
  */
-export function isElement(el: any): boolean;
+export function isElement(el: unknown): boolean;
 /**
- * @param { Element | typeof Strophe.Builder } stanza
- * @param { string } name
- * @returns { boolean }
+ * @param {Element | typeof Strophe.Builder} stanza
+ * @param {string} name
+ * @returns {boolean}
  */
 export function isTagEqual(stanza: Element | typeof Strophe.Builder, name: string): boolean;
 /**

+ 72 - 69
src/headless/types/utils/index.d.ts

@@ -17,83 +17,92 @@ export function getRandomInt(max: any): number;
  */
 export function getUniqueId(suffix?: string): string;
 declare const _default: {
-    arrayBufferToBase64: typeof arrayBufferToBase64;
-    arrayBufferToHex: typeof arrayBufferToHex;
-    arrayBufferToString: typeof arrayBufferToString;
-    base64ToArrayBuffer: typeof base64ToArrayBuffer;
-    checkFileTypes: typeof checkFileTypes;
-    createStore: typeof createStore;
-    getCurrentWord: typeof getCurrentWord;
-    getDefaultStore: typeof getDefaultStore;
     getLongestSubstring: typeof getLongestSubstring;
     getOpenPromise: any;
     getRandomInt: typeof getRandomInt;
-    getSelectValues: typeof getSelectValues;
-    getURI: typeof getURI;
     getUniqueId: typeof getUniqueId;
-    isAudioURL: typeof isAudioURL;
-    isElement: typeof isElement;
     isEmptyMessage: typeof isEmptyMessage;
-    isError: typeof isError;
     isErrorObject: typeof isErrorObject;
-    isErrorStanza: typeof isErrorStanza;
-    isFunction: typeof isFunction;
-    isGIFURL: typeof isGIFURL;
-    isImageURL: typeof isImageURL;
-    isMentionBoundary: typeof isMentionBoundary;
-    isSameBareJID: typeof isSameBareJID;
-    isTagEqual: typeof isTagEqual;
-    isURLWithImageExtension: typeof isURLWithImageExtension;
-    isValidJID: typeof isValidJID;
-    isValidMUCJID: typeof isValidMUCJID;
-    isVideoURL: typeof isVideoURL;
-    isUniView: typeof isUniView;
-    isTestEnv: typeof isTestEnv;
-    merge: typeof merge;
     onMultipleEvents: typeof onMultipleEvents;
-    placeCaretAtEnd: typeof placeCaretAtEnd;
     prefixMentions: typeof prefixMentions;
-    queryChildren: typeof queryChildren;
-    replaceCurrentWord: typeof replaceCurrentWord;
     safeSave: typeof safeSave;
-    shouldClearCache: typeof shouldClearCache;
     shouldCreateMessage: typeof shouldCreateMessage;
-    stringToArrayBuffer: typeof stringToArrayBuffer;
-    stringToElement: typeof stringToElement;
     toStanza: typeof toStanza;
     triggerEvent: typeof triggerEvent;
-    webForm2xForm: typeof webForm2xForm;
     waitUntil: typeof waitUntil;
-} & Record<string, Function>;
+    isValidURL(text: string): boolean;
+    getURI(url: any): any;
+    checkFileTypes(types: string[], url: string): boolean;
+    filterQueryParamsFromURL(url: any): any;
+    isURLWithImageExtension(url: any): boolean;
+    isGIFURL(url: any): boolean;
+    isAudioURL(url: any): boolean;
+    isVideoURL(url: any): boolean;
+    isImageURL(url: any): any;
+    isEncryptedFileURL(url: any): any;
+    getMediaURLsMetadata(text: string, offset?: number): {
+        media_urls?: url.MediaURLMetadata[];
+    };
+    getMediaURLs(arr: url.MediaURLMetadata[], text: string, offset?: number): url.MediaURLMetadata[];
+    getDefaultStore(): "session" | "persistent";
+    createStore(id: any, store: any): any;
+    initStorage(model: any, id: any, type: any): void;
+    isErrorStanza(stanza: Element): boolean;
+    isForbiddenError(stanza: Element): boolean;
+    isServiceUnavailableError(stanza: Element): boolean;
+    getAttributes(stanza: Element): any;
+    isUniView(): boolean;
+    isTestEnv(): boolean;
+    getUnloadEvent(): "pagehide" | "beforeunload" | "unload";
+    replacePromise(_converse: any, name: string): void;
+    shouldClearCache(_converse: any): boolean;
+    tearDown(_converse: any): Promise<any>;
+    clearSession(_converse: any): any;
+    /**
+     * @copyright The Converse.js contributors
+     * @license Mozilla Public License (MPLv2)
+     * @description This is the core utilities module.
+     */
+    merge(dst: any, src: any): void;
+    isError(obj: any): boolean;
+    isFunction(val: any): boolean;
+    isValidJID(jid: any): boolean;
+    isValidMUCJID(jid: any): boolean;
+    isSameBareJID(jid1: any, jid2: any): boolean;
+    isSameDomain(jid1: any, jid2: any): boolean;
+    getJIDFromURI(jid: string): string;
+    isElement(el: unknown): boolean;
+    isTagEqual(stanza: Element | typeof import("strophe.js/src/types/builder.js").default, name: string): boolean;
+    stringToElement(s: string): Element;
+    queryChildren(el: HTMLElement, selector: string): ChildNode[];
+    siblingIndex(el: Element): number;
+    decodeHTMLEntities(str: string): string;
+    getSelectValues(select: HTMLSelectElement): string[];
+    webForm2xForm(field: HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement): Element;
+    getCurrentWord(input: HTMLInputElement, index?: number, delineator?: string | RegExp): string;
+    isMentionBoundary(s: string): boolean;
+    replaceCurrentWord(input: HTMLInputElement, new_value: string): void;
+    placeCaretAtEnd(textarea: HTMLTextAreaElement): void;
+    /**
+     * @copyright The Converse.js contributors
+     * @license Mozilla Public License (MPLv2)
+     * @description This is the core utilities module.
+     */
+    appendArrayBuffer(buffer1: any, buffer2: any): ArrayBufferLike;
+    arrayBufferToHex(ab: any): any;
+    arrayBufferToString(ab: any): string;
+    stringToArrayBuffer(string: any): ArrayBufferLike;
+    arrayBufferToBase64(ab: any): string;
+    base64ToArrayBuffer(b64: any): ArrayBufferLike;
+    hexToArrayBuffer(hex: any): ArrayBufferLike;
+} & CommonUtils & PluginUtils;
 export default _default;
-import { arrayBufferToBase64 } from "./arraybuffer.js";
-import { arrayBufferToHex } from "./arraybuffer.js";
-import { arrayBufferToString } from "./arraybuffer.js";
-import { base64ToArrayBuffer } from "./arraybuffer.js";
-import { checkFileTypes } from "./url.js";
-import { createStore } from "./storage.js";
-import { getCurrentWord } from "./form.js";
-import { getDefaultStore } from "./storage.js";
+export type CommonUtils = Record<string, Function>;
+/**
+ * The utils object
+ */
+export type PluginUtils = Record<'muc' | 'mam', CommonUtils>;
 declare function getLongestSubstring(string: any, candidates: any): any;
-import { getSelectValues } from "./form.js";
-import { getURI } from "./url.js";
-import { isAudioURL } from "./url.js";
-import { isElement } from "./html.js";
-import { isError } from "./object.js";
-import { isErrorStanza } from "./stanza.js";
-import { isFunction } from "./object.js";
-import { isGIFURL } from "./url.js";
-import { isImageURL } from "./url.js";
-import { isMentionBoundary } from "./form.js";
-import { isSameBareJID } from "./jid.js";
-import { isTagEqual } from "./html.js";
-import { isURLWithImageExtension } from "./url.js";
-import { isValidJID } from "./jid.js";
-import { isValidMUCJID } from "./jid.js";
-import { isVideoURL } from "./url.js";
-import { isUniView } from "./session.js";
-import { isTestEnv } from "./session.js";
-import { merge } from "./object.js";
 /**
  * Call the callback once all the events have been triggered
  * @param { Array } events: An array of objects, with keys `object` and
@@ -102,13 +111,7 @@ import { merge } from "./object.js";
  *    been triggered.
  */
 declare function onMultipleEvents(events: any[], callback: Function): void;
-import { placeCaretAtEnd } from "./form.js";
-import { queryChildren } from "./html.js";
-import { replaceCurrentWord } from "./form.js";
-import { shouldClearCache } from "./session.js";
 declare function shouldCreateMessage(attrs: any): any;
-import { stringToArrayBuffer } from "./arraybuffer.js";
-import { stringToElement } from "./html.js";
 import { toStanza } from "strophe.js";
 /**
  * @param {Element} el
@@ -118,6 +121,6 @@ import { toStanza } from "strophe.js";
  * @param {boolean} [cancelable]
  */
 declare function triggerEvent(el: Element, name: string, type?: string, bubbles?: boolean, cancelable?: boolean): void;
-import { webForm2xForm } from "./form.js";
 import { waitUntil } from "./promise.js";
+import * as url from "./url.js";
 //# sourceMappingURL=index.d.ts.map

+ 21 - 3
src/headless/types/utils/stanza.d.ts

@@ -1,4 +1,22 @@
-export function isErrorStanza(stanza: any): boolean;
-export function isForbiddenError(stanza: any): boolean;
-export function isServiceUnavailableError(stanza: any): boolean;
+/**
+ * @param {Element} stanza
+ * @returns {boolean}
+ */
+export function isErrorStanza(stanza: Element): boolean;
+/**
+ * @param {Element} stanza
+ * @returns {boolean}
+ */
+export function isForbiddenError(stanza: Element): boolean;
+/**
+ * @param {Element} stanza
+ * @returns {boolean}
+ */
+export function isServiceUnavailableError(stanza: Element): boolean;
+/**
+ * Returns an object containing all attribute names and values for a particular element.
+ * @param {Element} stanza
+ * @returns {object}
+ */
+export function getAttributes(stanza: Element): object;
 //# sourceMappingURL=stanza.d.ts.map

+ 48 - 0
src/headless/types/utils/url.d.ts

@@ -25,4 +25,52 @@ export function isAudioURL(url: any): boolean;
 export function isVideoURL(url: any): boolean;
 export function isImageURL(url: any): any;
 export function isEncryptedFileURL(url: any): any;
+/**
+ * @typedef {Object} MediaURLMetadata
+ * An object representing the metadata of a URL found in a chat message
+ * The actual URL is not saved, it can be extracted via the `start` and `end` indexes.
+ * @property {boolean} [is_audio]
+ * @property {boolean} [is_image]
+ * @property {boolean} [is_video]
+ * @property {boolean} [is_encrypted]
+ * @property {number} [end]
+ * @property {number} [start]
+ */
+/**
+ * An object representing a URL found in a chat message
+ * @typedef {MediaURLMetadata} MediaURLData
+ * @property {string} url
+ */
+/**
+ * @param {string} text
+ * @param {number} offset
+ * @returns {{media_urls?: MediaURLMetadata[]}}
+ */
+export function getMediaURLsMetadata(text: string, offset?: number): {
+    media_urls?: MediaURLMetadata[];
+};
+/**
+ * Given an array of {@link MediaURLMetadata} objects and text, return an
+ * array of {@link MediaURL} objects.
+ * @param {Array<MediaURLMetadata>} arr
+ * @param {string} text
+ * @returns {MediaURLData[]}
+ */
+export function getMediaURLs(arr: Array<MediaURLMetadata>, text: string, offset?: number): MediaURLData[];
+/**
+ * An object representing the metadata of a URL found in a chat message
+ * The actual URL is not saved, it can be extracted via the `start` and `end` indexes.
+ */
+export type MediaURLMetadata = {
+    is_audio?: boolean;
+    is_image?: boolean;
+    is_video?: boolean;
+    is_encrypted?: boolean;
+    end?: number;
+    start?: number;
+};
+/**
+ * An object representing a URL found in a chat message
+ */
+export type MediaURLData = MediaURLMetadata;
 //# sourceMappingURL=url.d.ts.map

+ 1 - 1
src/headless/utils/form.js

@@ -59,7 +59,7 @@ export function webForm2xForm (field) {
  * @param {HTMLInputElement} input - The HTMLElement in which text is being entered
  * @param {number} [index] - An optional rightmost boundary index. If given, the text
  *  value of the input element will only be considered up until this index.
- * @param {string} [delineator] - An optional string delineator to
+ * @param {string|RegExp} [delineator] - An optional string delineator to
  *  differentiate between words.
  */
 export function getCurrentWord (input, index, delineator) {

+ 5 - 5
src/headless/utils/html.js

@@ -2,17 +2,17 @@ import DOMPurify from 'dompurify';
 import { Strophe } from 'strophe.js';
 
 /**
- * @param { any } el
- * @returns { boolean }
+ * @param {unknown} el
+ * @returns {boolean}
  */
 export function isElement (el) {
     return el instanceof Element || el instanceof HTMLDocument;
 }
 
 /**
- * @param { Element | typeof Strophe.Builder } stanza
- * @param { string } name
- * @returns { boolean }
+ * @param {Element | typeof Strophe.Builder} stanza
+ * @param {string} name
+ * @returns {boolean}
  */
 export function isTagEqual (stanza, name) {
     if (stanza instanceof Strophe.Builder) {

+ 28 - 74
src/headless/utils/index.js

@@ -3,54 +3,33 @@
  * @license Mozilla Public License (MPLv2)
  * @description This is the core utilities module.
  */
-import log, { LEVELS } from '../log.js';
-import { Model } from '@converse/skeletor';
 import { toStanza } from 'strophe.js';
 import { getOpenPromise } from '@converse/openpromise';
-import { shouldClearCache, isTestEnv, isUniView } from './session.js';
-import { merge, isError, isFunction } from './object.js';
-import { createStore, getDefaultStore } from './storage.js';
+import { Model } from '@converse/skeletor';
+import log, { LEVELS } from '../log.js';
 import { waitUntil } from './promise.js';
-import { isValidJID, isValidMUCJID, isSameBareJID } from './jid.js';
-import { isErrorStanza } from './stanza.js';
-import {
-    getCurrentWord,
-    getSelectValues,
-    isMentionBoundary,
-    placeCaretAtEnd,
-    replaceCurrentWord,
-    webForm2xForm
-} from './form.js';
-import {
-    isElement,
-    isTagEqual,
-    queryChildren,
-    stringToElement,
-} from './html.js';
-import {
-    arrayBufferToHex,
-    arrayBufferToString,
-    stringToArrayBuffer,
-    arrayBufferToBase64,
-    base64ToArrayBuffer,
-} from './arraybuffer.js';
-import {
-    checkFileTypes,
-    getURI,
-    isAudioURL,
-    isGIFURL,
-    isImageURL,
-    isURLWithImageExtension,
-    isVideoURL,
-} from './url.js';
-
+import * as stanza from './stanza.js';
+import * as session from './session.js';
+import * as object from './object.js';
+import * as storage from './storage.js';
+import * as jid from './jid';
+import * as form from './form.js';
+import * as html from './html.js';
+import * as arraybuffer from './arraybuffer.js';
+import * as url from './url.js';
 
 /**
+ * @typedef {Record<string, Function>} CommonUtils
+ * @typedef {Record<'muc'|'mam', CommonUtils>} PluginUtils
+ *
  * The utils object
  * @namespace u
- * @type {Record<string, Function>}
+ * @type {CommonUtils & PluginUtils}
  */
-const u = {};
+const u = {
+    muc: null,
+    mam: null,
+};
 
 
 /**
@@ -185,51 +164,26 @@ export function getUniqueId (suffix) {
 }
 
 export default Object.assign({
-    arrayBufferToBase64,
-    arrayBufferToHex,
-    arrayBufferToString,
-    base64ToArrayBuffer,
-    checkFileTypes,
-    createStore,
-    getCurrentWord,
-    getDefaultStore,
+    ...arraybuffer,
+    ...form,
+    ...html,
+    ...jid,
+    ...object,
+    ...session,
+    ...stanza,
+    ...storage,
+    ...url,
     getLongestSubstring,
     getOpenPromise,
     getRandomInt,
-    getSelectValues,
-    getURI,
     getUniqueId,
-    isAudioURL,
-    isElement,
     isEmptyMessage,
-    isError,
     isErrorObject,
-    isErrorStanza,
-    isFunction,
-    isGIFURL,
-    isImageURL,
-    isMentionBoundary,
-    isSameBareJID,
-    isTagEqual,
-    isURLWithImageExtension,
-    isValidJID,
-    isValidMUCJID,
-    isVideoURL,
-    isUniView,
-    isTestEnv,
-    merge,
     onMultipleEvents,
-    placeCaretAtEnd,
     prefixMentions,
-    queryChildren,
-    replaceCurrentWord,
     safeSave,
-    shouldClearCache,
     shouldCreateMessage,
-    stringToArrayBuffer,
-    stringToElement,
     toStanza,
     triggerEvent,
-    webForm2xForm,
     waitUntil, // TODO: remove. Only the API should be used
 }, u);

+ 24 - 0
src/headless/utils/stanza.js

@@ -2,6 +2,10 @@ import sizzle from "sizzle";
 import { Strophe } from 'strophe.js';
 import { isElement } from './html.js';
 
+/**
+ * @param {Element} stanza
+ * @returns {boolean}
+ */
 export function isErrorStanza (stanza) {
     if (!isElement(stanza)) {
         return false;
@@ -9,6 +13,10 @@ export function isErrorStanza (stanza) {
     return stanza.getAttribute('type') === 'error';
 }
 
+/**
+ * @param {Element} stanza
+ * @returns {boolean}
+ */
 export function isForbiddenError (stanza) {
     if (!isElement(stanza)) {
         return false;
@@ -16,9 +24,25 @@ export function isForbiddenError (stanza) {
     return sizzle(`error[type="auth"] forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0;
 }
 
+/**
+ * @param {Element} stanza
+ * @returns {boolean}
+ */
 export function isServiceUnavailableError (stanza) {
     if (!isElement(stanza)) {
         return false;
     }
     return sizzle(`error[type="cancel"] service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0;
 }
+
+/**
+ * Returns an object containing all attribute names and values for a particular element.
+ * @param {Element} stanza
+ * @returns {object}
+ */
+export function getAttributes (stanza) {
+    return stanza.getAttributeNames().reduce((acc, name) => {
+        acc[name] = Strophe.xmlunescape(stanza.getAttribute(name));
+        return acc;
+    }, {});
+}

+ 85 - 0
src/headless/utils/url.js

@@ -1,6 +1,7 @@
 import URI from 'urijs';
 import log from '../log.js';
 import { settings_api } from '../shared/settings/api.js';
+import { URL_PARSE_OPTIONS } from '../shared/constants.js';
 
 const settings = settings_api;
 
@@ -79,3 +80,87 @@ export function isImageURL (url) {
 export function isEncryptedFileURL (url) {
     return url.startsWith('aesgcm://');
 }
+
+/**
+ * @typedef {Object} MediaURLMetadata
+ * An object representing the metadata of a URL found in a chat message
+ * The actual URL is not saved, it can be extracted via the `start` and `end` indexes.
+ * @property {boolean} [is_audio]
+ * @property {boolean} [is_image]
+ * @property {boolean} [is_video]
+ * @property {boolean} [is_encrypted]
+ * @property {number} [end]
+ * @property {number} [start]
+ */
+
+/**
+ * An object representing a URL found in a chat message
+ * @typedef {MediaURLMetadata} MediaURLData
+ * @property {string} url
+ */
+
+/**
+ * @param {string} text
+ * @param {number} offset
+ * @returns {{media_urls?: MediaURLMetadata[]}}
+ */
+export function getMediaURLsMetadata (text, offset=0) {
+    const objs = [];
+    if (!text) {
+        return {};
+    }
+    try {
+        URI.withinString(
+            text,
+            (url, start, end) => {
+                if (url.startsWith('_')) {
+                    url = url.slice(1);
+                    start += 1;
+                }
+                if (url.endsWith('_')) {
+                    url = url.slice(0, url.length-1);
+                    end -= 1;
+                }
+                objs.push({ url, 'start': start+offset, 'end': end+offset });
+                return url;
+            },
+            URL_PARSE_OPTIONS
+        );
+    } catch (error) {
+        log.debug(error);
+    }
+
+    const media_urls = objs
+        .map(o => ({
+            'end': o.end,
+            'is_audio': isAudioURL(o.url),
+            'is_image': isImageURL(o.url),
+            'is_video': isVideoURL(o.url),
+            'is_encrypted': isEncryptedFileURL(o.url),
+            'start': o.start
+
+        }));
+    return media_urls.length ? { media_urls } : {};
+}
+
+/**
+ * Given an array of {@link MediaURLMetadata} objects and text, return an
+ * array of {@link MediaURL} objects.
+ * @param {Array<MediaURLMetadata>} arr
+ * @param {string} text
+ * @returns {MediaURLData[]}
+ */
+export function getMediaURLs (arr, text, offset=0) {
+    return arr.map(o => {
+        const start = o.start - offset;
+        const end = o.end - offset;
+        if (start < 0 || start >= text.length) {
+            return null;
+        }
+        return (Object.assign({}, o, {
+            start,
+            end,
+            'url': text.substring(o.start-offset, o.end-offset),
+        }));
+    }).filter(o => o);
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 13355 - 199
src/i18n/converse.pot


+ 2 - 3
src/i18n/index.js

@@ -5,8 +5,7 @@
  * @description This is the internationalization module
  */
 import Jed from 'jed';
-import { api, converse, log } from '@converse/headless';
-import { isTestEnv } from '@converse/headless/utils/session';
+import { api, converse, log, u } from '@converse/headless';
 
 const { dayjs } = converse.env;
 
@@ -112,7 +111,7 @@ const i18n = {
     },
 
     async initialize() {
-        if (isTestEnv()) {
+        if (u.isTestEnv()) {
             locale = 'en';
         } else {
             try {

+ 4 - 3
src/plugins/bookmark-views/components/bookmarks-list.js

@@ -1,13 +1,14 @@
 import debounce from "lodash-es/debounce";
+import { Model } from '@converse/skeletor';
+import { _converse, api, u } from '@converse/headless';
 import tplBookmarksList from './templates/list.js';
 import tplSpinner from "templates/spinner.js";
 import { CustomElement } from 'shared/components/element.js';
-import { Model } from '@converse/skeletor';
-import { _converse, api } from '@converse/headless';
-import { initStorage } from '@converse/headless/utils/storage.js';
 
 import '../styles/bookmarks.scss';
 
+const { initStorage }  = u;
+
 
 export default class BookmarksView extends CustomElement {
 

+ 1 - 2
src/plugins/bookmark-views/index.js

@@ -3,12 +3,11 @@
  * @copyright 2022, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
+import { _converse, api, converse } from '@converse/headless';
 import './modals/bookmark-list.js';
 import './modals/bookmark-form.js';
-import '@converse/headless/plugins/muc/index.js';
 import BookmarkForm from './components/bookmark-form.js';
 import BookmarksView from './components/bookmarks-list.js';
-import { _converse, api, converse } from '@converse/headless';
 import { bookmarkableChatRoomView } from './mixins.js';
 import { getHeadingButtons, removeBookmarkViaEvent, addBookmarkViaEvent } from './utils.js';
 

+ 3 - 4
src/plugins/bookmark-views/utils.js

@@ -1,8 +1,7 @@
+import { _converse, api, converse, constants, Bookmarks } from '@converse/headless';
 import { __ } from 'i18n';
-import { _converse, api, converse } from '@converse/headless';
-import { checkBookmarksSupport } from '@converse/headless/plugins/bookmarks/utils';
-import { CHATROOMS_TYPE } from '@converse/headless/shared/constants';
 
+const { CHATROOMS_TYPE } = constants;
 
 export function getHeadingButtons (view, buttons) {
     if (api.settings.get('allow_bookmarks') && view.model.get('type') === CHATROOMS_TYPE) {
@@ -16,7 +15,7 @@ export function getHeadingButtons (view, buttons) {
         };
         const names = buttons.map(t => t.name);
         const idx = names.indexOf('details');
-        const data_promise = checkBookmarksSupport().then((s) => (s ? data : null));
+        const data_promise = Bookmarks.checkBookmarksSupport().then((s) => (s ? data : null));
         return idx > -1 ? [...buttons.slice(0, idx), data_promise, ...buttons.slice(idx)] : [data_promise, ...buttons];
     }
     return buttons;

+ 2 - 4
src/plugins/chatboxviews/index.js

@@ -1,12 +1,10 @@
 /**
- * @module converse-chatboxviews
- * @copyright 2022, the Converse.js contributors
+ * @copyright 2024, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
+import { _converse, api, converse } from '@converse/headless';
 import './view.js';
-import '@converse/headless/plugins/chatboxes/index.js';
 import ChatBoxViews from './container.js';
-import { _converse, api, converse } from '@converse/headless';
 import { calculateViewportHeightUnit } from './utils.js';
 
 import './styles/chats.scss';

+ 2 - 2
src/plugins/chatboxviews/templates/chats.js

@@ -1,8 +1,8 @@
 import { html } from 'lit';
 import { repeat } from 'lit/directives/repeat.js';
-import { _converse, api } from '@converse/headless';
-import { CONTROLBOX_TYPE, CHATROOMS_TYPE, HEADLINES_TYPE } from '@converse/headless/shared/constants';
+import { _converse, api, constants } from '@converse/headless';
 
+const { CONTROLBOX_TYPE, CHATROOMS_TYPE, HEADLINES_TYPE } = constants;
 
 function shouldShowChat (c) {
     const is_minimized = (api.settings.get('view_mode') === 'overlayed' && c.get('minimized'));

+ 4 - 4
src/plugins/chatboxviews/view.js

@@ -1,8 +1,8 @@
+import { render } from 'lit';
+import { api, _converse } from '@converse/headless';
 import tplBackgroundLogo from '../../templates/background_logo.js';
 import tplChats from './templates/chats.js';
 import { CustomElement } from 'shared/components/element.js';
-import { api, _converse } from '@converse/headless';
-import { render } from 'lit';
 
 
 class ConverseChats extends CustomElement {
@@ -34,14 +34,14 @@ class ConverseChats extends CustomElement {
         body.classList.add(`converse-${api.settings.get('view_mode')}`);
 
         /**
-         * Triggered once the _converse.ChatBoxViews view-colleciton has been initialized
+         * Triggered once the ChatBoxViews view-colleciton has been initialized
          * @event _converse#chatBoxViewsInitialized
          * @example _converse.api.listen.on('chatBoxViewsInitialized', () => { ... });
          */
         api.trigger('chatBoxViewsInitialized');
     }
 
-    render () { // eslint-disable-line class-methods-use-this
+    render () {
         return tplChats();
     }
 }

+ 3 - 2
src/plugins/chatview/chat.js

@@ -1,10 +1,11 @@
+import { _converse, api, constants } from '@converse/headless';
 import 'plugins/chatview/heading.js';
 import 'plugins/chatview/bottom-panel.js';
 import BaseChatView from 'shared/chat/baseview.js';
 import tplChat from './templates/chat.js';
 import { __ } from 'i18n';
-import { _converse, api } from '@converse/headless';
-import { ACTIVE } from '@converse/headless/shared/constants.js';
+
+const { ACTIVE } = constants;
 
 /**
  * The view of an open/ongoing chat conversation.

+ 6 - 8
src/plugins/chatview/message-form.js

@@ -1,15 +1,13 @@
 /**
  * @typedef {import('shared/chat/emoji-dropdown.js').default} EmojiDropdown
  */
-import tplMessageForm from './templates/message-form.js';
-import { ACTIVE, COMPOSING } from '@converse/headless/shared/constants.js';
-import { CustomElement } from 'shared/components/element.js';
+import { _converse, api, converse, constants, u } from "@converse/headless";
 import { __ } from 'i18n';
-import { _converse, api, converse } from "@converse/headless";
+import { CustomElement } from 'shared/components/element.js';
+import tplMessageForm from './templates/message-form.js';
 import { parseMessageForCommands } from './utils.js';
-import { prefixMentions } from '@converse/headless/utils/index.js';
 
-const { u } = converse.env;
+const { ACTIVE, COMPOSING } = constants;
 
 
 export default class MessageForm extends CustomElement {
@@ -89,11 +87,11 @@ export default class MessageForm extends CustomElement {
 
     onMessageCorrecting (message) {
         if (message.get('correcting')) {
-            this.insertIntoTextArea(prefixMentions(message), true, true);
+            this.insertIntoTextArea(u.prefixMentions(message), true, true);
         } else {
             const currently_correcting = this.model.messages.findWhere('correcting');
             if (currently_correcting && currently_correcting !== message) {
-                this.insertIntoTextArea(prefixMentions(message), true, true);
+                this.insertIntoTextArea(u.prefixMentions(message), true, true);
             } else {
                 this.insertIntoTextArea('', true, false);
             }

+ 4 - 4
src/plugins/chatview/templates/chat-head.js

@@ -1,10 +1,10 @@
-import { __ } from 'i18n';
-import { _converse } from '@converse/headless';
-import { getStandaloneButtons, getDropdownButtons } from 'shared/chat/utils.js';
 import { html } from "lit";
 import { until } from 'lit/directives/until.js';
-import { HEADLINES_TYPE } from '@converse/headless/shared/constants.js';
+import { _converse, constants } from '@converse/headless';
+import { __ } from 'i18n';
+import { getStandaloneButtons, getDropdownButtons } from 'shared/chat/utils.js';
 
+const { HEADLINES_TYPE } = constants;
 
 export default (o) => {
     const i18n_profile = __("The User's Profile Image");

+ 3 - 2
src/plugins/chatview/templates/chat.js

@@ -1,6 +1,7 @@
 import { html } from "lit";
-import { _converse } from '@converse/headless';
-import { CHATROOMS_TYPE } from "@converse/headless/shared/constants";
+import { constants } from '@converse/headless';
+
+const { CHATROOMS_TYPE } = constants;
 
 export default (o) => html`
     <div class="flyout box-flyout">

+ 2 - 3
src/plugins/controlbox/controlbox.js

@@ -1,9 +1,8 @@
 import tplControlbox from './templates/controlbox.js';
 import { CustomElement } from 'shared/components/element.js';
-import { _converse, api, converse } from '@converse/headless';
-import { LOGOUT } from '@converse/headless/shared/constants.js';
+import { _converse, api, constants, u } from '@converse/headless';
 
-const u = converse.env.utils;
+const { LOGOUT } = constants;
 
 /**
  * The ControlBox is the section of the chat that contains the open groupchats,

+ 3 - 2
src/plugins/controlbox/index.js

@@ -2,6 +2,7 @@
  * @copyright 2022, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
+import { _converse, api, converse, log, constants } from '@converse/headless';
 import "shared/components/brand-heading.js";
 import "../chatview/index.js";
 import './loginform.js';
@@ -10,13 +11,13 @@ import ControlBox from './model.js';
 import ControlBoxToggle from './toggle.js';
 import ControlBoxView from './controlbox.js';
 import controlbox_api from './api.js';
-import { _converse, api, converse, log } from '@converse/headless';
 import { addControlBox, clearSession, disconnect, onChatBoxesFetched } from './utils.js';
-import { CONTROLBOX_TYPE } from "@converse/headless/shared/constants.js";
 
 import './styles/_controlbox.scss';
 import './styles/controlbox-head.scss';
 
+const { CONTROLBOX_TYPE } = constants;
+
 
 converse.plugins.add('converse-controlbox', {
     /* Plugin dependencies are other plugins which might be

+ 2 - 2
src/plugins/controlbox/loginform.js

@@ -1,11 +1,11 @@
 import bootstrap from 'bootstrap.native';
+import { _converse, api, converse, constants } from '@converse/headless';
 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';
 import { updateSettingsWithFormData, validateJID } from './utils.js';
 
 const { Strophe } = converse.env;
+const { ANONYMOUS } = constants;
 
 
 class LoginForm extends CustomElement {

+ 3 - 2
src/plugins/controlbox/model.js

@@ -1,8 +1,9 @@
-import { _converse, api, converse } from '@converse/headless';
 import { Model } from '@converse/skeletor';
-import { CONTROLBOX_TYPE } from '@converse/headless/shared/constants';
+import { _converse, api, converse, constants } from '@converse/headless';
 
 const { dayjs } = converse.env;
+const { CONTROLBOX_TYPE } = constants;
+
 
 /**
  * The ControlBox is the section of the chat that contains the open groupchats,

+ 2 - 2
src/plugins/controlbox/templates/controlbox.js

@@ -2,11 +2,11 @@
  * @typedef {import('../controlbox').default} ControlBoxView
  */
 import tplSpinner from "templates/spinner.js";
-import { _converse, api, converse } from "@converse/headless";
+import { _converse, api, converse, constants } from "@converse/headless";
 import { html } from 'lit';
-import { ANONYMOUS } from "@converse/headless/shared/constants";
 
 const { Strophe } = converse.env;
+const { ANONYMOUS } = constants;
 
 
 function whenNotConnected (o) {

+ 4 - 3
src/plugins/controlbox/templates/loginform.js

@@ -1,10 +1,11 @@
+import { html } from 'lit';
+import { _converse, api, constants } from '@converse/headless';
 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';
-import { html } from 'lit';
+
+const { ANONYMOUS, EXTERNAL, LOGIN, PREBIND, CONNECTION_STATUS } = constants;
 
 const trust_checkbox = (checked) => {
     const i18n_hint_trusted = __(

+ 2 - 3
src/plugins/fullscreen/index.js

@@ -3,11 +3,10 @@
  * @license Mozilla Public License (MPLv2)
  * @copyright 2022, the Converse.js contributors
  */
-import { api, converse } from "@converse/headless";
-import { isUniView } from '@converse/headless/utils/session.js';
-
+import { api, converse, u } from "@converse/headless";
 import './styles/fullscreen.scss';
 
+const { isUniView } = u;
 
 converse.plugins.add('converse-fullscreen', {
 

+ 4 - 3
src/plugins/headlines-view/feed-list.js

@@ -1,7 +1,8 @@
-import tplFeedsList from './templates/feeds-list.js';
+import { _converse, api, constants } from '@converse/headless';
 import { CustomElement } from 'shared/components/element.js';
-import { _converse, api } from '@converse/headless';
-import { HEADLINES_TYPE } from '@converse/headless/shared/constants.js';
+import tplFeedsList from './templates/feeds-list.js';
+
+const { HEADLINES_TYPE } = constants;
 
 /**
  * Custom element which renders a list of headline feeds

+ 3 - 1
src/plugins/headlines-view/templates/feeds-list.js

@@ -1,6 +1,8 @@
 import { __ } from 'i18n';
 import { html } from "lit";
-import { HEADLINES_TYPE } from '@converse/headless/shared/constants';
+import { constants } from '@converse/headless';
+
+const { HEADLINES_TYPE } = constants;
 
 function tplHeadlinesFeedsListItem (el, feed) {
     const open_title = __('Click to open this server message');

+ 2 - 3
src/plugins/mam-views/placeholder.js

@@ -1,7 +1,6 @@
+import { api, u } from "@converse/headless";
 import { CustomElement } from 'shared/components/element.js';
 import tplPlaceholder from './templates/placeholder.js';
-import { api } from "@converse/headless";
-import { fetchArchivedMessages } from '@converse/headless/plugins/mam/utils.js';
 
 import './styles/placeholder.scss';
 
@@ -30,7 +29,7 @@ class Placeholder extends CustomElement {
             'before': this.model.get('before'),
             'start': this.model.get('start')
         }
-        await fetchArchivedMessages(this.model.collection.chatbox, options);
+        await u.mam.fetchArchivedMessages(this.model.collection.chatbox, options);
         this.model.destroy();
     }
 }

+ 5 - 6
src/plugins/mam-views/utils.js

@@ -1,8 +1,7 @@
-import MAMPlaceholderMessage from '@converse/headless/plugins/mam/placeholder.js';
-import { _converse, api, log } from '@converse/headless';
-import { fetchArchivedMessages } from '@converse/headless/plugins/mam/utils';
 import { html } from 'lit/html.js';
-import { CHATROOMS_TYPE } from '@converse/headless/shared/constants';
+import { _converse, api, log, constants, u, MAMPlaceholderMessage } from '@converse/headless';
+
+const { CHATROOMS_TYPE } = constants;
 
 
 export function getPlaceholderTemplate (message, tpl) {
@@ -27,9 +26,9 @@ export async function fetchMessagesOnScrollUp (view) {
             view.model.ui.set('chat-content-spinner-top', true);
             try {
                 if (stanza_id) {
-                    await fetchArchivedMessages(view.model, { 'before': stanza_id });
+                    await u.mam.fetchArchivedMessages(view.model, { 'before': stanza_id });
                 } else {
-                    await fetchArchivedMessages(view.model, { 'end': oldest_message.get('time') });
+                    await u.mam.fetchArchivedMessages(view.model, { 'end': oldest_message.get('time') });
                 }
             } catch (e) {
                 log.error(e);

+ 4 - 3
src/plugins/minimize/index.js

@@ -6,12 +6,11 @@
  * @typedef {import('@converse/headless').MUC} MUC
  * @typedef {import('@converse/headless').ChatBox} ChatBox
  */
+import debounce from 'lodash-es/debounce';
+import { _converse, api, converse, constants } from '@converse/headless';
 import './view.js';
 import './components/minimized-chat.js';
-import debounce from 'lodash-es/debounce';
 import MinimizedChatsToggle from './toggle.js';
-import { _converse, api, converse } from '@converse/headless';
-import { CHATROOMS_TYPE } from '@converse/headless/shared/constants.js';
 import {
     addMinimizeButtonToChat,
     addMinimizeButtonToMUC,
@@ -24,6 +23,8 @@ import {
 
 import './styles/minimize.scss';
 
+const { CHATROOMS_TYPE } = constants;
+
 
 converse.plugins.add('converse-minimize', {
     /* Optional dependencies are other plugins which might be

+ 2 - 2
src/plugins/minimize/utils.js

@@ -6,11 +6,11 @@
  * @typedef {import('plugins/controlbox/controlbox').default} ControlBoxView
  * @typedef {import('plugins/headlines-view/view').default} HeadlinesFeedView
  */
-import { _converse, api, converse, u } from '@converse/headless';
+import { _converse, api, converse, u, constants } from '@converse/headless';
 import { __ } from 'i18n';
-import { ACTIVE, INACTIVE } from '@converse/headless/shared/constants';
 
 const { dayjs } = converse.env;
+const { ACTIVE, INACTIVE } = constants;
 
 /**
  * @param { ChatBox|MUC } chat

+ 3 - 4
src/plugins/minimize/view.js

@@ -1,8 +1,7 @@
+import { _converse, api, u } from '@converse/headless';
+import { CustomElement } from 'shared/components/element';
 import MinimizedChatsToggle from './toggle.js';
 import tplChatsPanel from './templates/chats-panel.js';
-import { CustomElement } from 'shared/components/element';
-import { _converse, api } from '@converse/headless';
-import { initStorage } from '@converse/headless/utils/storage.js';
 
 
 export default class MinimizedChats extends CustomElement {
@@ -38,7 +37,7 @@ export default class MinimizedChats extends CustomElement {
         const bare_jid = _converse.session.get('bare_jid');
         const id = `converse.minchatstoggle-${bare_jid}`;
         this.minchats = new MinimizedChatsToggle({id});
-        initStorage(this.minchats, id, 'session');
+        u.initStorage(this.minchats, id, 'session');
         await new Promise(resolve => this.minchats.fetch({'success': resolve, 'error': resolve}));
     }
 

+ 2 - 3
src/plugins/muc-views/affiliation-form.js

@@ -1,8 +1,7 @@
 import tplAffiliationForm from './templates/affiliation-form.js';
 import { CustomElement } from 'shared/components/element';
 import { __ } from 'i18n';
-import { api, converse, log } from '@converse/headless';
-import { setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js';
+import { api, converse, log, u } from '@converse/headless';
 
 const { Strophe, sizzle } = converse.env;
 
@@ -45,7 +44,7 @@ class AffiliationForm extends CustomElement {
         };
         const muc_jid = this.muc.get('jid');
         try {
-            await setAffiliation(affiliation, muc_jid, [attrs]);
+            await u.muc.setAffiliation(affiliation, muc_jid, [attrs]);
         } catch (e) {
             if (e === null) {
                 this.alert(__('Timeout error while trying to set the affiliation'), 'danger');

+ 2 - 2
src/plugins/muc-views/index.js

@@ -3,15 +3,15 @@
  * @description XEP-0045 Multi-User Chat Views
  * @license Mozilla Public License (MPLv2)
  */
+import { api, converse, constants } from '@converse/headless';
 import '../chatboxviews/index.js';
 import './affiliation-form.js';
 import './role-form.js';
 import MUCView from './muc.js';
-import { api, converse } from '@converse/headless';
-import { CHATROOMS_TYPE } from '@converse/headless/shared/constants.js';
 import { clearHistory, confirmDirectMUCInvitation, parseMessageForMUCCommands } from './utils.js';
 
 const { Strophe } = converse.env;
+const { CHATROOMS_TYPE } = constants;
 
 import './styles/index.scss';
 

+ 9 - 11
src/plugins/muc-views/modals/muc-list.js

@@ -3,24 +3,23 @@ import tplMUCDescription from "../templates/muc-description.js";
 import tplMUCList from "../templates/muc-list.js";
 import tplSpinner from "templates/spinner.js";
 import { __ } from 'i18n';
-import { api, converse, log } from "@converse/headless";
-import { getAttributes } from '@converse/headless/shared/parsers';
+import { api, converse, log, u } from "@converse/headless";
 
 const { Strophe, $iq, sizzle } = converse.env;
-const u = converse.env.utils;
+const { getAttributes } = u;
 
 
-/* Insert groupchat info (based on returned #disco IQ stanza)
- * @function insertRoomInfo
- * @param { HTMLElement } el - The HTML DOM element that contains the info.
- * @param { Element } stanza - The IQ stanza containing the groupchat info.
+/**
+ * Insert groupchat info (based on returned #disco IQ stanza)
+ * @param {HTMLElement} el - The HTML DOM element that contains the info.
+ * @param {Element} stanza - The IQ stanza containing the groupchat info.
  */
 function insertRoomInfo (el, stanza) {
     // All MUC features found here: https://xmpp.org/registrar/disco-features.html
     el.querySelector('span.spinner').remove();
     el.querySelector('a.room-info').classList.add('selected');
     el.insertAdjacentHTML(
-        'beforeEnd',
+        'beforeend',
         u.getElementFromTemplateResult(tplMUCDescription({
             'jid': stanza.getAttribute('from'),
             'desc': sizzle('field[var="muc#roominfo_description"] value', stanza).shift()?.textContent,
@@ -42,8 +41,7 @@ function insertRoomInfo (el, stanza) {
 
 /**
  * Show/hide extra information about a groupchat in a listing.
- * @function toggleRoomInfo
- * @param { Event } ev
+ * @param {Event} ev
  */
 function toggleRoomInfo (ev) {
     const parent_el = u.ancestor(ev.target, '.room-item');
@@ -120,7 +118,7 @@ export default class MUCListModal extends BaseModal {
      * Handle the IQ stanza returned from the server, containing
      * all its public groupchats.
      * @method _converse.ChatRoomView#onRoomsFound
-     * @param { HTMLElement } [iq]
+     * @param {HTMLElement} [iq]
      */
     onRoomsFound (iq) {
         this.loading_items = false;

+ 10 - 14
src/plugins/muc-views/modtools.js

@@ -1,19 +1,16 @@
 /**
  * @typedef {module:muc-affiliations-utils.NonOutcastAffiliation} NonOutcastAffiliation
  */
-
-import tplModeratorTools from './templates/moderator-tools.js';
-import { AFFILIATIONS, ROLES } from '@converse/headless/plugins/muc/constants.js';
-import { CustomElement } from 'shared/components/element.js';
-import { __ } from 'i18n';
-import { api, converse } from '@converse/headless';
-import { getAffiliationList, getAssignableAffiliations } from '@converse/headless/plugins/muc/affiliations/utils.js';
-import { getAssignableRoles, getAutoFetchedAffiliationLists } from '@converse/headless/plugins/muc/utils.js';
 import { getOpenPromise } from '@converse/openpromise';
+import { api, converse, MUCOccupants, constants } from '@converse/headless';
+import { __ } from 'i18n';
+import { CustomElement } from 'shared/components/element.js';
+import tplModeratorTools from './templates/moderator-tools.js';
 
 import './styles/moderator-tools.scss';
 
-const { u } = converse.env;
+const { u } = converse.env
+const { AFFILIATIONS, ROLES } = constants;
 
 export default class ModeratorTools extends CustomElement {
     static get properties () {
@@ -74,8 +71,8 @@ export default class ModeratorTools extends CustomElement {
                 'affiliations_filter': this.affiliations_filter,
                 'alert_message': this.alert_message,
                 'alert_type': this.alert_type,
-                'assignable_affiliations': getAssignableAffiliations(occupant),
-                'assignable_roles': getAssignableRoles(occupant),
+                'assignable_affiliations': occupant.getAssignableAffiliations(),
+                'assignable_roles': occupant.getAssignableRoles(),
                 'filterAffiliationResults': ev => this.filterAffiliationResults(ev),
                 'filterRoleResults': ev => this.filterRoleResults(ev),
                 'loading_users_with_affiliation': this.loading_users_with_affiliation,
@@ -113,7 +110,7 @@ export default class ModeratorTools extends CustomElement {
         this.users_with_affiliation = null;
 
         if (this.shouldFetchAffiliationsList()) {
-            const result = await getAffiliationList(this.affiliation, this.jid);
+            const result = await api.rooms.affiliations.get(this.affiliation, this.jid);
             if (result instanceof Error) {
                 this.alert(result.message, 'danger');
                 this.users_with_affiliation = [];
@@ -140,7 +137,7 @@ export default class ModeratorTools extends CustomElement {
         if (affiliation === 'none') {
             return false;
         }
-        const auto_fetched_affs = getAutoFetchedAffiliationLists();
+        const auto_fetched_affs = MUCOccupants.getAutoFetchedAffiliationLists();
         if (auto_fetched_affs.includes(affiliation)) {
             return false;
         } else {
@@ -148,7 +145,6 @@ export default class ModeratorTools extends CustomElement {
         }
     }
 
-    // eslint-disable-next-line class-methods-use-this
     toggleForm (ev) {
         ev.stopPropagation();
         ev.preventDefault();

+ 3 - 4
src/plugins/muc-views/role-form.js

@@ -1,8 +1,7 @@
 import tplRoleForm from './templates/role-form.js';
 import { CustomElement } from 'shared/components/element.js';
 import { __ } from 'i18n';
-import { api, converse, log } from '@converse/headless';
-import { isErrorObject } from '@converse/headless/utils/index.js';
+import { api, converse, log, u } from '@converse/headless';
 
 const { Strophe, sizzle } = converse.env;
 
@@ -57,12 +56,12 @@ class RoleForm extends CustomElement {
                 this.dispatchEvent(event);
 
             },
-            e => {
+            (e) => {
                 if (sizzle(`not-allowed[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
                     this.alert(__("You're not allowed to make that change"), 'danger');
                 } else {
                     this.alert(__('Sorry, something went wrong while trying to set the role'), 'danger');
-                    if (isErrorObject(e)) log.error(e);
+                    if (u.isErrorObject(e)) log.error(e);
                 }
             }
         );

+ 4 - 6
src/plugins/muc-views/sidebar.js

@@ -1,15 +1,13 @@
+import debounce from 'lodash-es/debounce.js';
+import { _converse, api, u, RosterFilter } from "@converse/headless";
 import 'shared/autocomplete/index.js';
-import tplMUCSidebar from "./templates/muc-sidebar.js";
 import { CustomElement } from 'shared/components/element.js';
-import { _converse, api, converse } from "@converse/headless";
-import { RosterFilter } from '@converse/headless/plugins/roster/filter.js';
-import { initStorage } from "@converse/headless/utils/storage";
-import debounce from 'lodash-es/debounce.js';
+import tplMUCSidebar from "./templates/muc-sidebar.js";
 
 import 'shared/styles/status.scss';
 import './styles/muc-occupants.scss';
 
-const { u } = converse.env;
+const { initStorage } = u;
 
 export default class MUCSidebar extends CustomElement {
 

+ 1 - 2
src/plugins/muc-views/templates/affiliation-form.js

@@ -1,13 +1,12 @@
 import { __ } from 'i18n';
 import { html } from "lit";
-import { getAssignableAffiliations } from '@converse/headless/plugins/muc/affiliations/utils.js';
 
 export default (el) => {
     const i18n_change_affiliation = __('Change affiliation');
     const i18n_new_affiliation = __('New affiliation');
     const i18n_reason = __('Reason');
     const occupant = el.muc.getOwnOccupant();
-    const assignable_affiliations = getAssignableAffiliations(occupant);
+    const assignable_affiliations = occupant.getAssignableAffiliations();
 
     return html`
         <form class="affiliation-form" @submit=${ev => el.assignAffiliation(ev)}>

+ 4 - 2
src/plugins/muc-views/templates/muc-chatarea.js

@@ -1,9 +1,11 @@
+import { html } from "lit";
+import { constants } from '@converse/headless';
 import '../bottom-panel.js';
 import '../sidebar.js';
 import 'shared/chat/chat-content.js';
 import 'shared/chat/help-messages.js';
-import { CHATROOMS_TYPE } from '@converse/headless/shared/constants.js';
-import { html } from "lit";
+
+const { CHATROOMS_TYPE } = constants;
 
 export default (o) => html`
     <div class="chat-area">

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů