Selaa lähdekoodia

Fix type errors

JC Brand 1 vuosi sitten
vanhempi
commit
b8212a1567
100 muutettua tiedostoa jossa 1474 lisäystä ja 980 poistoa
  1. 1 1
      .eslintrc.json
  2. 13 6
      package-lock.json
  3. 1 0
      package.json
  4. 0 6
      src/entry.js
  5. 1 1
      src/headless/index.js
  6. 2 2
      src/headless/package.json
  7. 9 7
      src/headless/plugins/chat/api.js
  8. 26 16
      src/headless/plugins/chat/message.js
  9. 1 4
      src/headless/plugins/chat/messages.js
  10. 2 0
      src/headless/plugins/chat/model-with-contact.js
  11. 48 38
      src/headless/plugins/chat/model.js
  12. 10 8
      src/headless/plugins/chat/parsers.js
  13. 41 22
      src/headless/plugins/chat/utils.js
  14. 9 5
      src/headless/plugins/chatboxes/api.js
  15. 24 4
      src/headless/plugins/chatboxes/chatboxes.js
  16. 10 2
      src/headless/plugins/chatboxes/utils.js
  17. 33 28
      src/headless/plugins/disco/api.js
  18. 9 6
      src/headless/plugins/disco/entity.js
  19. 15 8
      src/headless/plugins/disco/index.js
  20. 33 20
      src/headless/plugins/disco/utils.js
  21. 3 1
      src/headless/plugins/emoji/index.js
  22. 24 7
      src/headless/plugins/emoji/utils.js
  23. 11 2
      src/headless/plugins/headlines/api.js
  24. 12 0
      src/headless/plugins/headlines/feed.js
  25. 3 7
      src/headless/plugins/headlines/index.js
  26. 1 1
      src/headless/plugins/headlines/utils.js
  27. 22 19
      src/headless/plugins/mam/api.js
  28. 3 1
      src/headless/plugins/mam/index.js
  29. 30 20
      src/headless/plugins/mam/utils.js
  30. 10 6
      src/headless/plugins/muc/affiliations/api.js
  31. 25 26
      src/headless/plugins/muc/affiliations/utils.js
  32. 25 20
      src/headless/plugins/muc/api.js
  33. 23 15
      src/headless/plugins/muc/index.js
  34. 9 13
      src/headless/plugins/muc/message.js
  35. 2 8
      src/headless/plugins/muc/messages.js
  36. 168 118
      src/headless/plugins/muc/muc.js
  37. 47 24
      src/headless/plugins/muc/occupants.js
  38. 42 40
      src/headless/plugins/muc/parsers.js
  39. 33 16
      src/headless/plugins/muc/utils.js
  40. 6 5
      src/headless/plugins/ping/api.js
  41. 1 1
      src/headless/plugins/ping/utils.js
  42. 4 3
      src/headless/plugins/pubsub.js
  43. 4 3
      src/headless/plugins/roster/api.js
  44. 6 6
      src/headless/plugins/roster/contact.js
  45. 27 21
      src/headless/plugins/roster/contacts.js
  46. 13 10
      src/headless/plugins/roster/index.js
  47. 68 47
      src/headless/plugins/roster/utils.js
  48. 19 7
      src/headless/plugins/smacks/tests/smacks.js
  49. 4 4
      src/headless/plugins/status/api.js
  50. 10 14
      src/headless/plugins/status/index.js
  51. 8 8
      src/headless/plugins/status/status.js
  52. 76 42
      src/headless/plugins/status/utils.js
  53. 9 6
      src/headless/plugins/vcard/api.js
  54. 3 2
      src/headless/plugins/vcard/index.js
  55. 47 28
      src/headless/plugins/vcard/utils.js
  56. 27 6
      src/headless/shared/_converse.js
  57. 7 7
      src/headless/shared/api/events.js
  58. 4 1
      src/headless/shared/api/presence.js
  59. 3 3
      src/headless/shared/api/promise.js
  60. 11 15
      src/headless/shared/api/public.js
  61. 9 6
      src/headless/shared/api/send.js
  62. 26 15
      src/headless/shared/chat/utils.js
  63. 1 1
      src/headless/shared/connection/api.js
  64. 1 1
      src/headless/shared/connection/feedback.js
  65. 47 25
      src/headless/shared/connection/index.js
  66. 10 1
      src/headless/shared/errors.js
  67. 1 1
      src/headless/shared/i18n.js
  68. 35 14
      src/headless/shared/parsers.js
  69. 5 6
      src/headless/shared/rsm.js
  70. 3 0
      src/headless/shared/settings/api.js
  71. 2 1
      src/headless/shared/settings/constants.js
  72. 9 7
      src/headless/shared/settings/utils.js
  73. 8 21
      src/headless/tests/converse.js
  74. 1 1
      src/headless/utils/arraybuffer.js
  75. 1 0
      src/headless/utils/index.js
  76. 31 26
      src/headless/utils/init.js
  77. 3 0
      src/headless/utils/jid.js
  78. 5 12
      src/headless/utils/session.js
  79. 5 3
      src/headless/utils/storage.js
  80. 5 2
      src/headless/utils/url.js
  81. 16 7
      src/plugins/adhoc-views/adhoc-commands.js
  82. 5 0
      src/plugins/bookmark-views/components/bookmark-form.js
  83. 5 0
      src/plugins/bookmark-views/modals/bookmark-form.js
  84. 3 1
      src/plugins/chatboxviews/index.js
  85. 9 13
      src/plugins/chatview/bottom-panel.js
  86. 20 13
      src/plugins/chatview/heading.js
  87. 23 11
      src/plugins/chatview/message-form.js
  88. 1 1
      src/plugins/chatview/tests/chatbox.js
  89. 2 2
      src/plugins/chatview/tests/receipts.js
  90. 8 3
      src/plugins/controlbox/api.js
  91. 2 2
      src/plugins/controlbox/controlbox.js
  92. 2 2
      src/plugins/controlbox/loginform.js
  93. 5 0
      src/plugins/controlbox/navback.js
  94. 1 1
      src/plugins/controlbox/templates/controlbox.js
  95. 3 2
      src/plugins/controlbox/templates/loginform.js
  96. 8 6
      src/plugins/controlbox/utils.js
  97. 5 5
      src/plugins/dragresize/index.js
  98. 5 0
      src/plugins/headlines-view/heading.js
  99. 0 1
      src/plugins/headlines-view/view.js
  100. 5 0
      src/plugins/mam-views/placeholder.js

+ 1 - 1
.eslintrc.json

@@ -184,7 +184,7 @@
         "no-underscore-dangle": "off",
         "no-underscore-dangle": "off",
         "no-unmodified-loop-condition": "error",
         "no-unmodified-loop-condition": "error",
         "no-unneeded-ternary": "off",
         "no-unneeded-ternary": "off",
-        "no-unused-vars": "error",
+        "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
         "no-unused-expressions": "off",
         "no-unused-expressions": "off",
         "no-use-before-define": "off",
         "no-use-before-define": "off",
         "no-useless-call": "error",
         "no-useless-call": "error",

+ 13 - 6
package-lock.json

@@ -33,6 +33,7 @@
         "@babel/core": "^7.18.5",
         "@babel/core": "^7.18.5",
         "@babel/preset-env": "^7.18.2",
         "@babel/preset-env": "^7.18.2",
         "@converse/headless": "file:src/headless",
         "@converse/headless": "file:src/headless",
+        "@types/webappsec-credential-management": "^0.6.8",
         "@typescript-eslint/eslint-plugin": "^5.48.0",
         "@typescript-eslint/eslint-plugin": "^5.48.0",
         "autoprefixer": "^10.4.5",
         "autoprefixer": "^10.4.5",
         "babel-loader": "^9.1.0",
         "babel-loader": "^9.1.0",
@@ -1834,8 +1835,8 @@
     },
     },
     "node_modules/@converse/skeletor": {
     "node_modules/@converse/skeletor": {
       "version": "0.0.8",
       "version": "0.0.8",
-      "resolved": "git+ssh://git@github.com/conversejs/skeletor.git#0cc6669bb2e5852caa1ef29892ce3822eadbff9a",
-      "integrity": "sha512-rnonEzuZPckk9OGv0WOTeB67suaDS0MIEWD/xg/SdXlZVfWEdHeAAasSUU5VQ+VcOdnx2axgVDK8UBq4qm6Y3A==",
+      "resolved": "git+ssh://git@github.com/conversejs/skeletor.git#d018e3524a5fb41b152ef9b27c6297253292946a",
+      "integrity": "sha512-HOx5ueepo4UrdJwIeVf/0dTNnzj/+jUNIbfKRUGh2nGEUu2Zg53DooLlUP/pSafHyhOFZyMBNxnyhIBaM9lHqg==",
       "license": "MIT",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "@converse/localforage-getitems": "1.4.3",
         "@converse/localforage-getitems": "1.4.3",
@@ -2409,6 +2410,12 @@
       "integrity": "sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ==",
       "integrity": "sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ==",
       "dev": true
       "dev": true
     },
     },
+    "node_modules/@types/webappsec-credential-management": {
+      "version": "0.6.8",
+      "resolved": "https://registry.npmjs.org/@types/webappsec-credential-management/-/webappsec-credential-management-0.6.8.tgz",
+      "integrity": "sha512-DES/SkK54U7AG8hmMkGCJkOSlywM3R+TzaWT+rBnX3lQTJ3K57jWr+UccWY8ImkuKekC9BjB+AH4zLJB4JKpvQ==",
+      "dev": true
+    },
     "node_modules/@types/ws": {
     "node_modules/@types/ws": {
       "version": "8.5.8",
       "version": "8.5.8",
       "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz",
       "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz",
@@ -10127,8 +10134,8 @@
     },
     },
     "node_modules/strophe.js": {
     "node_modules/strophe.js": {
       "version": "2.0.0",
       "version": "2.0.0",
-      "resolved": "git+ssh://git@github.com/strophe/strophejs.git#6b24a2a2121884b2d02aeb5756142f7dcaf05d9e",
-      "integrity": "sha512-YIK1PUyJEwZgiPk30cEtxhN5ifLzyLOnch9T6tw7812pO2yuaGdBEQcqGd8NQjH3LzdcoS/DZJnXFxj+QKyviQ==",
+      "resolved": "git+ssh://git@github.com/strophe/strophejs.git#a75895308216e81b28f58d276395a01b15c9e645",
+      "integrity": "sha512-bI975yewZrq7r3Xbxc79HJxN2MLcwAxYpBGmI7pizZT7q2R0gd0ylBJBGcvXu87TvzkoLROAEOR5P+VJ3Pkp2Q==",
       "license": "MIT",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "abab": "^2.0.3"
         "abab": "^2.0.3"
@@ -11419,7 +11426,7 @@
       "license": "MPL-2.0",
       "license": "MPL-2.0",
       "dependencies": {
       "dependencies": {
         "@converse/openpromise": "^0.0.1",
         "@converse/openpromise": "^0.0.1",
-        "@converse/skeletor": "conversejs/skeletor#0cc6669bb2e5852caa1ef29892ce3822eadbff9a",
+        "@converse/skeletor": "conversejs/skeletor#d018e3524a5fb41b152ef9b27c6297253292946a",
         "dayjs": "^1.11.8",
         "dayjs": "^1.11.8",
         "dompurify": "^2.3.1",
         "dompurify": "^2.3.1",
         "filesize": "^10.0.7",
         "filesize": "^10.0.7",
@@ -11428,7 +11435,7 @@
         "pluggable.js": "3.0.1",
         "pluggable.js": "3.0.1",
         "sizzle": "^2.3.5",
         "sizzle": "^2.3.5",
         "sprintf-js": "^1.1.2",
         "sprintf-js": "^1.1.2",
-        "strophe.js": "strophe/strophejs#6b24a2a2121884b2d02aeb5756142f7dcaf05d9e",
+        "strophe.js": "strophe/strophejs#a75895308216e81b28f58d276395a01b15c9e645",
         "urijs": "^1.19.10"
         "urijs": "^1.19.10"
       },
       },
       "devDependencies": {}
       "devDependencies": {}

+ 1 - 0
package.json

@@ -73,6 +73,7 @@
     "@babel/core": "^7.18.5",
     "@babel/core": "^7.18.5",
     "@babel/preset-env": "^7.18.2",
     "@babel/preset-env": "^7.18.2",
     "@converse/headless": "file:src/headless",
     "@converse/headless": "file:src/headless",
+    "@types/webappsec-credential-management": "^0.6.8",
     "@typescript-eslint/eslint-plugin": "^5.48.0",
     "@typescript-eslint/eslint-plugin": "^5.48.0",
     "autoprefixer": "^10.4.5",
     "autoprefixer": "^10.4.5",
     "babel-loader": "^9.1.0",
     "babel-loader": "^9.1.0",

+ 0 - 6
src/entry.js

@@ -12,9 +12,6 @@
 //
 //
 // Once the rest converse.js has been loaded, window.converse will be replaced
 // Once the rest converse.js has been loaded, window.converse will be replaced
 // with the full-fledged public API.
 // with the full-fledged public API.
-/**
- * @typedef {module:shared.converse.ConversePrivateGlobal} ConversePrivateGlobal
- */
 
 
 const plugins = {};
 const plugins = {};
 
 
@@ -68,9 +65,6 @@ const converse = {
     }
     }
 }
 }
 
 
-/**
- * @typedef {Window & {converse: ConversePrivateGlobal} } window
- */
 window['converse'] = converse;
 window['converse'] = converse;
 
 
 /**
 /**

+ 1 - 1
src/headless/index.js

@@ -17,7 +17,7 @@ dayjs.extend(advancedFormat);
  */
  */
 
 
 import "./plugins/bookmarks/index.js";  // XEP-0199 XMPP Ping
 import "./plugins/bookmarks/index.js";  // XEP-0199 XMPP Ping
-import "./plugins/bosh/index.js";             // XEP-0206 BOSH
+import "./plugins/bosh/index.js";       // XEP-0206 BOSH
 import "./plugins/caps/index.js";       // XEP-0115 Entity Capabilities
 import "./plugins/caps/index.js";       // XEP-0115 Entity Capabilities
 import "./plugins/chat/index.js";       // RFC-6121 Instant messaging
 import "./plugins/chat/index.js";       // RFC-6121 Instant messaging
 import "./plugins/chatboxes/index.js";
 import "./plugins/chatboxes/index.js";

+ 2 - 2
src/headless/package.json

@@ -32,7 +32,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@converse/openpromise": "^0.0.1",
     "@converse/openpromise": "^0.0.1",
-    "@converse/skeletor": "conversejs/skeletor#0cc6669bb2e5852caa1ef29892ce3822eadbff9a",
+    "@converse/skeletor": "conversejs/skeletor#d018e3524a5fb41b152ef9b27c6297253292946a",
     "dayjs": "^1.11.8",
     "dayjs": "^1.11.8",
     "dompurify": "^2.3.1",
     "dompurify": "^2.3.1",
     "filesize": "^10.0.7",
     "filesize": "^10.0.7",
@@ -41,7 +41,7 @@
     "pluggable.js": "3.0.1",
     "pluggable.js": "3.0.1",
     "sizzle": "^2.3.5",
     "sizzle": "^2.3.5",
     "sprintf-js": "^1.1.2",
     "sprintf-js": "^1.1.2",
-    "strophe.js": "strophe/strophejs#6b24a2a2121884b2d02aeb5756142f7dcaf05d9e",
+    "strophe.js": "strophe/strophejs#a75895308216e81b28f58d276395a01b15c9e645",
     "urijs": "^1.19.10"
     "urijs": "^1.19.10"
   },
   },
   "devDependencies": {}
   "devDependencies": {}

+ 9 - 7
src/headless/plugins/chat/api.js

@@ -18,7 +18,8 @@ export default {
         /**
         /**
          * @method api.chats.create
          * @method api.chats.create
          * @param {string|string[]} jids An jid or array of jids
          * @param {string|string[]} jids An jid or array of jids
-         * @param { object } [attrs] An object containing configuration attributes.
+         * @param {object} [attrs] An object containing configuration attributes.
+         * @returns {Promise<ChatBox|ChatBox[]>}
          */
          */
         async create (jids, attrs) {
         async create (jids, attrs) {
             if (typeof jids === 'string') {
             if (typeof jids === 'string') {
@@ -34,7 +35,7 @@ export default {
                 return chatbox;
                 return chatbox;
             }
             }
             if (Array.isArray(jids)) {
             if (Array.isArray(jids)) {
-                return Promise.all(jids.forEach(async jid => {
+                return Promise.all(jids.map(async jid => {
                     const contact = await api.contacts.get(jids);
                     const contact = await api.contacts.get(jids);
                     attrs.fullname = contact?.attributes?.fullname;
                     attrs.fullname = contact?.attributes?.fullname;
                     return api.chats.get(jid, attrs, true).maybeShow();
                     return api.chats.get(jid, attrs, true).maybeShow();
@@ -48,10 +49,10 @@ export default {
          * Opens a new one-on-one chat.
          * Opens a new one-on-one chat.
          *
          *
          * @method api.chats.open
          * @method api.chats.open
-         * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
-         * @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model.
-         * @param { Boolean } [attrs.minimized] - Should the chat be created in minimized state.
-         * @param { Boolean } [force=false] - By default, a minimized
+         * @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
+         * @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
+         * @param {Boolean} [attrs.minimized] - Should the chat be created in minimized state.
+         * @param {Boolean} [force=false] - By default, a minimized
          *   chat won't be maximized (in `overlayed` view mode) and in
          *   chat won't be maximized (in `overlayed` view mode) and in
          *   `fullscreen` view mode a newly opened chat won't replace
          *   `fullscreen` view mode a newly opened chat won't replace
          *   another chat already in the foreground.
          *   another chat already in the foreground.
@@ -109,7 +110,7 @@ export default {
          * @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
          * @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
          * @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model.
          * @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model.
          * @param { Boolean } [create=false] - Whether the chat should be created if it's not found.
          * @param { Boolean } [create=false] - Whether the chat should be created if it's not found.
-         * @returns { Promise<ChatBox> }
+         * @returns { Promise<ChatBox[]> }
          *
          *
          * @example
          * @example
          * // To return a single chat, provide the JID of the contact you're chatting with in that chat:
          * // To return a single chat, provide the JID of the contact you're chatting with in that chat:
@@ -127,6 +128,7 @@ export default {
         async get (jids, attrs={}, create=false) {
         async get (jids, attrs={}, create=false) {
             await api.waitUntil('chatBoxesFetched');
             await api.waitUntil('chatBoxesFetched');
 
 
+            /** @param {string} jid */
             async function _get (jid) {
             async function _get (jid) {
                 let model = await api.chatboxes.get(jid);
                 let model = await api.chatboxes.get(jid);
                 if (!model && create) {
                 if (!model && create) {

+ 26 - 16
src/headless/plugins/chat/message.js

@@ -1,9 +1,13 @@
+/**
+ * @typedef {import('@converse/skeletor').Model} Model
+ */
 import ModelWithContact from './model-with-contact.js';
 import ModelWithContact from './model-with-contact.js';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
 import log from '../../log.js';
 import log from '../../log.js';
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
+import { SUCCESS, FAILURE } from '../../shared/constants.js';
 
 
 const { Strophe, sizzle, u } = converse.env;
 const { Strophe, sizzle, u } = converse.env;
 
 
@@ -16,7 +20,7 @@ const { Strophe, sizzle, u } = converse.env;
  */
  */
 class Message extends ModelWithContact {
 class Message extends ModelWithContact {
 
 
-    defaults () { // eslint-disable-line class-methods-use-this
+    defaults () {
         return {
         return {
             'msgid': u.getUniqueId(),
             'msgid': u.getUniqueId(),
             'time': new Date().toISOString(),
             'time': new Date().toISOString(),
@@ -24,12 +28,19 @@ class Message extends ModelWithContact {
         };
         };
     }
     }
 
 
+    /**
+     * @param {Model[]} [models]
+     * @param {object} [options]
+     */
+    constructor (models, options) {
+        super(models, options);
+        this.file = null;
+    }
+
     async initialize () {
     async initialize () {
         super.initialize();
         super.initialize();
+        if (!this.checkValidity()) return;
 
 
-        if (!this.checkValidity()) {
-            return;
-        }
         this.initialized = getOpenPromise();
         this.initialized = getOpenPromise();
         if (this.get('file')) {
         if (this.get('file')) {
             this.on('change:put', () => this.uploadFile());
             this.on('change:put', () => this.uploadFile());
@@ -41,9 +52,9 @@ class Message extends ModelWithContact {
         await this.setContact();
         await this.setContact();
         this.setTimerForEphemeralMessage();
         this.setTimerForEphemeralMessage();
         /**
         /**
-         * Triggered once a {@link _converse.Message} has been created and initialized.
+         * Triggered once a {@link Message} has been created and initialized.
          * @event _converse#messageInitialized
          * @event _converse#messageInitialized
-         * @type { _converse.Message}
+         * @type {Message}
          * @example _converse.api.listen.on('messageInitialized', model => { ... });
          * @example _converse.api.listen.on('messageInitialized', model => { ... });
          */
          */
         await api.trigger('messageInitialized', this, { 'Synchronous': true });
         await api.trigger('messageInitialized', this, { 'Synchronous': true });
@@ -53,13 +64,12 @@ class Message extends ModelWithContact {
     setContact () {
     setContact () {
         if (['chat', 'normal'].includes(this.get('type'))) {
         if (['chat', 'normal'].includes(this.get('type'))) {
             ModelWithContact.prototype.initialize.apply(this, arguments);
             ModelWithContact.prototype.initialize.apply(this, arguments);
-            this.setRosterContact(Strophe.getBareJidFromJid(this.get('from')));
+            return this.setRosterContact(Strophe.getBareJidFromJid(this.get('from')));
         }
         }
     }
     }
 
 
     /**
     /**
      * Sets an auto-destruct timer for this message, if it's is_ephemeral.
      * Sets an auto-destruct timer for this message, if it's is_ephemeral.
-     * @private
      * @method _converse.Message#setTimerForEphemeralMessage
      * @method _converse.Message#setTimerForEphemeralMessage
      */
      */
     setTimerForEphemeralMessage () {
     setTimerForEphemeralMessage () {
@@ -179,12 +189,11 @@ class Message extends ModelWithContact {
      * @method _converse.Message#sendSlotRequestStanza
      * @method _converse.Message#sendSlotRequestStanza
      */
      */
     sendSlotRequestStanza () {
     sendSlotRequestStanza () {
-        if (!this.file) {
-            return Promise.reject(new Error('file is undefined'));
-        }
+        if (!this.file) return Promise.reject(new Error('file is undefined'));
+
         const iq = converse.env
         const iq = converse.env
             .$iq({
             .$iq({
-                'from': _converse.jid,
+                'from': _converse.session.get('jid'),
                 'to': this.get('slot_request_url'),
                 'to': this.get('slot_request_url'),
                 'type': 'get'
                 'type': 'get'
             })
             })
@@ -241,12 +250,12 @@ class Message extends ModelWithContact {
     uploadFile () {
     uploadFile () {
         const xhr = new XMLHttpRequest();
         const xhr = new XMLHttpRequest();
 
 
-        xhr.onreadystatechange = async () => {
+        xhr.onreadystatechange = async (event) => {
             if (xhr.readyState === XMLHttpRequest.DONE) {
             if (xhr.readyState === XMLHttpRequest.DONE) {
                 log.info('Status: ' + xhr.status);
                 log.info('Status: ' + xhr.status);
                 if (xhr.status === 200 || xhr.status === 201) {
                 if (xhr.status === 200 || xhr.status === 201) {
                     let attrs = {
                     let attrs = {
-                        'upload': _converse.SUCCESS,
+                        'upload': SUCCESS,
                         'oob_url': this.get('get'),
                         'oob_url': this.get('get'),
                         'message': this.get('get'),
                         'message': this.get('get'),
                         'body': this.get('get'),
                         'body': this.get('get'),
@@ -259,7 +268,8 @@ class Message extends ModelWithContact {
                     attrs = await api.hook('afterFileUploaded', this, attrs);
                     attrs = await api.hook('afterFileUploaded', this, attrs);
                     this.save(attrs);
                     this.save(attrs);
                 } else {
                 } else {
-                    xhr.onerror();
+                    log.error(event);
+                    xhr.onerror(new ProgressEvent(`Response status: ${xhr.status}`));
                 }
                 }
             }
             }
         };
         };
@@ -287,7 +297,7 @@ class Message extends ModelWithContact {
             }
             }
             this.save({
             this.save({
                 'type': 'error',
                 'type': 'error',
-                'upload': _converse.FAILURE,
+                'upload': FAILURE,
                 'message': message,
                 'message': message,
                 'is_ephemeral': true
                 'is_ephemeral': true
             });
             });

+ 1 - 4
src/headless/plugins/chat/messages.js

@@ -3,12 +3,9 @@ import { Collection } from '@converse/skeletor';
 
 
 class Messages extends Collection {
 class Messages extends Collection {
 
 
-    get comparator () {
-        return 'time';
-    }
-
     constructor () {
     constructor () {
         super();
         super();
+        this.comparator = 'time';
         this.model = Message;
         this.model = Message;
         this.fetched = null;
         this.fetched = null;
         this.chatbox = null;
         this.chatbox = null;

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

@@ -7,6 +7,8 @@ class ModelWithContact extends Model {
     initialize () {
     initialize () {
         super.initialize();
         super.initialize();
         this.rosterContactAdded = getOpenPromise();
         this.rosterContactAdded = getOpenPromise();
+        this.contact = null;
+        this.vcard = null;
     }
     }
 
 
     /**
     /**

+ 48 - 38
src/headless/plugins/chat/model.js

@@ -12,7 +12,7 @@ import isMatch from "lodash-es/isMatch";
 import log from '../../log.js';
 import log from '../../log.js';
 import pick from "lodash-es/pick";
 import pick from "lodash-es/pick";
 import { Model } from '@converse/skeletor';
 import { Model } from '@converse/skeletor';
-import { PRIVATE_CHAT_TYPE, COMPOSING, INACTIVE, PAUSED, GONE } from '@converse/headless/shared/constants.js';
+import { ACTIVE, PRIVATE_CHAT_TYPE, COMPOSING, INACTIVE, PAUSED, SUCCESS, GONE } from '@converse/headless/shared/constants.js';
 import { TimeoutError } from '../../shared/errors.js';
 import { TimeoutError } from '../../shared/errors.js';
 import { debouncedPruneHistory, handleCorrection } from '../../shared/chat/utils.js';
 import { debouncedPruneHistory, handleCorrection } from '../../shared/chat/utils.js';
 import { filesize } from "filesize";
 import { filesize } from "filesize";
@@ -38,18 +38,20 @@ class ChatBox extends ModelWithContact {
     defaults () {
     defaults () {
         return {
         return {
             'bookmarked': false,
             'bookmarked': false,
-            'chat_state': undefined,
             'hidden': isUniView() && !api.settings.get('singleton'),
             'hidden': isUniView() && !api.settings.get('singleton'),
             'message_type': 'chat',
             'message_type': 'chat',
-            'nickname': undefined,
             'num_unread': 0,
             'num_unread': 0,
             'time_opened': this.get('time_opened') || (new Date()).getTime(),
             'time_opened': this.get('time_opened') || (new Date()).getTime(),
             'time_sent': (new Date(0)).toISOString(),
             'time_sent': (new Date(0)).toISOString(),
             'type': PRIVATE_CHAT_TYPE,
             'type': PRIVATE_CHAT_TYPE,
-            'url': ''
         }
         }
     }
     }
 
 
+    constructor (attrs, options) {
+        super(attrs, options);
+        this.disable_mam = false;
+    }
+
     async initialize () {
     async initialize () {
         super.initialize();
         super.initialize();
         this.initialized = getOpenPromise();
         this.initialized = getOpenPromise();
@@ -70,7 +72,8 @@ class ChatBox extends ModelWithContact {
         this.initMessages();
         this.initMessages();
 
 
         if (this.get('type') === PRIVATE_CHAT_TYPE) {
         if (this.get('type') === PRIVATE_CHAT_TYPE) {
-            this.presence = _converse.presences.get(jid) || _converse.presences.create({ jid });
+            const { presences } = _converse.state;
+            this.presence = presences.get(jid) || presences.create({ jid });
             await this.setRosterContact(jid);
             await this.setRosterContact(jid);
             this.presence.on('change:show', item => this.onPresenceChanged(item));
             this.presence.on('change:show', item => this.onPresenceChanged(item));
         }
         }
@@ -89,11 +92,11 @@ class ChatBox extends ModelWithContact {
     }
     }
 
 
     getMessagesCollection () {
     getMessagesCollection () {
-        return new _converse.Messages();
+        return new _converse.exports.Messages();
     }
     }
 
 
     getMessagesCacheKey () {
     getMessagesCacheKey () {
-        return `converse.messages-${this.get('jid')}-${_converse.bare_jid}`;
+        return `converse.messages-${this.get('jid')}-${_converse.session.get('bare_jid')}`;
     }
     }
 
 
     initMessages () {
     initMessages () {
@@ -148,7 +151,7 @@ class ChatBox extends ModelWithContact {
         const resolve = this.messages.fetched.resolve;
         const resolve = this.messages.fetched.resolve;
         this.messages.fetch({
         this.messages.fetch({
             'add': true,
             'add': true,
-            'success': msgs => { this.afterMessagesFetched(msgs); resolve() },
+            'success': () => { this.afterMessagesFetched(); resolve() },
             'error': () => { this.afterMessagesFetched(); resolve() }
             'error': () => { this.afterMessagesFetched(); resolve() }
         });
         });
         return this.messages.fetched;
         return this.messages.fetched;
@@ -156,7 +159,7 @@ class ChatBox extends ModelWithContact {
 
 
     async handleErrorMessageStanza (stanza) {
     async handleErrorMessageStanza (stanza) {
         const { __ } = _converse;
         const { __ } = _converse;
-        const attrs = await parseMessage(stanza, _converse);
+        const attrs = await parseMessage(stanza);
         if (!await this.shouldShowErrorMessage(attrs)) {
         if (!await this.shouldShowErrorMessage(attrs)) {
             return;
             return;
         }
         }
@@ -195,9 +198,8 @@ class ChatBox extends ModelWithContact {
     /**
     /**
      * Queue an incoming `chat` message stanza for processing.
      * Queue an incoming `chat` message stanza for processing.
      * @async
      * @async
-     * @private
      * @method ChatBox#queueMessage
      * @method ChatBox#queueMessage
-     * @param { Promise<MessageAttributes> } attrs - A promise which resolves to the message attributes
+     * @param {MessageAttributes} attrs - A promise which resolves to the message attributes
      */
      */
     queueMessage (attrs) {
     queueMessage (attrs) {
         this.msg_chain = (this.msg_chain || this.messages.fetched)
         this.msg_chain = (this.msg_chain || this.messages.fetched)
@@ -208,12 +210,11 @@ class ChatBox extends ModelWithContact {
 
 
     /**
     /**
      * @async
      * @async
-     * @private
      * @method ChatBox#onMessage
      * @method ChatBox#onMessage
-     * @param { MessageAttributes } attrs_promse - A promise which resolves to the message attributes.
+     * @param {Promise<MessageAttributes>} attrs_promise - A promise which resolves to the message attributes.
      */
      */
-    async onMessage (attrs) {
-        attrs = await attrs;
+    async onMessage (attrs_promise) {
+        const attrs = await attrs_promise;
         if (u.isErrorObject(attrs)) {
         if (u.isErrorObject(attrs)) {
             attrs.stanza && log.error(attrs.stanza);
             attrs.stanza && log.error(attrs.stanza);
             return log.error(attrs.message);
             return log.error(attrs.message);
@@ -240,7 +241,7 @@ class ChatBox extends ModelWithContact {
     }
     }
 
 
     async onMessageUploadChanged (message) {
     async onMessageUploadChanged (message) {
-        if (message.get('upload') === _converse.SUCCESS) {
+        if (message.get('upload') === SUCCESS) {
             const attrs = {
             const attrs = {
                 'body': message.get('body'),
                 'body': message.get('body'),
                 'spoiler_hint': message.get('spoiler_hint'),
                 'spoiler_hint': message.get('spoiler_hint'),
@@ -278,7 +279,7 @@ class ChatBox extends ModelWithContact {
         if (api.connection.connected()) {
         if (api.connection.connected()) {
             // Immediately sending the chat state, because the
             // Immediately sending the chat state, because the
             // model is going to be destroyed afterwards.
             // model is going to be destroyed afterwards.
-            this.setChatState(_converse.INACTIVE);
+            this.setChatState(INACTIVE);
             this.sendChatState();
             this.sendChatState();
         }
         }
         try {
         try {
@@ -507,7 +508,7 @@ class ChatBox extends ModelWithContact {
     /**
     /**
      * Given an error `<message>` stanza's attributes, find the saved message model which is
      * Given an error `<message>` stanza's attributes, find the saved message model which is
      * referenced by that error.
      * referenced by that error.
-     * @param { Object } attrs
+     * @param {object} attrs
      */
      */
     getMessageReferencedByError (attrs) {
     getMessageReferencedByError (attrs) {
         const id = attrs.msgid;
         const id = attrs.msgid;
@@ -515,9 +516,9 @@ class ChatBox extends ModelWithContact {
     }
     }
 
 
     /**
     /**
-     * @private
      * @method ChatBox#shouldShowErrorMessage
      * @method ChatBox#shouldShowErrorMessage
-     * @returns {boolean}
+     * @param {object} attrs
+     * @returns {Promise<boolean>}
      */
      */
     shouldShowErrorMessage (attrs) {
     shouldShowErrorMessage (attrs) {
         const msg = this.getMessageReferencedByError(attrs);
         const msg = this.getMessageReferencedByError(attrs);
@@ -529,9 +530,14 @@ class ChatBox extends ModelWithContact {
             return;
             return;
         }
         }
         // Gets overridden in MUC
         // Gets overridden in MUC
-        return true;
+        // Return promise because subclasses need to return promises
+        return Promise.resolve(true);
     }
     }
 
 
+    /**
+     * @param {string} jid1
+     * @param {string} jid2
+     */
     isSameUser (jid1, jid2) {
     isSameUser (jid1, jid2) {
         return u.isSameBareJID(jid1, jid2);
         return u.isSameBareJID(jid1, jid2);
     }
     }
@@ -568,11 +574,10 @@ class ChatBox extends ModelWithContact {
 
 
     /**
     /**
      * Handles message retraction based on the passed in attributes.
      * Handles message retraction based on the passed in attributes.
-     * @private
      * @method ChatBox#handleRetraction
      * @method ChatBox#handleRetraction
-     * @param { object } attrs - Attributes representing a received
+     * @param {object} attrs - Attributes representing a received
      *  message, as returned by {@link parseMessage}
      *  message, as returned by {@link parseMessage}
-     * @returns { Boolean } Returns `true` or `false` depending on
+     * @returns {Promise<Boolean>} Returns `true` or `false` depending on
      *  whether a message was retracted or not.
      *  whether a message was retracted or not.
      */
      */
     async handleRetraction (attrs) {
     async handleRetraction (attrs) {
@@ -606,11 +611,10 @@ class ChatBox extends ModelWithContact {
     /**
     /**
      * Returns an already cached message (if it exists) based on the
      * Returns an already cached message (if it exists) based on the
      * passed in attributes map.
      * passed in attributes map.
-     * @private
      * @method ChatBox#getDuplicateMessage
      * @method ChatBox#getDuplicateMessage
      * @param {object} attrs - Attributes representing a received
      * @param {object} attrs - Attributes representing a received
      *  message, as returned by {@link parseMessage}
      *  message, as returned by {@link parseMessage}
-     * @returns {Promise<Message>}
+     * @returns {Message}
      */
      */
     getDuplicateMessage (attrs) {
     getDuplicateMessage (attrs) {
         const queries = [
         const queries = [
@@ -654,7 +658,6 @@ class ChatBox extends ModelWithContact {
 
 
     /**
     /**
      * Retract one of your messages in this chat
      * Retract one of your messages in this chat
-     * @private
      * @method ChatBoxView#retractOwnMessage
      * @method ChatBoxView#retractOwnMessage
      * @param { Message } message - The message which we're retracting.
      * @param { Message } message - The message which we're retracting.
      */
      */
@@ -725,7 +728,7 @@ class ChatBox extends ModelWithContact {
 
 
     handleChatMarker (attrs) {
     handleChatMarker (attrs) {
         const to_bare_jid = Strophe.getBareJidFromJid(attrs.to);
         const to_bare_jid = Strophe.getBareJidFromJid(attrs.to);
-        if (to_bare_jid !== _converse.bare_jid) {
+        if (to_bare_jid !== _converse.session.get('bare_jid')) {
             return false;
             return false;
         }
         }
         if (attrs.is_markable) {
         if (attrs.is_markable) {
@@ -782,7 +785,7 @@ class ChatBox extends ModelWithContact {
                 'type': this.get('message_type'),
                 'type': this.get('message_type'),
                 'id': message.get('edited') && u.getUniqueId() || message.get('msgid'),
                 'id': message.get('edited') && u.getUniqueId() || message.get('msgid'),
             }).c('body').t(message.get('body')).up()
             }).c('body').t(message.get('body')).up()
-              .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).root();
+              .c(ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).root();
 
 
         if (message.get('type') === 'chat') {
         if (message.get('type') === 'chat') {
             stanza.c('request', {'xmlns': Strophe.NS.RECEIPTS}).root();
             stanza.c('request', {'xmlns': Strophe.NS.RECEIPTS}).root();
@@ -849,8 +852,8 @@ class ChatBox extends ModelWithContact {
         const text = attrs?.body;
         const text = attrs?.body;
         const body = text ? u.shortnamesToUnicode(text) : undefined;
         const body = text ? u.shortnamesToUnicode(text) : undefined;
         attrs = Object.assign({}, attrs, {
         attrs = Object.assign({}, attrs, {
-            'from': _converse.bare_jid,
-            'fullname': _converse.xmppstatus.get('fullname'),
+            'from': _converse.session.get('bare_jid'),
+            'fullname': _converse.state.xmppstatus.get('fullname'),
             'id': origin_id,
             'id': origin_id,
             'is_only_emojis': text ? u.isOnlyEmojis(text) : false,
             'is_only_emojis': text ? u.isOnlyEmojis(text) : false,
             'jid': this.get('jid'),
             'jid': this.get('jid'),
@@ -975,7 +978,6 @@ class ChatBox extends ModelWithContact {
     /**
     /**
      * Sends a message with the current XEP-0085 chat state of the user
      * Sends a message with the current XEP-0085 chat state of the user
      * as taken from the `chat_state` attribute of the {@link ChatBox}.
      * as taken from the `chat_state` attribute of the {@link ChatBox}.
-     * @private
      * @method ChatBox#sendChatState
      * @method ChatBox#sendChatState
      */
      */
     sendChatState () {
     sendChatState () {
@@ -997,9 +999,12 @@ class ChatBox extends ModelWithContact {
     }
     }
 
 
 
 
+    /**
+     * @param {File[]} files
+     */
     async sendFiles (files) {
     async sendFiles (files) {
-        const { __ } = _converse;
-        const result = await api.disco.features.get(Strophe.NS.HTTPUPLOAD, _converse.domain);
+        const { __, session } = _converse;
+        const result = await api.disco.features.get(Strophe.NS.HTTPUPLOAD, session.get('domain'));
         const item = result.pop();
         const item = result.pop();
         if (!item) {
         if (!item) {
             this.createMessage({
             this.createMessage({
@@ -1031,7 +1036,7 @@ class ChatBox extends ModelWithContact {
              */
              */
             file = await api.hook('beforeFileUpload', this, file);
             file = await api.hook('beforeFileUpload', this, file);
 
 
-            if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) {
+            if (!window.isNaN(max_file_size) && file.size > max_file_size) {
                 return this.createMessage({
                 return this.createMessage({
                     'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
                     'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
                         file.name, filesize(max_file_size)),
                         file.name, filesize(max_file_size)),
@@ -1054,12 +1059,15 @@ class ChatBox extends ModelWithContact {
         });
         });
     }
     }
 
 
+    /**
+     * @param {boolean} force
+     */
     maybeShow (force) {
     maybeShow (force) {
         if (isUniView()) {
         if (isUniView()) {
-            const filter = c => !c.get('hidden') &&
+            const filter = (c) => !c.get('hidden') &&
                 c.get('jid') !== this.get('jid') &&
                 c.get('jid') !== this.get('jid') &&
                 c.get('id') !== 'controlbox';
                 c.get('id') !== 'controlbox';
-            const other_chats = _converse.chatboxes.filter(filter);
+            const other_chats = _converse.state.chatboxes.filter(filter);
             if (force || other_chats.length === 0) {
             if (force || other_chats.length === 0) {
                 // We only have one chat visible at any one time.
                 // We only have one chat visible at any one time.
                 // So before opening a chat, we make sure all other chats are hidden.
                 // So before opening a chat, we make sure all other chats are hidden.
@@ -1086,7 +1094,6 @@ class ChatBox extends ModelWithContact {
     /**
     /**
      * Given a newly received {@link Message} instance,
      * Given a newly received {@link Message} instance,
      * update the unread counter if necessary.
      * update the unread counter if necessary.
-     * @private
      * @method ChatBox#handleUnreadMessage
      * @method ChatBox#handleUnreadMessage
      * @param {Message} message
      * @param {Message} message
      */
      */
@@ -1109,6 +1116,9 @@ class ChatBox extends ModelWithContact {
         }
         }
     }
     }
 
 
+    /**
+     * @param {Message} message
+     */
     incrementUnreadMsgsCounter (message) {
     incrementUnreadMsgsCounter (message) {
         const settings = {
         const settings = {
             'num_unread': this.get('num_unread') + 1
             'num_unread': this.get('num_unread') + 1

+ 10 - 8
src/headless/plugins/chat/parsers.js

@@ -45,17 +45,19 @@ export async function parseMessage (stanza) {
 
 
     let to_jid = stanza.getAttribute('to');
     let to_jid = stanza.getAttribute('to');
     const to_resource = Strophe.getResourceFromJid(to_jid);
     const to_resource = Strophe.getResourceFromJid(to_jid);
-    if (api.settings.get('filter_by_resource') && to_resource && to_resource !== _converse.resource) {
+    const resource = _converse.session.get('resource');
+    if (api.settings.get('filter_by_resource') && to_resource && to_resource !== resource) {
         return new StanzaParseError(
         return new StanzaParseError(
             `Ignoring incoming message intended for a different resource: ${to_jid}`,
             `Ignoring incoming message intended for a different resource: ${to_jid}`,
             stanza
             stanza
         );
         );
     }
     }
 
 
+    const bare_jid = _converse.session.get('bare_jid');
     const original_stanza = stanza;
     const original_stanza = stanza;
-    let from_jid = stanza.getAttribute('from') || _converse.bare_jid;
+    let from_jid = stanza.getAttribute('from') || bare_jid;
     if (isCarbon(stanza)) {
     if (isCarbon(stanza)) {
-        if (from_jid === _converse.bare_jid) {
+        if (from_jid === bare_jid) {
             const selector = `[xmlns="${Strophe.NS.CARBONS}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
             const selector = `[xmlns="${Strophe.NS.CARBONS}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
             stanza = sizzle(selector, stanza).pop();
             stanza = sizzle(selector, stanza).pop();
             to_jid = stanza.getAttribute('to');
             to_jid = stanza.getAttribute('to');
@@ -69,7 +71,7 @@ export async function parseMessage (stanza) {
 
 
     const is_archived = isArchived(stanza);
     const is_archived = isArchived(stanza);
     if (is_archived) {
     if (is_archived) {
-        if (from_jid === _converse.bare_jid) {
+        if (from_jid === bare_jid) {
             const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
             const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
             stanza = sizzle(selector, stanza).pop();
             stanza = sizzle(selector, stanza).pop();
             to_jid = stanza.getAttribute('to');
             to_jid = stanza.getAttribute('to');
@@ -83,7 +85,7 @@ export async function parseMessage (stanza) {
     }
     }
 
 
     const from_bare_jid = Strophe.getBareJidFromJid(from_jid);
     const from_bare_jid = Strophe.getBareJidFromJid(from_jid);
-    const is_me = from_bare_jid === _converse.bare_jid;
+    const is_me = from_bare_jid === bare_jid;
     if (is_me && to_jid === null) {
     if (is_me && to_jid === null) {
         return new StanzaParseError(
         return new StanzaParseError(
             `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`,
             `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`,
@@ -107,7 +109,7 @@ export async function parseMessage (stanza) {
     }
     }
     /**
     /**
      * The object which {@link parseMessage} returns
      * The object which {@link parseMessage} returns
-     * @typedef {Object} module:plugin-chat-parsers.MessageAttributes
+     * @typedef {Object} MessageAttributes
      * @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else
      * @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else
      * @property { Array<Object> } references - A list of objects representing XEP-0372 references
      * @property { Array<Object> } references - A list of objects representing XEP-0372 references
      * @property { Boolean } editable - Is this message editable via XEP-0308?
      * @property { Boolean } editable - Is this message editable via XEP-0308?
@@ -190,12 +192,12 @@ export async function parseMessage (stanza) {
         getCorrectionAttributes(stanza, original_stanza),
         getCorrectionAttributes(stanza, original_stanza),
         getStanzaIDs(stanza, original_stanza),
         getStanzaIDs(stanza, original_stanza),
         getRetractionAttributes(stanza, original_stanza),
         getRetractionAttributes(stanza, original_stanza),
-        getEncryptionAttributes(stanza, _converse)
+        getEncryptionAttributes(stanza)
     );
     );
 
 
     if (attrs.is_archived) {
     if (attrs.is_archived) {
         const from = original_stanza.getAttribute('from');
         const from = original_stanza.getAttribute('from');
-        if (from && from !== _converse.bare_jid) {
+        if (from && from !== bare_jid) {
             return new StanzaParseError(`Invalid Stanza: Forged MAM message from ${from}`, stanza);
             return new StanzaParseError(`Invalid Stanza: Forged MAM message from ${from}`, stanza);
         }
         }
     }
     }

+ 41 - 22
src/headless/plugins/chat/utils.js

@@ -1,3 +1,9 @@
+/**
+ * @module:headless-plugins-chat-utils
+ * @typedef {import('./model.js').default} ChatBox
+ * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes
+ * @typedef {import('strophe.js').Builder} Builder
+ */
 import sizzle from "sizzle";
 import sizzle from "sizzle";
 import { Model } from '@converse/skeletor';
 import { Model } from '@converse/skeletor';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
@@ -24,18 +30,23 @@ export function routeToChat (event) {
 
 
 export async function onClearSession () {
 export async function onClearSession () {
     if (shouldClearCache()) {
     if (shouldClearCache()) {
+        const { chatboxes } = _converse.state;
         await Promise.all(
         await Promise.all(
-            _converse.chatboxes.map(c => c.messages && c.messages.clearStore({ 'silent': true }))
+            chatboxes.map(/** @param {ChatBox} c */(c) => c.messages?.clearStore({ 'silent': true }))
         );
         );
-        const filter = o => o.get('type') !== CONTROLBOX_TYPE;
-        _converse.chatboxes.clearStore({ 'silent': true }, filter);
+        chatboxes.clearStore(
+            { 'silent': true },
+            /** @param {Model} o */(o) => o.get('type') !== CONTROLBOX_TYPE);
     }
     }
 }
 }
 
 
+
+/**
+ * Given a stanza, determine whether it's a new
+ * message, i.e. not a MAM archived one.
+ * @param {Element|Model|object} message
+ */
 export function isNewMessage (message) {
 export function isNewMessage (message) {
-    /* Given a stanza, determine whether it's a new
-     * message, i.e. not a MAM archived one.
-     */
     if (message instanceof Element) {
     if (message instanceof Element) {
         return !(
         return !(
             sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length &&
             sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length &&
@@ -48,9 +59,13 @@ export function isNewMessage (message) {
 }
 }
 
 
 
 
+/**
+ * @param {Element} stanza
+ */
 async function handleErrorMessage (stanza) {
 async function handleErrorMessage (stanza) {
     const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
     const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
-    if (u.isSameBareJID(from_jid, _converse.bare_jid)) {
+    const bare_jid = _converse.session.get('bare_jid');
+    if (u.isSameBareJID(from_jid, bare_jid)) {
         return;
         return;
     }
     }
     const chatbox = await api.chatboxes.get(from_jid);
     const chatbox = await api.chatboxes.get(from_jid);
@@ -62,8 +77,8 @@ async function handleErrorMessage (stanza) {
 export function autoJoinChats () {
 export function autoJoinChats () {
     // Automatically join private chats, based on the
     // Automatically join private chats, based on the
     // "auto_join_private_chats" configuration setting.
     // "auto_join_private_chats" configuration setting.
-    api.settings.get('auto_join_private_chats').forEach(jid => {
-        if (_converse.chatboxes.where({ 'jid': jid }).length) {
+    api.settings.get('auto_join_private_chats').forEach(/** @param {string} jid */(jid) => {
+        if (_converse.state.chatboxes.where({ 'jid': jid }).length) {
             return;
             return;
         }
         }
         if (typeof jid === 'string') {
         if (typeof jid === 'string') {
@@ -85,7 +100,8 @@ export function autoJoinChats () {
 
 
 export function registerMessageHandlers () {
 export function registerMessageHandlers () {
     api.connection.get().addHandler(
     api.connection.get().addHandler(
-        stanza => {
+        /** @param {Element} stanza */
+        (stanza) => {
             if (
             if (
                 ['groupchat', 'error'].includes(stanza.getAttribute('type')) ||
                 ['groupchat', 'error'].includes(stanza.getAttribute('type')) ||
                 isHeadline(stanza) ||
                 isHeadline(stanza) ||
@@ -94,14 +110,18 @@ export function registerMessageHandlers () {
             ) {
             ) {
                 return true;
                 return true;
             }
             }
-            return _converse.handleMessageStanza(stanza) || true;
+            return _converse.exports.handleMessageStanza(stanza) || true;
         },
         },
         null,
         null,
         'message',
         'message',
     );
     );
 
 
     api.connection.get().addHandler(
     api.connection.get().addHandler(
-        stanza => handleErrorMessage(stanza) || true,
+        /** @param {Element} stanza */
+        (stanza) => {
+            handleErrorMessage(stanza);
+            return true;
+        },
         null,
         null,
         'message',
         'message',
         'error'
         'error'
@@ -111,10 +131,10 @@ export function registerMessageHandlers () {
 
 
 /**
 /**
  * Handler method for all incoming single-user chat "message" stanzas.
  * Handler method for all incoming single-user chat "message" stanzas.
- * @param { MessageAttributes } attrs - The message attributes
+ * @param {Element|Builder} stanza
  */
  */
 export async function handleMessageStanza (stanza) {
 export async function handleMessageStanza (stanza) {
-    stanza = stanza.tree?.() ?? stanza;
+    stanza = (stanza instanceof Element) ? stanza : stanza.tree();
 
 
     if (isServerMessage(stanza)) {
     if (isServerMessage(stanza)) {
         // Prosody sends headline messages with type `chat`, so we need to filter them out here.
         // Prosody sends headline messages with type `chat`, so we need to filter them out here.
@@ -136,19 +156,18 @@ export async function handleMessageStanza (stanza) {
     const chatbox = await api.chats.get(attrs.contact_jid, { 'nickname': attrs.nick }, has_body);
     const chatbox = await api.chats.get(attrs.contact_jid, { 'nickname': attrs.nick }, has_body);
     await chatbox?.queueMessage(attrs);
     await chatbox?.queueMessage(attrs);
     /**
     /**
-     * @typedef { Object } MessageData
+     * @typedef {Object} MessageData
      * An object containing the original message stanza, as well as the
      * An object containing the original message stanza, as well as the
      * parsed attributes.
      * parsed attributes.
-     * @property { Element } stanza
-     * @property { MessageAttributes } stanza
-     * @property { ChatBox } chatbox
+     * @property {Element} stanza
+     * @property {MessageAttributes} stanza
+     * @property {ChatBox} chatbox
      */
      */
     const data = { stanza, attrs, chatbox };
     const data = { stanza, attrs, chatbox };
     /**
     /**
      * Triggered when a message stanza is been received and processed.
      * Triggered when a message stanza is been received and processed.
      * @event _converse#message
      * @event _converse#message
-     * @type { object }
-     * @property { module:converse-chat~MessageData } data
+     * @type {MessageData} data
      */
      */
     api.trigger('message', data);
     api.trigger('message', data);
 }
 }
@@ -156,10 +175,10 @@ export async function handleMessageStanza (stanza) {
 /**
 /**
  * Ask the XMPP server to enable Message Carbons
  * Ask the XMPP server to enable Message Carbons
  * See [XEP-0280](https://xmpp.org/extensions/xep-0280.html#enabling)
  * See [XEP-0280](https://xmpp.org/extensions/xep-0280.html#enabling)
- * @param { Boolean } reconnecting
  */
  */
 export async function enableCarbons () {
 export async function enableCarbons () {
-    const domain = Strophe.getDomainFromJid(_converse.bare_jid);
+    const bare_jid = _converse.session.get('bare_jid');
+    const domain = Strophe.getDomainFromJid(bare_jid);
     const supported = await api.disco.supports(Strophe.NS.CARBONS, domain);
     const supported = await api.disco.supports(Strophe.NS.CARBONS, domain);
 
 
     if (!supported) {
     if (!supported) {

+ 9 - 5
src/headless/plugins/chatboxes/api.js

@@ -1,10 +1,13 @@
+/**
+ * @typedef {import('@converse/skeletor').Model} Model
+ * @typedef {import('../chat/model.js').default} ChatBox
+ */
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 import { createChatBox } from './utils.js';
 import { createChatBox } from './utils.js';
 
 
 const _chatBoxTypes = {};
 const _chatBoxTypes = {};
 
 
-/** @typedef {import('@converse/skeletor').Model} Model */
 
 
 /**
 /**
  * The "chatboxes" namespace.
  * The "chatboxes" namespace.
@@ -17,7 +20,7 @@ export default {
      * @method api.chatboxes.create
      * @method api.chatboxes.create
      * @param {string|string[]} jids - A JID or array of JIDs
      * @param {string|string[]} jids - A JID or array of JIDs
      * @param {Object} attrs An object containing configuration attributes
      * @param {Object} attrs An object containing configuration attributes
-     * @param {Model} model - The type of chatbox that should be created
+     * @param {new (attrs: object, options: object) => ChatBox} model - The type of chatbox that should be created
      */
      */
     async create (jids=[], attrs={}, model) {
     async create (jids=[], attrs={}, model) {
         await api.waitUntil('chatBoxesFetched');
         await api.waitUntil('chatBoxesFetched');
@@ -34,13 +37,14 @@ export default {
      */
      */
     async get (jids) {
     async get (jids) {
         await api.waitUntil('chatBoxesFetched');
         await api.waitUntil('chatBoxesFetched');
+        const { chatboxes } = _converse.state;
         if (jids === undefined) {
         if (jids === undefined) {
-            return _converse.chatboxes.models;
+            return chatboxes.models;
         } else if (typeof jids === 'string') {
         } else if (typeof jids === 'string') {
-            return _converse.chatboxes.get(jids.toLowerCase());
+            return chatboxes.get(jids.toLowerCase());
         } else {
         } else {
             jids = jids.map(j => j.toLowerCase());
             jids = jids.map(j => j.toLowerCase());
-            return _converse.chatboxes.models.filter(m => jids.includes(m.get('jid')));
+            return chatboxes.models.filter(m => jids.includes(m.get('jid')));
         }
         }
     },
     },
 
 

+ 24 - 4
src/headless/plugins/chatboxes/chatboxes.js

@@ -1,13 +1,24 @@
+/**
+ * @typedef {import('@converse/skeletor').Model} Model
+ */
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 import { Collection } from "@converse/skeletor";
 import { Collection } from "@converse/skeletor";
 import { initStorage } from '../../utils/storage.js';
 import { initStorage } from '../../utils/storage.js';
 
 
 class ChatBoxes extends Collection {
 class ChatBoxes extends Collection {
-    get comparator () {
-        return 'time_opened';
+
+    /**
+     * @param {Model[]} models
+     * @param {object} options
+     */
+    constructor (models, options) {
+        super(models, Object.assign({ comparator: 'time_opened' }, options));
     }
     }
 
 
+    /**
+     * @param {Collection} collection
+     */
     onChatBoxesFetched (collection) {
     onChatBoxesFetched (collection) {
         collection.filter(c => !c.isValid()).forEach(c => c.destroy());
         collection.filter(c => !c.isValid()).forEach(c => c.destroy());
         /**
         /**
@@ -22,15 +33,24 @@ class ChatBoxes extends Collection {
         api.trigger('chatBoxesFetched');
         api.trigger('chatBoxesFetched');
     }
     }
 
 
+    /**
+     * @param {boolean} reconnecting
+     */
     onConnected (reconnecting) {
     onConnected (reconnecting) {
-        if (reconnecting) { return; }
-        initStorage(this, `converse.chatboxes-${_converse.bare_jid}`);
+        if (reconnecting) return;
+
+        const bare_jid = _converse.session.get('bare_jid');
+        initStorage(this, `converse.chatboxes-${bare_jid}`);
         this.fetch({
         this.fetch({
             'add': true,
             'add': true,
             'success': c => this.onChatBoxesFetched(c)
             'success': c => this.onChatBoxesFetched(c)
         });
         });
     }
     }
 
 
+    /**
+     * @param {object} attrs
+     * @param {object} options
+     */
     createModel (attrs, options) {
     createModel (attrs, options) {
         if (!attrs.type) {
         if (!attrs.type) {
             throw new Error("You need to specify a type of chatbox to be created");
             throw new Error("You need to specify a type of chatbox to be created");

+ 10 - 2
src/headless/plugins/chatboxes/utils.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('../chat/model.js').default} ChatBox
+ */
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import { converse } from '../../shared/api/index.js';
 import { converse } from '../../shared/api/index.js';
 import log from "../../log";
 import log from "../../log";
@@ -5,12 +8,17 @@ import log from "../../log";
 const { Strophe } = converse.env;
 const { Strophe } = converse.env;
 
 
 
 
+/**
+ * @param {string} jid
+ * @param {object} attrs
+ * @param {new (attrs: object, options: object) => ChatBox} Model
+ */
 export async function createChatBox (jid, attrs, Model) {
 export async function createChatBox (jid, attrs, Model) {
     jid = Strophe.getBareJidFromJid(jid.toLowerCase());
     jid = Strophe.getBareJidFromJid(jid.toLowerCase());
     Object.assign(attrs, {'jid': jid, 'id': jid});
     Object.assign(attrs, {'jid': jid, 'id': jid});
     let chatbox;
     let chatbox;
     try {
     try {
-        chatbox = new Model(attrs, {'collection': _converse.chatboxes});
+        chatbox = new Model(attrs, {'collection': _converse.state.chatboxes});
     } catch (e) {
     } catch (e) {
         log.error(e);
         log.error(e);
         return null;
         return null;
@@ -20,6 +28,6 @@ export async function createChatBox (jid, attrs, Model) {
         chatbox.destroy();
         chatbox.destroy();
         return null;
         return null;
     }
     }
-    _converse.chatboxes.add(chatbox);
+    _converse.state.chatboxes.add(chatbox);
     return chatbox;
     return chatbox;
 }
 }

+ 33 - 28
src/headless/plugins/disco/api.js

@@ -30,16 +30,18 @@ export default {
              */
              */
             async getFeature (name, xmlns) {
             async getFeature (name, xmlns) {
                 await api.waitUntil('streamFeaturesAdded');
                 await api.waitUntil('streamFeaturesAdded');
+
+                const { stream_features } = _converse.state;
                 if (!name || !xmlns) {
                 if (!name || !xmlns) {
                     throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature");
                     throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature");
                 }
                 }
-                if (_converse.stream_features === undefined && !api.connection.connected()) {
+                if (stream_features === undefined && !api.connection.connected()) {
                     // Happens during tests when disco lookups happen asynchronously after teardown.
                     // Happens during tests when disco lookups happen asynchronously after teardown.
-                    const msg = `Tried to get feature ${name} ${xmlns} but _converse.stream_features has been torn down`;
+                    const msg = `Tried to get feature ${name} ${xmlns} but stream_features has been torn down`;
                     log.warn(msg);
                     log.warn(msg);
                     return;
                     return;
                 }
                 }
-                return _converse.stream_features.findWhere({'name': name, 'xmlns': xmlns});
+                return stream_features.findWhere({'name': name, 'xmlns': xmlns});
             }
             }
         },
         },
 
 
@@ -65,15 +67,16 @@ export default {
                  * @example _converse.api.disco.own.identities.clear();
                  * @example _converse.api.disco.own.identities.clear();
                  */
                  */
                 add (category, type, name, lang) {
                 add (category, type, name, lang) {
-                    for (var i=0; i<_converse.disco._identities.length; i++) {
-                        if (_converse.disco._identities[i].category == category &&
-                            _converse.disco._identities[i].type == type &&
-                            _converse.disco._identities[i].name == name &&
-                            _converse.disco._identities[i].lang == lang) {
+                    const { disco } = _converse.state;
+                    for (var i=0; i<disco._identities.length; i++) {
+                        if (disco._identities[i].category == category &&
+                                disco._identities[i].type == type &&
+                                disco._identities[i].name == name &&
+                                disco._identities[i].lang == lang) {
                             return false;
                             return false;
                         }
                         }
                     }
                     }
-                    _converse.disco._identities.push({category: category, type: type, name: name, lang: lang});
+                    disco._identities.push({category: category, type: type, name: name, lang: lang});
                 },
                 },
                 /**
                 /**
                  * Clears all previously registered identities.
                  * Clears all previously registered identities.
@@ -81,7 +84,7 @@ export default {
                  * @example _converse.api.disco.own.identities.clear();
                  * @example _converse.api.disco.own.identities.clear();
                  */
                  */
                 clear () {
                 clear () {
-                    _converse.disco._identities = []
+                    _converse.state.disco._identities = []
                 },
                 },
                 /**
                 /**
                  * Returns all of the identities registered for this client
                  * Returns all of the identities registered for this client
@@ -90,7 +93,7 @@ export default {
                  * @example const identities = api.disco.own.identities.get();
                  * @example const identities = api.disco.own.identities.get();
                  */
                  */
                 get () {
                 get () {
-                    return _converse.disco._identities;
+                    return _converse.state.disco._identities;
                 }
                 }
             },
             },
 
 
@@ -106,10 +109,11 @@ export default {
                  * @example _converse.api.disco.own.features.add("http://jabber.org/protocol/caps");
                  * @example _converse.api.disco.own.features.add("http://jabber.org/protocol/caps");
                  */
                  */
                 add (name) {
                 add (name) {
-                    for (var i=0; i<_converse.disco._features.length; i++) {
-                        if (_converse.disco._features[i] == name) { return false; }
+                    const { disco } = _converse.state;
+                    for (var i=0; i<disco._features.length; i++) {
+                        if (disco._features[i] == name) { return false; }
                     }
                     }
-                    _converse.disco._features.push(name);
+                    disco._features.push(name);
                 },
                 },
                 /**
                 /**
                  * Clears all previously registered features.
                  * Clears all previously registered features.
@@ -117,7 +121,7 @@ export default {
                  * @example _converse.api.disco.own.features.clear();
                  * @example _converse.api.disco.own.features.clear();
                  */
                  */
                 clear () {
                 clear () {
-                    _converse.disco._features = []
+                    _converse.state.disco._features = []
                 },
                 },
                 /**
                 /**
                  * Returns all of the features registered for this client (i.e. instance of Converse).
                  * Returns all of the features registered for this client (i.e. instance of Converse).
@@ -125,7 +129,7 @@ export default {
                  * @example const features = api.disco.own.features.get();
                  * @example const features = api.disco.own.features.get();
                  */
                  */
                 get () {
                 get () {
-                    return _converse.disco._features;
+                    return _converse.state.disco._features;
                 }
                 }
             }
             }
         },
         },
@@ -190,15 +194,16 @@ export default {
              */
              */
             async get (jid, create=false) {
             async get (jid, create=false) {
                 await api.waitUntil('discoInitialized');
                 await api.waitUntil('discoInitialized');
+                const { disco_entities } = _converse.state;
                 if (!jid) {
                 if (!jid) {
-                    return _converse.disco_entities;
+                    return disco_entities;
                 }
                 }
-                if (_converse.disco_entities === undefined) {
+                if (disco_entities === undefined) {
                     // Happens during tests when disco lookups happen asynchronously after teardown.
                     // Happens during tests when disco lookups happen asynchronously after teardown.
-                    log.warn(`Tried to look up entity ${jid} but _converse.disco_entities has been torn down`);
+                    log.warn(`Tried to look up entity ${jid} but disco_entities has been torn down`);
                     return;
                     return;
                 }
                 }
-                const entity = _converse.disco_entities.get(jid);
+                const entity = disco_entities.get(jid);
                 if (entity || !create) {
                 if (entity || !create) {
                     return entity;
                     return entity;
                 }
                 }
@@ -209,11 +214,11 @@ export default {
              * Return any disco items advertised on this entity
              * Return any disco items advertised on this entity
              *
              *
              * @method api.disco.entities.items
              * @method api.disco.entities.items
-             * @param { string } jid The Jabber ID of the entity for which we want to fetch items
+             * @param { string } jid - The Jabber ID of the entity for which we want to fetch items
              * @example api.disco.entities.items(jid);
              * @example api.disco.entities.items(jid);
              */
              */
             items (jid) {
             items (jid) {
-                return _converse.disco_entities.filter(e => e.get('parent_jids')?.includes(jid));
+                return _converse.state.disco_entities.filter(e => e.get('parent_jids')?.includes(jid));
             },
             },
 
 
             /**
             /**
@@ -235,7 +240,7 @@ export default {
              * @example _converse.api.disco.entities.create({ jid }, {'ignore_cache': true});
              * @example _converse.api.disco.entities.create({ jid }, {'ignore_cache': true});
              */
              */
             create (data, options) {
             create (data, options) {
-                return _converse.disco_entities.create(data, options);
+                return _converse.state.disco_entities.create(data, options);
             }
             }
         },
         },
 
 
@@ -266,7 +271,7 @@ export default {
 
 
                 const entity = await api.disco.entities.get(jid, true);
                 const entity = await api.disco.entities.get(jid, true);
 
 
-                if (_converse.disco_entities === undefined && !api.connection.connected()) {
+                if (_converse.state.disco_entities === undefined && !api.connection.connected()) {
                     // Happens during tests when disco lookups happen asynchronously after teardown.
                     // Happens during tests when disco lookups happen asynchronously after teardown.
                     log.warn(`Tried to get feature ${feature} for ${jid} but _converse.disco_entities has been torn down`);
                     log.warn(`Tried to get feature ${feature} for ${jid} but _converse.disco_entities has been torn down`);
                     return [];
                     return [];
@@ -300,7 +305,7 @@ export default {
 
 
                 const entity = await api.disco.entities.get(jid, true);
                 const entity = await api.disco.entities.get(jid, true);
 
 
-                if (_converse.disco_entities === undefined && !api.connection.connected()) {
+                if (_converse.state.disco_entities === undefined && !api.connection.connected()) {
                     // Happens during tests when disco lookups happen asynchronously after teardown.
                     // Happens during tests when disco lookups happen asynchronously after teardown.
                     log.warn(`Tried to check if ${jid} supports feature ${feature}`);
                     log.warn(`Tried to check if ${jid} supports feature ${feature}`);
                     return false;
                     return false;
@@ -424,15 +429,15 @@ export default {
          * XEP-0163: https://xmpp.org/extensions/xep-0163.html#support
          * XEP-0163: https://xmpp.org/extensions/xep-0163.html#support
          *
          *
          * @method api.disco.getIdentity
          * @method api.disco.getIdentity
-         * @param { string } The identity category.
+         * @param {string} category -The identity category.
          *     In the XML stanza, this is the `category`
          *     In the XML stanza, this is the `category`
          *     attribute of the `<identity>` element.
          *     attribute of the `<identity>` element.
          *     For example: 'pubsub'
          *     For example: 'pubsub'
-         * @param { string } type The identity type.
+         * @param {string} type - The identity type.
          *     In the XML stanza, this is the `type`
          *     In the XML stanza, this is the `type`
          *     attribute of the `<identity>` element.
          *     attribute of the `<identity>` element.
          *     For example: 'pep'
          *     For example: 'pep'
-         * @param { string } jid The JID of the entity which might have the identity
+         * @param {string} jid - The JID of the entity which might have the identity
          * @returns {promise} A promise which resolves with a map indicating
          * @returns {promise} A promise which resolves with a map indicating
          *     whether an identity with a given type is provided by the entity.
          *     whether an identity with a given type is provided by the entity.
          * @example
          * @example

+ 9 - 6
src/headless/plugins/disco/entity.js

@@ -2,8 +2,7 @@ import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
 import log from '../../log.js';
 import log from '../../log.js';
 import sizzle from 'sizzle';
 import sizzle from 'sizzle';
-import { Collection } from '@converse/skeletor';
-import { Model } from '@converse/skeletor';
+import { Collection, Model } from '@converse/skeletor';
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
 import { createStore } from '../../utils/storage.js';
 import { createStore } from '../../utils/storage.js';
 
 
@@ -50,7 +49,6 @@ class DiscoEntity extends Model {
     /**
     /**
      * Returns a Promise which resolves with a map indicating
      * Returns a Promise which resolves with a map indicating
      * whether a given identity is provided by this entity.
      * whether a given identity is provided by this entity.
-     * @private
      * @method _converse.DiscoEntity#getIdentity
      * @method _converse.DiscoEntity#getIdentity
      * @param { String } category - The identity category
      * @param { String } category - The identity category
      * @param { String } type - The identity type
      * @param { String } type - The identity type
@@ -66,7 +64,6 @@ class DiscoEntity extends Model {
     /**
     /**
      * Returns a Promise which resolves with a map indicating
      * Returns a Promise which resolves with a map indicating
      * whether a given feature is supported.
      * whether a given feature is supported.
-     * @private
      * @method _converse.DiscoEntity#getFeature
      * @method _converse.DiscoEntity#getFeature
      * @param { String } feature - The feature that might be supported.
      * @param { String } feature - The feature that might be supported.
      */
      */
@@ -133,6 +130,9 @@ class DiscoEntity extends Model {
         this.onInfo(stanza);
         this.onInfo(stanza);
     }
     }
 
 
+    /**
+     * @param {Element} stanza
+     */
     onDiscoItems (stanza) {
     onDiscoItems (stanza) {
         sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"] item`, stanza).forEach(item => {
         sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"] item`, stanza).forEach(item => {
             if (item.getAttribute('node')) {
             if (item.getAttribute('node')) {
@@ -141,7 +141,7 @@ class DiscoEntity extends Model {
                 return;
                 return;
             }
             }
             const jid = item.getAttribute('jid');
             const jid = item.getAttribute('jid');
-            const entity = _converse.disco_entities.get(jid);
+            const entity = _converse.state.disco_entities.get(jid);
             if (entity) {
             if (entity) {
                 entity.set({ parent_jids: [this.get('jid')] });
                 entity.set({ parent_jids: [this.get('jid')] });
             } else {
             } else {
@@ -155,7 +155,7 @@ class DiscoEntity extends Model {
     }
     }
 
 
     async queryForItems () {
     async queryForItems () {
-        if (this.identities.where({ 'category': 'server' }).length === 0) {
+        if (this.identities.where({ category: 'server' }).length === 0) {
             // Don't fetch features and items if this is not a
             // Don't fetch features and items if this is not a
             // server or a conference component.
             // server or a conference component.
             return;
             return;
@@ -164,6 +164,9 @@ class DiscoEntity extends Model {
         this.onDiscoItems(stanza);
         this.onDiscoItems(stanza);
     }
     }
 
 
+    /**
+     * @param {Element} stanza
+     */
     async onInfo (stanza) {
     async onInfo (stanza) {
         Array.from(stanza.querySelectorAll('identity')).forEach(identity => {
         Array.from(stanza.querySelectorAll('identity')).forEach(identity => {
             this.identities.create({
             this.identities.create({

+ 15 - 8
src/headless/plugins/disco/index.js

@@ -25,19 +25,23 @@ converse.plugins.add('converse-disco', {
         api.promises.add('discoInitialized');
         api.promises.add('discoInitialized');
         api.promises.add('streamFeaturesAdded');
         api.promises.add('streamFeaturesAdded');
 
 
-        _converse.DiscoEntity = DiscoEntity;
-        _converse.DiscoEntities = DiscoEntities;
+        const exports = { DiscoEntity, DiscoEntities };
 
 
-        _converse.disco = {
+        Object.assign(_converse, exports); // XXX: DEPRECATED
+        Object.assign(_converse.exports, exports);
+
+        const disco = {
             _identities: [],
             _identities: [],
             _features: []
             _features: []
         };
         };
+        Object.assign(_converse, { disco }); // XXX: DEPRECATED
+        Object.assign(_converse.state, { disco });
 
 
         api.listen.on('userSessionInitialized', async () => {
         api.listen.on('userSessionInitialized', async () => {
             initStreamFeatures();
             initStreamFeatures();
-            if (_converse.connfeedback.get('connection_status') === Strophe.Status.ATTACHED) {
+            if (_converse.state.connfeedback.get('connection_status') === Strophe.Status.ATTACHED) {
                 // When re-attaching to a BOSH session, we fetch the stream features from the cache.
                 // When re-attaching to a BOSH session, we fetch the stream features from the cache.
-                await new Promise((success, error) => _converse.stream_features.fetch({ success, error }));
+                await new Promise((success, error) => _converse.state.stream_features.fetch({ success, error }));
                 notifyStreamFeaturesAdded();
                 notifyStreamFeaturesAdded();
             }
             }
         });
         });
@@ -47,9 +51,12 @@ converse.plugins.add('converse-disco', {
 
 
         api.listen.on('beforeTearDown', async () => {
         api.listen.on('beforeTearDown', async () => {
             api.promises.add('streamFeaturesAdded');
             api.promises.add('streamFeaturesAdded');
-            if (_converse.stream_features) {
-                await _converse.stream_features.clearStore();
-                delete _converse.stream_features;
+
+            const { stream_features } = _converse.state;
+            if (stream_features) {
+                await stream_features.clearStore();
+                delete _converse.state.stream_features;
+                Object.assign(_converse, { stream_features: undefined }); // XXX: DEPRECATED
             }
             }
         });
         });
 
 

+ 33 - 20
src/headless/plugins/disco/utils.js

@@ -18,7 +18,7 @@ function onDiscoInfoRequest (stanza) {
     }
     }
 
 
     iqresult.c('query', attrs);
     iqresult.c('query', attrs);
-    _converse.disco._identities.forEach(identity => {
+    _converse.state.disco._identities.forEach(identity => {
         const attrs = {
         const attrs = {
             'category': identity.category,
             'category': identity.category,
             'type': identity.type
             'type': identity.type
@@ -31,7 +31,7 @@ function onDiscoInfoRequest (stanza) {
         }
         }
         iqresult.c('identity', attrs).up();
         iqresult.c('identity', attrs).up();
     });
     });
-    _converse.disco._features.forEach(f => iqresult.c('feature', {'var': f}).up());
+    _converse.state.disco._features.forEach(f => iqresult.c('feature', {'var': f}).up());
     api.send(iqresult.tree());
     api.send(iqresult.tree());
     return true;
     return true;
 }
 }
@@ -64,14 +64,22 @@ export async function initializeDisco () {
         'iq', 'get', null, null
         'iq', 'get', null, null
     );
     );
 
 
-    _converse.disco_entities = new _converse.DiscoEntities();
-    const id = `converse.disco-entities-${_converse.bare_jid}`;
-    _converse.disco_entities.browserStorage = createStore(id, 'session');
+    const disco_entities = new _converse.exports.DiscoEntities();
 
 
-    const collection = await _converse.disco_entities.fetchEntities();
-    if (collection.length === 0 || !collection.get(_converse.domain)) {
+    Object.assign(_converse, { disco_entities }); // XXX: DEPRECATED
+    Object.assign(_converse.state, { disco_entities });
+
+    const bare_jid = _converse.session.get('bare_jid');
+    const id = `converse.disco-entities-${bare_jid}`;
+
+    disco_entities.browserStorage = createStore(id, 'session');
+    const collection = await disco_entities.fetchEntities();
+
+    const domain = _converse.session.get('domain');
+
+    if (collection.length === 0 || !collection.get(domain)) {
         // If we don't have an entity for our own XMPP server, create one.
         // If we don't have an entity for our own XMPP server, create one.
-        api.disco.entities.create({'jid': _converse.domain}, {'ignore_cache': true});
+        api.disco.entities.create({'jid': domain}, {'ignore_cache': true});
     }
     }
     /**
     /**
      * Triggered once the `converse-disco` plugin has been initialized and the
      * Triggered once the `converse-disco` plugin has been initialized and the
@@ -89,12 +97,15 @@ export function initStreamFeatures () {
     // features from cache.
     // features from cache.
     // Otherwise the features will be created once we've received them
     // Otherwise the features will be created once we've received them
     // from the server (see populateStreamFeatures).
     // from the server (see populateStreamFeatures).
-    if (!_converse.stream_features) {
-        const bare_jid = Strophe.getBareJidFromJid(_converse.jid);
+    if (!_converse.state.stream_features) {
+        const bare_jid = _converse.session.get('bare_jid');
         const id = `converse.stream-features-${bare_jid}`;
         const id = `converse.stream-features-${bare_jid}`;
         api.promises.add('streamFeaturesAdded');
         api.promises.add('streamFeaturesAdded');
-        _converse.stream_features = new Collection();
-        _converse.stream_features.browserStorage = createStore(id, "session");
+
+        const stream_features = new Collection();
+        stream_features.browserStorage = createStore(id, "session");
+        Object.assign(_converse, { stream_features }); // XXX: DEPRECATED
+        Object.assign(_converse.state, { stream_features });
     }
     }
 }
 }
 
 
@@ -113,11 +124,11 @@ export function populateStreamFeatures () {
     // Strophe.js sets the <stream:features> element on the
     // Strophe.js sets the <stream:features> element on the
     // Strophe.Connection instance.
     // Strophe.Connection instance.
     //
     //
-    // Once this is we populate the _converse.stream_features collection
+    // Once this is we populate the stream_features collection
     // and trigger streamFeaturesAdded.
     // and trigger streamFeaturesAdded.
     initStreamFeatures();
     initStreamFeatures();
     Array.from(api.connection.get().features.childNodes).forEach(feature => {
     Array.from(api.connection.get().features.childNodes).forEach(feature => {
-        _converse.stream_features.create({
+        _converse.state.stream_features.create({
             'name': feature.nodeName,
             'name': feature.nodeName,
             'xmlns': feature.getAttribute('xmlns')
             'xmlns': feature.getAttribute('xmlns')
         });
         });
@@ -126,10 +137,12 @@ export function populateStreamFeatures () {
 }
 }
 
 
 export function clearSession () {
 export function clearSession () {
-    _converse.disco_entities?.forEach(e => e.features.clearStore());
-    _converse.disco_entities?.forEach(e => e.identities.clearStore());
-    _converse.disco_entities?.forEach(e => e.dataforms.clearStore());
-    _converse.disco_entities?.forEach(e => e.fields.clearStore());
-    _converse.disco_entities?.clearStore();
-    delete _converse.disco_entities;
+    const { disco_entities } = _converse.state;
+    disco_entities?.forEach(e => e.features.clearStore());
+    disco_entities?.forEach(e => e.identities.clearStore());
+    disco_entities?.forEach(e => e.dataforms.clearStore());
+    disco_entities?.forEach(e => e.fields.clearStore());
+    disco_entities?.clearStore();
+    delete _converse.state.disco_entities;
+    Object.assign(_converse, { disco_entities: undefined });
 }
 }

+ 3 - 1
src/headless/plugins/emoji/index.js

@@ -73,7 +73,9 @@ converse.plugins.add('converse-emoji', {
             }
             }
         }
         }
 
 
-        _converse.EmojiPicker = EmojiPicker;
+        const exports = { EmojiPicker };
+        Object.assign(_converse, exports); // XXX: DEPRECATED
+        Object.assign(_converse.exports, exports);
 
 
         // We extend the default converse.js API to add methods specific to MUC groupchats.
         // We extend the default converse.js API to add methods specific to MUC groupchats.
         Object.assign(api, {
         Object.assign(api, {

+ 24 - 7
src/headless/plugins/emoji/utils.js

@@ -56,27 +56,33 @@ function fromCodePoint (codepoint) {
 }
 }
 
 
 
 
+/**
+ * Converts unicode code points and code pairs to their respective characters
+ * @param {string} unicode
+ */
 function convert (unicode) {
 function convert (unicode) {
-    // Converts unicode code points and code pairs to their respective characters
     if (unicode.indexOf("-") > -1) {
     if (unicode.indexOf("-") > -1) {
-        const parts = [],
-              s = unicode.split('-');
+        const parts = [];
+        const s = unicode.split('-');
+
         for (let i = 0; i < s.length; i++) {
         for (let i = 0; i < s.length; i++) {
-            let part = parseInt(s[i], 16);
+            const part = parseInt(s[i], 16);
             if (part >= 0x10000 && part <= 0x10FFFF) {
             if (part >= 0x10000 && part <= 0x10FFFF) {
                 const hi = Math.floor((part - 0x10000) / 0x400) + 0xD800;
                 const hi = Math.floor((part - 0x10000) / 0x400) + 0xD800;
                 const lo = ((part - 0x10000) % 0x400) + 0xDC00;
                 const lo = ((part - 0x10000) % 0x400) + 0xDC00;
-                part = (String.fromCharCode(hi) + String.fromCharCode(lo));
+                parts.push(String.fromCharCode(hi) + String.fromCharCode(lo));
             } else {
             } else {
-                part = String.fromCharCode(part);
+                parts.push(String.fromCharCode(part));
             }
             }
-            parts.push(part);
         }
         }
         return parts.join('');
         return parts.join('');
     }
     }
     return fromCodePoint(unicode);
     return fromCodePoint(unicode);
 }
 }
 
 
+/**
+ * @param {string} str
+ */
 export function convertASCII2Emoji (str) {
 export function convertASCII2Emoji (str) {
     // Replace ASCII smileys
     // Replace ASCII smileys
     return str.replace(ASCII_REPLACE_REGEX, (entire, _, m2, m3) => {
     return str.replace(ASCII_REPLACE_REGEX, (entire, _, m2, m3) => {
@@ -90,6 +96,9 @@ export function convertASCII2Emoji (str) {
     });
     });
 }
 }
 
 
+/**
+ * @param {string} text
+ */
 export function getShortnameReferences (text) {
 export function getShortnameReferences (text) {
     if (!converse.emojis.initialized) {
     if (!converse.emojis.initialized) {
         throw new Error(
         throw new Error(
@@ -111,16 +120,24 @@ export function getShortnameReferences (text) {
 }
 }
 
 
 
 
+/**
+ * @param {string} str
+ * @param {Function} callback
+ */
 function parseStringForEmojis(str, callback) {
 function parseStringForEmojis(str, callback) {
     const UFE0Fg = /\uFE0F/g;
     const UFE0Fg = /\uFE0F/g;
     const U200D = String.fromCharCode(0x200D);
     const U200D = String.fromCharCode(0x200D);
     return String(str).replace(CODEPOINTS_REGEX, (emoji, _, offset) => {
     return String(str).replace(CODEPOINTS_REGEX, (emoji, _, offset) => {
         const icon_id = toCodePoint(emoji.indexOf(U200D) < 0 ? emoji.replace(UFE0Fg, '') : emoji);
         const icon_id = toCodePoint(emoji.indexOf(U200D) < 0 ? emoji.replace(UFE0Fg, '') : emoji);
         if (icon_id) callback(icon_id, emoji, offset);
         if (icon_id) callback(icon_id, emoji, offset);
+        return emoji;
     });
     });
 }
 }
 
 
 
 
+/**
+ * @param {string} text
+ */
 export function getCodePointReferences (text) {
 export function getCodePointReferences (text) {
     const references = [];
     const references = [];
     parseStringForEmojis(text, (icon_id, emoji, offset) => {
     parseStringForEmojis(text, (icon_id, emoji, offset) => {

+ 11 - 2
src/headless/plugins/headlines/api.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('./feed.js').default} HeadlinesFeed
+ */
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 import { HEADLINES_TYPE } from '../../shared/constants.js';
 import { HEADLINES_TYPE } from '../../shared/constants.js';
@@ -19,13 +22,18 @@ export default {
          * @param {String|String[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
          * @param {String|String[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
          * @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model.
          * @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model.
          * @param { Boolean } [create=false] - Whether the chat should be created if it's not found.
          * @param { Boolean } [create=false] - Whether the chat should be created if it's not found.
-         * @returns { Promise<_converse.HeadlinesFeed> }
+         * @returns { Promise<HeadlinesFeed[]|HeadlinesFeed> }
          */
          */
         async get (jids, attrs={}, create=false) {
         async get (jids, attrs={}, create=false) {
+            /**
+             * @param {string} jid
+             * @returns {Promise<HeadlinesFeed>}
+             */
             async function _get (jid) {
             async function _get (jid) {
                 let model = await api.chatboxes.get(jid);
                 let model = await api.chatboxes.get(jid);
                 if (!model && create) {
                 if (!model && create) {
-                    model = await api.chatboxes.create(jid, attrs, _converse.HeadlinesFeed);
+                    const { HeadlinesFeed } = _converse.exports;
+                    model = await api.chatboxes.create(jid, attrs, HeadlinesFeed);
                 } else {
                 } else {
                     model = (model && model.get('type') === HEADLINES_TYPE) ? model : null;
                     model = (model && model.get('type') === HEADLINES_TYPE) ? model : null;
                     if (model && Object.keys(attrs).length) {
                     if (model && Object.keys(attrs).length) {
@@ -34,6 +42,7 @@ export default {
                 }
                 }
                 return model;
                 return model;
             }
             }
+
             if (jids === undefined) {
             if (jids === undefined) {
                 const chats = await api.chatboxes.get();
                 const chats = await api.chatboxes.get();
                 return chats.filter(c => (c.get('type') === HEADLINES_TYPE));
                 return chats.filter(c => (c.get('type') === HEADLINES_TYPE));

+ 12 - 0
src/headless/plugins/headlines/feed.js

@@ -3,6 +3,12 @@ import api from "../../shared/api/index.js";
 import { HEADLINES_TYPE } from '../../shared/constants.js';
 import { HEADLINES_TYPE } from '../../shared/constants.js';
 
 
 
 
+/**
+ * Shows headline messages
+ * @class
+ * @namespace _converse.HeadlinesFeed
+ * @memberOf _converse
+ */
 export default class HeadlinesFeed extends ChatBox {
 export default class HeadlinesFeed extends ChatBox {
 
 
     defaults () {
     defaults () {
@@ -12,10 +18,16 @@ export default class HeadlinesFeed extends ChatBox {
             'message_type': 'headline',
             'message_type': 'headline',
             'num_unread': 0,
             'num_unread': 0,
             'time_opened': this.get('time_opened') || (new Date()).getTime(),
             'time_opened': this.get('time_opened') || (new Date()).getTime(),
+            'time_sent': undefined,
             'type': HEADLINES_TYPE
             'type': HEADLINES_TYPE
         }
         }
     }
     }
 
 
+    constructor (attrs, options) {
+        super(attrs, options);
+        this.disable_mam = true; // Don't do MAM queries for this box
+    }
+
     async initialize () {
     async initialize () {
         super.initialize();
         super.initialize();
         this.set({'box_id': `box-${this.get('jid')}`});
         this.set({'box_id': `box-${this.get('jid')}`});

+ 3 - 7
src/headless/plugins/headlines/index.js

@@ -13,13 +13,9 @@ converse.plugins.add('converse-headlines', {
     dependencies: ["converse-chat"],
     dependencies: ["converse-chat"],
 
 
     initialize () {
     initialize () {
-        /**
-         * Shows headline messages
-         * @class
-         * @namespace _converse.HeadlinesFeed
-         * @memberOf _converse
-         */
-        _converse.HeadlinesFeed = HeadlinesFeed;
+        const exports = { HeadlinesFeed };
+        Object.assign(_converse, exports); // XXX: DEPRECATED
+        Object.assign(_converse.exports, exports);
 
 
         function registerHeadlineHandler () {
         function registerHeadlineHandler () {
             api.connection.get()?.addHandler(m => {
             api.connection.get()?.addHandler(m => {

+ 1 - 1
src/headless/plugins/headlines/utils.js

@@ -15,7 +15,7 @@ export async function onHeadlineMessage (stanza) {
 
 
         await api.waitUntil('rosterInitialized')
         await api.waitUntil('rosterInitialized')
         if (from_jid.includes('@') &&
         if (from_jid.includes('@') &&
-                !_converse.roster.get(from_jid) &&
+                !_converse.state.roster.get(from_jid) &&
                 !api.settings.get("allow_non_roster_messaging")) {
                 !api.settings.get("allow_non_roster_messaging")) {
             return;
             return;
         }
         }

+ 22 - 19
src/headless/plugins/mam/api.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {module:converse-rsm.RSMQueryParameters} RSMQueryParameters
+ */
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
@@ -26,11 +29,11 @@ export default {
      */
      */
     archive: {
     archive: {
          /**
          /**
-          * @typedef { module:converse-rsm~RSMQueryParameters } MAMFilterParameters
-          * Filter parameters which can be used to filter a MAM XEP-0313 archive
-          * @property { String } [end] - A date string in ISO-8601 format, before which messages should be returned. Implies backward paging.
-          * @property { String } [start] - A date string in ISO-8601 format, after which messages should be returned. Implies forward paging.
-          * @property { String } [with] - A JID against which to match messages, according to either their `to` or `from` attributes.
+          * @typedef {RSMQueryParameters} MAMFilterParameters
+          * Filter parmeters which can be used to filter a MAM XEP-0313 archive
+          * @property String} [end] - A date string in ISO-8601 format, before which messages should be returned. Implies backward paging.
+          * @property {String} [start] - A date string in ISO-8601 format, after which messages should be returned. Implies forward paging.
+          * @property {String} [with] - A JID against which to match messages, according to either their `to` or `from` attributes.
           *     An item in a MUC archive matches if the publisher of the item matches the JID.
           *     An item in a MUC archive matches if the publisher of the item matches the JID.
           *     If `with` is omitted, all messages that match the rest of the query will be returned, regardless of to/from
           *     If `with` is omitted, all messages that match the rest of the query will be returned, regardless of to/from
           *     addresses of each message.
           *     addresses of each message.
@@ -38,8 +41,8 @@ export default {
 
 
          /**
          /**
           * The options that can be passed in to the {@link _converse.api.archive.query } method
           * The options that can be passed in to the {@link _converse.api.archive.query } method
-          * @typedef { module:converse-mam~MAMFilterParameters } ArchiveQueryOptions
-          * @property { Boolean } [groupchat=false] - Whether the MAM archive is for a groupchat.
+          * @typedef {MAMFilterParameters} ArchiveQueryOptions
+          * @property {boolean} [groupchat=false] - Whether the MAM archive is for a groupchat.
           */
           */
 
 
          /**
          /**
@@ -49,10 +52,9 @@ export default {
           * RSM to enable easy querying between results pages.
           * RSM to enable easy querying between results pages.
           *
           *
           * @method _converse.api.archive.query
           * @method _converse.api.archive.query
-          * @param { module:converse-mam~ArchiveQueryOptions } options - An object containing query parameters
+          * @param {ArchiveQueryOptions} options - An object containing query parameters
           * @throws {Error} An error is thrown if the XMPP server responds with an error.
           * @throws {Error} An error is thrown if the XMPP server responds with an error.
-          * @returns { Promise<module:converse-mam~MAMQueryResult> } A promise which resolves
-          *     to a {@link module:converse-mam~MAMQueryResult } object.
+          * @returns {Promise<MAMQueryResult>} A promise which resolves to a {@link MAMQueryResult} object.
           *
           *
           * @example
           * @example
           * // Requesting all archived messages
           * // Requesting all archived messages
@@ -211,7 +213,8 @@ export default {
                 attrs.to = options['with'];
                 attrs.to = options['with'];
             }
             }
 
 
-            const jid = attrs.to || _converse.bare_jid;
+            const bare_jid = _converse.session.get('bare_jid');
+            const jid = attrs.to || bare_jid;
             const supported = await api.disco.supports(NS.MAM, jid);
             const supported = await api.disco.supports(NS.MAM, jid);
             if (!supported) {
             if (!supported) {
                 log.warn(`Did not fetch MAM archive for ${jid} because it doesn't support ${NS.MAM}`);
                 log.warn(`Did not fetch MAM archive for ${jid} because it doesn't support ${NS.MAM}`);
@@ -249,18 +252,18 @@ export default {
             const connection = api.connection.get();
             const connection = api.connection.get();
 
 
             const messages = [];
             const messages = [];
-            const message_handler = connection.addHandler(stanza => {
+            const message_handler = connection.addHandler(/** @param {Element} stanza */(stanza) => {
                 const result = sizzle(`message > result[xmlns="${NS.MAM}"]`, stanza).pop();
                 const result = sizzle(`message > result[xmlns="${NS.MAM}"]`, stanza).pop();
                 if (result === undefined || result.getAttribute('queryid') !== queryid) {
                 if (result === undefined || result.getAttribute('queryid') !== queryid) {
                     return true;
                     return true;
                 }
                 }
-                const from = stanza.getAttribute('from') || _converse.bare_jid;
+                const from = stanza.getAttribute('from') || bare_jid;
                 if (options.groupchat) {
                 if (options.groupchat) {
                     if (from !== options['with']) {
                     if (from !== options['with']) {
                         log.warn(`Ignoring alleged groupchat MAM message from ${stanza.getAttribute('from')}`);
                         log.warn(`Ignoring alleged groupchat MAM message from ${stanza.getAttribute('from')}`);
                         return true;
                         return true;
                     }
                     }
-                } else if (from !== _converse.bare_jid) {
+                } else if (from !== bare_jid) {
                     log.warn(`Ignoring alleged MAM message from ${stanza.getAttribute('from')}`);
                     log.warn(`Ignoring alleged MAM message from ${stanza.getAttribute('from')}`);
                     return true;
                     return true;
                 }
                 }
@@ -296,14 +299,14 @@ export default {
                 rsm = new RSM({...options, 'xml': set});
                 rsm = new RSM({...options, 'xml': set});
             }
             }
             /**
             /**
-             * @typedef { Object } MAMQueryResult
-             * @property { Array } messages
-             * @property { RSM } [rsm] - An instance of {@link RSM}.
+             * @typedef {Object} MAMQueryResult
+             * @property {Array} messages
+             * @property {RSM} [rsm] - An instance of {@link RSM}.
              *  You can call `next()` or `previous()` on this instance,
              *  You can call `next()` or `previous()` on this instance,
              *  to get the RSM query parameters for the next or previous
              *  to get the RSM query parameters for the next or previous
              *  page in the result set.
              *  page in the result set.
-             * @property { Boolean } complete
-             * @property { Error } [error]
+             * @property {boolean} [complete]
+             * @property {Error} [error]
              */
              */
             return { messages, rsm, complete };
             return { messages, rsm, complete };
         }
         }

+ 3 - 1
src/headless/plugins/mam/index.js

@@ -34,7 +34,9 @@ converse.plugins.add('converse-mam', {
 
 
         Object.assign(api, mam_api);
         Object.assign(api, mam_api);
         // This is mainly done to aid with tests
         // This is mainly done to aid with tests
-        Object.assign(_converse, { onMAMError, onMAMPreferences, handleMAMResult, MAMPlaceholderMessage });
+        const exports = { onMAMError, onMAMPreferences, handleMAMResult, MAMPlaceholderMessage };
+        Object.assign(_converse, exports); // XXX DEPRECATED
+        Object.assign(_converse.exports, exports);
 
 
         /************************ Event Handlers ************************/
         /************************ Event Handlers ************************/
         api.listen.on('addClientFeatures', () => api.disco.own.features.add(NS.MAM));
         api.listen.on('addClientFeatures', () => api.disco.own.features.add(NS.MAM));

+ 30 - 20
src/headless/plugins/mam/utils.js

@@ -1,3 +1,7 @@
+/**
+ * @typedef {import('../muc/muc.js').default} MUC
+ * @typedef {import('../chat/model.js').default} ChatBox
+ */
 import MAMPlaceholderMessage from './placeholder.js';
 import MAMPlaceholderMessage from './placeholder.js';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
@@ -11,6 +15,9 @@ import { CHATROOMS_TYPE } from '../../shared/constants.js';
 const { NS } = Strophe;
 const { NS } = Strophe;
 const u = converse.env.utils;
 const u = converse.env.utils;
 
 
+/**
+ * @param {Element} iq
+ */
 export function onMAMError (iq) {
 export function onMAMError (iq) {
     if (iq?.querySelectorAll('feature-not-implemented').length) {
     if (iq?.querySelectorAll('feature-not-implemented').length) {
         log.warn(`Message Archive Management (XEP-0313) not supported by ${iq.getAttribute('from')}`);
         log.warn(`Message Archive Management (XEP-0313) not supported by ${iq.getAttribute('from')}`);
@@ -46,7 +53,7 @@ export function onMAMPreferences (iq, feature) {
         // but Prosody doesn't do this, so we don't rely on it.
         // but Prosody doesn't do this, so we don't rely on it.
         api.sendIQ(stanza)
         api.sendIQ(stanza)
             .then(() => feature.save({ 'preferences': { 'default': api.settings.get('message_archiving') } }))
             .then(() => feature.save({ 'preferences': { 'default': api.settings.get('message_archiving') } }))
-            .catch(_converse.onMAMError);
+            .catch(_converse.exports.onMAMError);
     } else {
     } else {
         feature.save({ 'preferences': { 'default': api.settings.get('message_archiving') } });
         feature.save({ 'preferences': { 'default': api.settings.get('message_archiving') } });
     }
     }
@@ -59,8 +66,8 @@ export function getMAMPrefsFromFeature (feature) {
     }
     }
     if (prefs['default'] !== api.settings.get('message_archiving')) {
     if (prefs['default'] !== api.settings.get('message_archiving')) {
         api.sendIQ($iq({ 'type': 'get' }).c('prefs', { 'xmlns': NS.MAM }))
         api.sendIQ($iq({ 'type': 'get' }).c('prefs', { 'xmlns': NS.MAM }))
-            .then(iq => _converse.onMAMPreferences(iq, feature))
-            .catch(_converse.onMAMError);
+            .then(iq => _converse.exports.onMAMPreferences(iq, feature))
+            .catch(_converse.exports.onMAMError);
     }
     }
 }
 }
 
 
@@ -100,28 +107,28 @@ export async function handleMAMResult (model, result, query, options, should_pag
 }
 }
 
 
 /**
 /**
- * @typedef { Object } MAMOptions
+ * @typedef {Object} MAMOptions
  * A map of MAM related options that may be passed to fetchArchivedMessages
  * A map of MAM related options that may be passed to fetchArchivedMessages
- * @param { number } [options.max] - The maximum number of items to return.
+ * @param {number} [options.max] - The maximum number of items to return.
  *  Defaults to "archived_messages_page_size"
  *  Defaults to "archived_messages_page_size"
- * @param { string } [options.after] - The XEP-0359 stanza ID of a message
+ * @param {string} [options.after] - The XEP-0359 stanza ID of a message
  *  after which messages should be returned. Implies forward paging.
  *  after which messages should be returned. Implies forward paging.
- * @param { string } [options.before] - The XEP-0359 stanza ID of a message
+ * @param {string} [options.before] - The XEP-0359 stanza ID of a message
  *  before which messages should be returned. Implies backward paging.
  *  before which messages should be returned. Implies backward paging.
- * @param { string } [options.end] - A date string in ISO-8601 format,
+ * @param {string} [options.end] - A date string in ISO-8601 format,
  *  before which messages should be returned. Implies backward paging.
  *  before which messages should be returned. Implies backward paging.
- * @param { string } [options.start] - A date string in ISO-8601 format,
+ * @param {string} [options.start] - A date string in ISO-8601 format,
  *  after which messages should be returned. Implies forward paging.
  *  after which messages should be returned. Implies forward paging.
- * @param { string } [options.with] - The JID of the entity with
+ * @param {string} [options.with] - The JID of the entity with
  *  which messages were exchanged.
  *  which messages were exchanged.
- * @param { boolean } [options.groupchat] - True if archive in groupchat.
+ * @param {boolean} [options.groupchat] - True if archive in groupchat.
  */
  */
 
 
 /**
 /**
  * Fetch XEP-0313 archived messages based on the passed in criteria.
  * Fetch XEP-0313 archived messages based on the passed in criteria.
- * @param { ChatBox | ChatRoom } model
- * @param { MAMOptions } [options]
- * @param { ('forwards'|'backwards'|null)} [should_page=null] - Determines whether
+ * @param {ChatBox} model
+ * @param {MAMOptions} [options]
+ * @param {('forwards'|'backwards'|null)} [should_page=null] - Determines whether
  *  this function should recursively page through the entire result set if a limited
  *  this function should recursively page through the entire result set if a limited
  *  number of results were returned.
  *  number of results were returned.
  */
  */
@@ -130,7 +137,8 @@ export async function fetchArchivedMessages (model, options = {}, should_page =
         return;
         return;
     }
     }
     const is_muc = model.get('type') === CHATROOMS_TYPE;
     const is_muc = model.get('type') === CHATROOMS_TYPE;
-    const mam_jid = is_muc ? model.get('jid') : _converse.bare_jid;
+    const bare_jid = _converse.session.get('bare_jid');
+    const mam_jid = is_muc ? model.get('jid') : bare_jid;
     if (!(await api.disco.supports(NS.MAM, mam_jid))) {
     if (!(await api.disco.supports(NS.MAM, mam_jid))) {
         return;
         return;
     }
     }
@@ -163,9 +171,9 @@ export async function fetchArchivedMessages (model, options = {}, should_page =
 
 
 /**
 /**
  * Create a placeholder message which is used to indicate gaps in the history.
  * Create a placeholder message which is used to indicate gaps in the history.
- * @param { _converse.ChatBox | _converse.ChatRoom } model
- * @param { MAMOptions } options
- * @param { object } result - The RSM result object
+ * @param {ChatBox} model
+ * @param {MAMOptions} options
+ * @param {object} result - The RSM result object
  */
  */
 async function createPlaceholder (model, options, result) {
 async function createPlaceholder (model, options, result) {
     if (options.before == '' && (model.messages.length === 0 || !options.start)) {
     if (options.before == '' && (model.messages.length === 0 || !options.start)) {
@@ -186,9 +194,11 @@ async function createPlaceholder (model, options, result) {
     const { rsm } = result;
     const { rsm } = result;
     const key = `stanza_id ${model.get('jid')}`;
     const key = `stanza_id ${model.get('jid')}`;
     const adjacent_message = msgs.find(m => m[key] === rsm.result.first);
     const adjacent_message = msgs.find(m => m[key] === rsm.result.first);
+    const adjacent_message_date = new Date(adjacent_message['time']);
+
     const msg_data = {
     const msg_data = {
         'template_hook': 'getMessageTemplate',
         'template_hook': 'getMessageTemplate',
-        'time': new Date(new Date(adjacent_message['time']) - 1).toISOString(),
+        'time': new Date(adjacent_message_date.getTime() - 1).toISOString(),
         'before': rsm.result.first,
         'before': rsm.result.first,
         'start': options.start
         'start': options.start
     }
     }
@@ -198,7 +208,7 @@ async function createPlaceholder (model, options, result) {
 /**
 /**
  * Fetches messages that might have been archived *after*
  * Fetches messages that might have been archived *after*
  * the last archived message in our local cache.
  * the last archived message in our local cache.
- * @param { _converse.ChatBox | _converse.ChatRoom }
+ * @param {ChatBox} model
  */
  */
 export function fetchNewestMessages (model) {
 export function fetchNewestMessages (model) {
     if (model.disable_mam) {
     if (model.disable_mam) {

+ 10 - 6
src/headless/plugins/muc/affiliations/api.js

@@ -1,3 +1,6 @@
+/**
+ * @module:plugin-muc-affiliations-api
+ */
 import { setAffiliations } from './utils.js';
 import { setAffiliations } from './utils.js';
 
 
 export default {
 export default {
@@ -11,15 +14,16 @@ export default {
     affiliations: {
     affiliations: {
         /**
         /**
          * Set the given affliation for the given JIDs in the specified MUCs
          * Set the given affliation for the given JIDs in the specified MUCs
+         * @typedef {Object} User
+         * @property {string} User.jid - The JID of the user whose affiliation will change
+         * @property {Array} User.affiliation - The new affiliation for this user
+         * @property {string} [User.reason] - An optional reason for the affiliation change
          *
          *
-         * @param { String|Array<String> } muc_jids - The JIDs of the MUCs in
+         * @param {String|Array<String>} muc_jids - The JIDs of the MUCs in
          *  which the affiliation should be set.
          *  which the affiliation should be set.
-         * @param { Object[] } users - An array of objects representing users
+         * @param {User[]} users - An array of objects representing users
          *  for whom the affiliation is to be set.
          *  for whom the affiliation is to be set.
-         * @param { String } users[].jid - The JID of the user whose affiliation will change
-         * @param { ('outcast'|'member'|'admin'|'owner') } users[].affiliation - The new affiliation for this user
-         * @param { String } [users[].reason] - An optional reason for the affiliation change
-         * @returns { Promise }
+         * @returns {Promise}
          *
          *
          * @example
          * @example
          *  api.rooms.affiliations.set(
          *  api.rooms.affiliations.set(

+ 25 - 26
src/headless/plugins/muc/affiliations/utils.js

@@ -1,6 +1,10 @@
 /**
 /**
  * @copyright The Converse.js contributors
  * @copyright The Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  * @license Mozilla Public License (MPLv2)
+ * @module:muc-affiliations-utils
+ * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem
+ * @typedef {module:plugin-muc-affiliations-api.User} User
+ * @typedef {import('@converse/skeletor').Model} Model
  */
  */
 import _converse from '../../../shared/_converse.js';
 import _converse from '../../../shared/_converse.js';
 import api, { converse } from '../../../shared/api/index.js';
 import api, { converse } from '../../../shared/api/index.js';
@@ -15,9 +19,10 @@ const { Strophe, $iq, u } = converse.env;
  * Returns an array of {@link MemberListItem} objects, representing occupants
  * Returns an array of {@link MemberListItem} objects, representing occupants
  * that have the given affiliation.
  * that have the given affiliation.
  * See: https://xmpp.org/extensions/xep-0045.html#modifymember
  * See: https://xmpp.org/extensions/xep-0045.html#modifymember
- * @param { ("admin"|"owner"|"member") } affiliation
- * @param { String } muc_jid - The JID of the MUC for which the affiliation list should be fetched
- * @returns { Promise<MemberListItem[]> }
+ * @typedef {("admin"|"owner"|"member")} NonOutcastAffiliation
+ * @param {NonOutcastAffiliation} affiliation
+ * @param {string} muc_jid - The JID of the MUC for which the affiliation list should be fetched
+ * @returns {Promise<MemberListItem[]|Error>}
  */
  */
 export async function getAffiliationList (affiliation, muc_jid) {
 export async function getAffiliationList (affiliation, muc_jid) {
     const { __ } = _converse;
     const { __ } = _converse;
@@ -45,8 +50,8 @@ export async function getAffiliationList (affiliation, muc_jid) {
 
 
 /**
 /**
  * Given an occupant model, see which affiliations may be assigned by that user
  * Given an occupant model, see which affiliations may be assigned by that user
- * @param { Model } occupant
- * @returns { Array<('owner'|'admin'|'member'|'outcast'|'none')> } - An array of assignable affiliations
+ * @param {Model} occupant
+ * @returns {typeof AFFILIATIONS} An array of assignable affiliations
  */
  */
 export function getAssignableAffiliations (occupant) {
 export function getAssignableAffiliations (occupant) {
     let disabled = api.settings.get('modtools_disable_assign');
     let disabled = api.settings.get('modtools_disable_assign');
@@ -62,17 +67,12 @@ export function getAssignableAffiliations (occupant) {
     }
     }
 }
 }
 
 
-// Necessary for tests
-_converse.getAssignableAffiliations = getAssignableAffiliations;
-
 /**
 /**
  * Send IQ stanzas to the server to modify affiliations for users in this groupchat.
  * Send IQ stanzas to the server to modify affiliations for users in this groupchat.
  * See: https://xmpp.org/extensions/xep-0045.html#modifymember
  * See: https://xmpp.org/extensions/xep-0045.html#modifymember
- * @param { Array<Object> } users
- * @param { string } users[].jid - The JID of the user whose affiliation will change
- * @param { Array } users[].affiliation - The new affiliation for this user
- * @param { string } [users[].reason] - An optional reason for the affiliation change
- * @returns { Promise }
+ * @param {String|Array<String>} muc_jid - The JID(s) of the MUCs in which the
+ * @param {Array<User>} users
+ * @returns {Promise}
  */
  */
 export function setAffiliations (muc_jid, users) {
 export function setAffiliations (muc_jid, users) {
     const affiliations = [...new Set(users.map(u => u.affiliation))];
     const affiliations = [...new Set(users.map(u => u.affiliation))];
@@ -89,13 +89,13 @@ export function setAffiliations (muc_jid, users) {
  * a separate stanza for each JID.
  * a separate stanza for each JID.
  * Related ticket: https://issues.prosody.im/345
  * Related ticket: https://issues.prosody.im/345
  *
  *
- * @param { ('outcast'|'member'|'admin'|'owner') } affiliation - The affiliation to be set
- * @param { String|Array<String> } muc_jids - The JID(s) of the MUCs in which the
+ * @param {typeof 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.
  *  affiliations need to be set.
- * @param { object } members - A map of jids, affiliations and
+ * @param {object} members - A map of jids, affiliations and
  *  optionally reasons. Only those entries with the
  *  optionally reasons. Only those entries with the
  *  same affiliation as being currently set will be considered.
  *  same affiliation as being currently set will be considered.
- * @returns { Promise } A promise which resolves and fails depending on the XMPP server response.
+ * @returns {Promise} A promise which resolves and fails depending on the XMPP server response.
  */
  */
 export function setAffiliation (affiliation, muc_jids, members) {
 export function setAffiliation (affiliation, muc_jids, members) {
     if (!Array.isArray(muc_jids)) {
     if (!Array.isArray(muc_jids)) {
@@ -109,10 +109,9 @@ export function setAffiliation (affiliation, muc_jids, members) {
 
 
 /**
 /**
  * Send an IQ stanza specifying an affiliation change.
  * Send an IQ stanza specifying an affiliation change.
- * @private
- * @param { String } affiliation: affiliation (could also be stored on the member object).
- * @param { String } muc_jid: The JID of the MUC in which the affiliation should be set.
- * @param { Object } member: Map containing the member's jid and optionally a reason and affiliation.
+ * @param {typeof AFFILIATIONS[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.
  */
  */
 function sendAffiliationIQ (affiliation, muc_jid, member) {
 function sendAffiliationIQ (affiliation, muc_jid, member) {
     const iq = $iq({ to: muc_jid, type: 'set' })
     const iq = $iq({ to: muc_jid, type: 'set' })
@@ -139,20 +138,20 @@ function sendAffiliationIQ (affiliation, muc_jid, member) {
  *
  *
  * The 'reason' property is not taken into account when
  * The 'reason' property is not taken into account when
  * comparing whether affiliations have been changed.
  * comparing whether affiliations have been changed.
- * @param { boolean } exclude_existing - Indicates whether JIDs from
+ * @param {boolean} exclude_existing - Indicates whether JIDs from
  *      the new list which are also in the old list
  *      the new list which are also in the old list
  *      (regardless of affiliation) should be excluded
  *      (regardless of affiliation) should be excluded
  *      from the delta. One reason to do this
  *      from the delta. One reason to do this
  *      would be when you want to add a JID only if it
  *      would be when you want to add a JID only if it
  *      doesn't have *any* existing affiliation at all.
  *      doesn't have *any* existing affiliation at all.
- * @param { boolean } remove_absentees - Indicates whether JIDs
+ * @param {boolean} remove_absentees - Indicates whether JIDs
  *      from the old list which are not in the new list
  *      from the old list which are not in the new list
  *      should be considered removed and therefore be
  *      should be considered removed and therefore be
  *      included in the delta with affiliation set
  *      included in the delta with affiliation set
  *      to 'none'.
  *      to 'none'.
- * @param { array } new_list - Array containing the new affiliations
- * @param { array } old_list - Array containing the old affiliations
- * @returns { array }
+ * @param {array} new_list - Array containing the new affiliations
+ * @param {array} old_list - Array containing the old affiliations
+ * @returns {array}
  */
  */
 export function computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
 export function computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
     const new_jids = new_list.map(o => o.jid);
     const new_jids = new_list.map(o => o.jid);

+ 25 - 20
src/headless/plugins/muc/api.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('./muc.js').default} MUC
+ */
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 import log from '../../log';
 import log from '../../log';
@@ -22,15 +25,16 @@ export default {
          * the chatroom in the background (i.e. doesn't cause a view to open).
          * the chatroom in the background (i.e. doesn't cause a view to open).
          *
          *
          * @method api.rooms.create
          * @method api.rooms.create
-         * @param {(string[]|string)} jid|jids The JID or array of
+         * @param {(string[]|string)} jids The JID or array of
          *     JIDs of the chatroom(s) to create
          *     JIDs of the chatroom(s) to create
-         * @param { object } [attrs] attrs The room attributes
-         * @returns {Promise} Promise which resolves with the Model representing the chat.
+         * @param {object} [attrs] attrs The room attributes
+         * @returns {Promise[]} Promise which resolves with the Model representing the chat.
          */
          */
         create (jids, attrs = {}) {
         create (jids, attrs = {}) {
             attrs = typeof attrs === 'string' ? { 'nick': attrs } : attrs || {};
             attrs = typeof attrs === 'string' ? { 'nick': attrs } : attrs || {};
             if (!attrs.nick && api.settings.get('muc_nickname_from_jid')) {
             if (!attrs.nick && api.settings.get('muc_nickname_from_jid')) {
-                attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
+                const bare_jid = _converse.session.get('bare_jid');
+                attrs.nick = Strophe.getNodeFromJid(bare_jid);
             }
             }
             if (jids === undefined) {
             if (jids === undefined) {
                 throw new TypeError('rooms.create: You need to provide at least one JID');
                 throw new TypeError('rooms.create: You need to provide at least one JID');
@@ -46,30 +50,31 @@ export default {
          * Similar to {@link api.chats.open}, but for groupchats.
          * Similar to {@link api.chats.open}, but for groupchats.
          *
          *
          * @method api.rooms.open
          * @method api.rooms.open
-         * @param { string } jid The room JID or JIDs (if not specified, all
+         * @param {string|string[]} jids The room JID or JIDs (if not specified, all
          *     currently open rooms will be returned).
          *     currently open rooms will be returned).
-         * @param { string } attrs A map  containing any extra room attributes.
-         * @param { string } [attrs.nick] The current user's nickname for the MUC
-         * @param { boolean } [attrs.auto_configure] A boolean, indicating
+         * @param {object} attrs A map  containing any extra room attributes.
+         * @param {string} [attrs.nick] The current user's nickname for the MUC
+         * @param {boolean} [attrs.hidden]
+         * @param {boolean} [attrs.auto_configure] A boolean, indicating
          *     whether the room should be configured automatically or not.
          *     whether the room should be configured automatically or not.
          *     If set to `true`, then it makes sense to pass in configuration settings.
          *     If set to `true`, then it makes sense to pass in configuration settings.
-         * @param { object } [attrs.roomconfig] A map of configuration settings to be used when the room gets
+         * @param {object} [attrs.roomconfig] A map of configuration settings to be used when the room gets
          *     configured automatically. Currently it doesn't make sense to specify
          *     configured automatically. Currently it doesn't make sense to specify
          *     `roomconfig` values if `auto_configure` is set to `false`.
          *     `roomconfig` values if `auto_configure` is set to `false`.
          *     For a list of configuration values that can be passed in, refer to these values
          *     For a list of configuration values that can be passed in, refer to these values
          *     in the [XEP-0045 MUC specification](https://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner).
          *     in the [XEP-0045 MUC specification](https://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner).
          *     The values should be named without the `muc#roomconfig_` prefix.
          *     The values should be named without the `muc#roomconfig_` prefix.
-         * @param { boolean } [attrs.minimized] A boolean, indicating whether the room should be opened minimized or not.
-         * @param { boolean } [attrs.bring_to_foreground] A boolean indicating whether the room should be
+         * @param {boolean} [attrs.minimized] A boolean, indicating whether the room should be opened minimized or not.
+         * @param {boolean} [attrs.bring_to_foreground] A boolean indicating whether the room should be
          *     brought to the foreground and therefore replace the currently shown chat.
          *     brought to the foreground and therefore replace the currently shown chat.
          *     If there is no chat currently open, then this option is ineffective.
          *     If there is no chat currently open, then this option is ineffective.
-         * @param { Boolean } [force=false] - By default, a minimized
+         * @param {boolean} [force=false] - By default, a minimized
          *   room won't be maximized (in `overlayed` view mode) and in
          *   room won't be maximized (in `overlayed` view mode) and in
          *   `fullscreen` view mode a newly opened room won't replace
          *   `fullscreen` view mode a newly opened room won't replace
          *   another chat already in the foreground.
          *   another chat already in the foreground.
          *   Set `force` to `true` if you want to force the room to be
          *   Set `force` to `true` if you want to force the room to be
          *   maximized or shown.
          *   maximized or shown.
-         * @returns {Promise} Promise which resolves with the Model representing the chat.
+         * @returns {Promise<MUC[]>} Promise which resolves with the Model representing the chat.
          *
          *
          * @example
          * @example
          * api.rooms.open('group@muc.example.com')
          * api.rooms.open('group@muc.example.com')
@@ -120,14 +125,14 @@ export default {
          * Fetches the object representing a MUC chatroom (aka groupchat)
          * Fetches the object representing a MUC chatroom (aka groupchat)
          *
          *
          * @method api.rooms.get
          * @method api.rooms.get
-         * @param { String } [jid] The room JID (if not specified, all rooms will be returned).
-         * @param { Object } [attrs] A map containing any extra room attributes
+         * @param {string|string[]} [jids] The room JID (if not specified, all rooms will be returned).
+         * @param {object} [attrs] A map containing any extra room attributes
          *  to be set if `create` is set to `true`
          *  to be set if `create` is set to `true`
-         * @param { String } [attrs.nick] Specify the nickname
-         * @param { String } [attrs.password ] Specify a password if needed to enter a new room
-         * @param { Boolean } create A boolean indicating whether the room should be created
+         * @param {string} [attrs.nick] Specify the nickname
+         * @param {string} [attrs.password ] Specify a password if needed to enter a new room
+         * @param {boolean} create A boolean indicating whether the room should be created
          *     if not found (default: `false`)
          *     if not found (default: `false`)
-         * @returns { Promise<_converse.ChatRoom> }
+         * @returns {Promise<MUC[]>}
          * @example
          * @example
          * api.waitUntil('roomsAutoJoined').then(() => {
          * api.waitUntil('roomsAutoJoined').then(() => {
          *     const create_if_not_found = true;
          *     const create_if_not_found = true;
@@ -145,7 +150,7 @@ export default {
                 jid = getJIDFromURI(jid);
                 jid = getJIDFromURI(jid);
                 let model = await api.chatboxes.get(jid);
                 let model = await api.chatboxes.get(jid);
                 if (!model && create) {
                 if (!model && create) {
-                    model = await api.chatboxes.create(jid, attrs, _converse.ChatRoom);
+                    model = await api.chatboxes.create(jid, attrs, _converse.exports.MUC);
                 } else {
                 } else {
                     model = model && model.get('type') === CHATROOMS_TYPE ? model : null;
                     model = model && model.get('type') === CHATROOMS_TYPE ? model : null;
                     if (model && Object.keys(attrs).length) {
                     if (model && Object.keys(attrs).length) {

+ 23 - 15
src/headless/plugins/muc/index.js

@@ -30,7 +30,7 @@ import {
     registerDirectInvitationHandler,
     registerDirectInvitationHandler,
     routeToRoom,
     routeToRoom,
 } from './utils.js';
 } from './utils.js';
-import { computeAffiliationsDelta } from './affiliations/utils.js';
+import { computeAffiliationsDelta, getAssignableAffiliations } from './affiliations/utils.js';
 import {
 import {
     AFFILIATION_CHANGES,
     AFFILIATION_CHANGES,
     AFFILIATION_CHANGES_LIST,
     AFFILIATION_CHANGES_LIST,
@@ -145,7 +145,7 @@ converse.plugins.add('converse-muc', {
          * 322 presence     Removal from groupchat       Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member
          * 322 presence     Removal from groupchat       Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member
          * 332 presence     Removal from groupchat       Inform user that he or she is being removed from the groupchat because of a system shutdown
          * 332 presence     Removal from groupchat       Inform user that he or she is being removed from the groupchat because of a system shutdown
          */
          */
-        _converse.muc = {
+        const MUC_FEEDBACK_MESSAGES = {
             info_messages: {
             info_messages: {
                 100: __('This groupchat is not anonymous'),
                 100: __('This groupchat is not anonymous'),
                 102: __('This groupchat now shows unavailable members'),
                 102: __('This groupchat now shows unavailable members'),
@@ -177,28 +177,36 @@ converse.plugins.add('converse-muc', {
             },
             },
         };
         };
 
 
+        const labels = { muc: MUC_FEEDBACK_MESSAGES };
+        Object.assign(_converse.labels, labels);
+        Object.assign(_converse, labels); // XXX DEPRECATED
+
         routeToRoom();
         routeToRoom();
         addEventListener('hashchange', routeToRoom);
         addEventListener('hashchange', routeToRoom);
 
 
         // TODO: DEPRECATED
         // TODO: DEPRECATED
-        _converse.ChatRoom = MUC;
-        _converse.ChatRoomMessage = MUCMessage;
-        _converse.ChatRoomOccupants = ChatRoomOccupants;
-        _converse.ChatRoomOccupant = ChatRoomOccupant;
-
-        const exports = { MUC, MUCMessage, ChatRoomOccupants, ChatRoomOccupant };
-        Object.assign(_converse.exports, exports);
-
-        /** @type {module:shared-api.APIEndpoint} */(api.chatboxes.registry).add(CHATROOMS_TYPE, MUC);
-
-        Object.assign(_converse, {
+        const legacy_exports = {
+            ChatRoom: MUC,
+            ChatRoomMessage: MUCMessage,
+        };
+        Object.assign(_converse, legacy_exports);
+
+        const exports = {
+            MUC,
+            MUCMessage,
+            ChatRoomOccupants,
+            ChatRoomOccupant,
+            getAssignableAffiliations,
             getDefaultMUCNickname,
             getDefaultMUCNickname,
             isInfoVisible,
             isInfoVisible,
             onDirectMUCInvitation,
             onDirectMUCInvitation,
             ChatRoomMessages: MUCMessages,
             ChatRoomMessages: MUCMessages,
-        });
+        };
+        Object.assign(_converse.exports, exports);
+        Object.assign(_converse, exports); // XXX DEPRECATED
+
+        /** @type {module:shared-api.APIEndpoint} */(api.chatboxes.registry).add(CHATROOMS_TYPE, MUC);
 
 
-        /************************ BEGIN Event Handlers ************************/
 
 
         if (api.settings.get('allow_muc_invitations')) {
         if (api.settings.get('allow_muc_invitations')) {
             api.listen.on('connected', registerDirectInvitationHandler);
             api.listen.on('connected', registerDirectInvitationHandler);

+ 9 - 13
src/headless/plugins/muc/message.js

@@ -3,16 +3,13 @@ import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 import { Strophe } from 'strophe.js';
 import { Strophe } from 'strophe.js';
 
 
-/**
- * @namespace _converse.ChatRoomMessage
- * @memberOf _converse
- */
+
 class MUCMessage extends Message {
 class MUCMessage extends Message {
 
 
-    initialize () {
-        if (!this.checkValidity()) {
-            return;
-        }
+    async initialize () { // eslint-disable-line require-await
+        this.chatbox = this.collection?.chatbox;
+        if (!this.checkValidity()) return;
+
         if (this.get('file')) {
         if (this.get('file')) {
             this.on('change:put', () => this.uploadFile());
             this.on('change:put', () => this.uploadFile());
         }
         }
@@ -20,13 +17,12 @@ class MUCMessage extends Message {
         this.on('change:type', () => this.setOccupant());
         this.on('change:type', () => this.setOccupant());
         this.on('change:is_ephemeral', () => this.setTimerForEphemeralMessage());
         this.on('change:is_ephemeral', () => this.setTimerForEphemeralMessage());
 
 
-        this.chatbox = this.collection?.chatbox;
         this.setTimerForEphemeralMessage();
         this.setTimerForEphemeralMessage();
         this.setOccupant();
         this.setOccupant();
         /**
         /**
-         * Triggered once a { @link _converse.ChatRoomMessage } has been created and initialized.
+         * Triggered once a { @link MUCMessage} has been created and initialized.
          * @event _converse#chatRoomMessageInitialized
          * @event _converse#chatRoomMessageInitialized
-         * @type { _converse.ChatRoomMessages}
+         * @type {MUCMessage}
          * @example _converse.api.listen.on('chatRoomMessageInitialized', model => { ... });
          * @example _converse.api.listen.on('chatRoomMessageInitialized', model => { ... });
          */
          */
         api.trigger('chatRoomMessageInitialized', this);
         api.trigger('chatRoomMessageInitialized', this);
@@ -41,7 +37,7 @@ class MUCMessage extends Message {
      * based on configuration settings and server support.
      * based on configuration settings and server support.
      * @async
      * @async
      * @method _converse.ChatRoomMessages#mayBeModerated
      * @method _converse.ChatRoomMessages#mayBeModerated
-     * @returns { Boolean }
+     * @returns {boolean}
      */
      */
     mayBeModerated () {
     mayBeModerated () {
         if (typeof this.get('from_muc')  === 'undefined') {
         if (typeof this.get('from_muc')  === 'undefined') {
@@ -57,7 +53,7 @@ class MUCMessage extends Message {
     }
     }
 
 
     checkValidity () {
     checkValidity () {
-        const result = _converse.Message.prototype.checkValidity.call(this);
+        const result = _converse.exports.Message.prototype.checkValidity.call(this);
         !result && this.chatbox.debouncedRejoin();
         !result && this.chatbox.debouncedRejoin();
         return result;
         return result;
     }
     }

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

@@ -3,17 +3,11 @@ import { Collection } from '@converse/skeletor';
 
 
 /**
 /**
  * Collection which stores MUC messages
  * Collection which stores MUC messages
- * @namespace _converse.ChatRoomMessages
- * @memberOf _converse
  */
  */
 class MUCMessages extends Collection {
 class MUCMessages extends Collection {
 
 
-    get comparator () {
-        return 'time';
-    }
-
-    constructor () {
-        super();
+    constructor (attrs, options={}) {
+        super(attrs, Object.assign({ comparator: 'time' }, options));
         this.model = MUCMessage;
         this.model = MUCMessage;
     }
     }
 }
 }

+ 168 - 118
src/headless/plugins/muc/muc.js

@@ -1,6 +1,11 @@
 /**
 /**
- * @typedef {import('../muc/message.js').default} MUCMessage
+ * @module:headless-plugins-muc-muc
+ * @typedef {import('./message.js').default} MUCMessage
+ * @typedef {import('./occupant.js').default} ChatRoomOccupant
+ * @typedef {import('./affiliations/utils.js').NonOutcastAffiliation} NonOutcastAffiliation
+ * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem
  * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes
  * @typedef {module:plugin-chat-parsers.MessageAttributes} MessageAttributes
+ * @typedef {module:plugin-muc-parsers.MUCMessageAttributes} MUCMessageAttributes
  * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder
  * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder
  */
  */
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
@@ -13,7 +18,7 @@ import pick from 'lodash-es/pick';
 import sizzle from 'sizzle';
 import sizzle from 'sizzle';
 import { Model } from '@converse/skeletor';
 import { Model } from '@converse/skeletor';
 import { ROOMSTATUS } from './constants.js';
 import { ROOMSTATUS } from './constants.js';
-import { CHATROOMS_TYPE } from '../../shared/constants.js';
+import { CHATROOMS_TYPE, GONE } from '../../shared/constants.js';
 import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js';
 import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js';
 import { TimeoutError } from '../../shared/errors.js';
 import { TimeoutError } from '../../shared/errors.js';
 import { computeAffiliationsDelta, setAffiliations, getAffiliationList }  from './affiliations/utils.js';
 import { computeAffiliationsDelta, setAffiliations, getAffiliationList }  from './affiliations/utils.js';
@@ -21,7 +26,7 @@ import { getOpenPromise } from '@converse/openpromise';
 import { handleCorrection } from '../../shared/chat/utils.js';
 import { handleCorrection } from '../../shared/chat/utils.js';
 import { initStorage, createStore } from '../../utils/storage.js';
 import { initStorage, createStore } from '../../utils/storage.js';
 import { isArchived, getMediaURLsMetadata } from '../../shared/parsers.js';
 import { isArchived, getMediaURLsMetadata } from '../../shared/parsers.js';
-import { getUniqueId, safeSave } from '../../utils/index.js';
+import { getUniqueId, isErrorObject, safeSave } from '../../utils/index.js';
 import { isUniView } from '../../utils/session.js';
 import { isUniView } from '../../utils/session.js';
 import { parseMUCMessage, parseMUCPresence } from './parsers.js';
 import { parseMUCMessage, parseMUCPresence } from './parsers.js';
 import { sendMarker } from '../../shared/actions.js';
 import { sendMarker } from '../../shared/actions.js';
@@ -84,7 +89,7 @@ class MUC extends ChatBox {
             // user.
             // user.
             //
             //
             // To keep things simple, we reuse `num_unread` from
             // To keep things simple, we reuse `num_unread` from
-            // _converse.ChatBox to indicate unread messages which
+            // ChatBox to indicate unread messages which
             // mention the user and `num_unread_general` to indicate
             // mention the user and `num_unread_general` to indicate
             // generally unread messages (which *includes* mentions!).
             // generally unread messages (which *includes* mentions!).
             'num_unread_general': 0,
             'num_unread_general': 0,
@@ -153,7 +158,7 @@ class MUC extends ChatBox {
      * Checks whether we're still joined and if so, restores the MUC state from cache.
      * Checks whether we're still joined and if so, restores the MUC state from cache.
      * @private
      * @private
      * @method MUC#restoreFromCache
      * @method MUC#restoreFromCache
-     * @returns { Boolean } Returns `true` if we're still joined, otherwise returns `false`.
+     * @returns {Promise<boolean>} Returns `true` if we're still joined, otherwise returns `false`.
      */
      */
     async restoreFromCache () {
     async restoreFromCache () {
         if (this.isEntered()) {
         if (this.isEntered()) {
@@ -217,6 +222,9 @@ class MUC extends ChatBox {
         return this.join();
         return this.join();
     }
     }
 
 
+    /**
+     * @param {string} password
+     */
     async constructJoinPresence (password) {
     async constructJoinPresence (password) {
         let stanza = $pres({
         let stanza = $pres({
             'id': getUniqueId(),
             'id': getUniqueId(),
@@ -235,8 +243,7 @@ class MUC extends ChatBox {
          /**
          /**
           * *Hook* which allows plugins to update an outgoing MUC join presence stanza
           * *Hook* which allows plugins to update an outgoing MUC join presence stanza
           * @event _converse#constructedMUCPresence
           * @event _converse#constructedMUCPresence
-          * @param { MUC } - The MUC from which this message stanza is being sent.
-          * @param { Element } stanza - The stanza which will be sent out
+          * @type {Element} The stanza which will be sent out
           */
           */
         stanza = await api.hook('constructedMUCPresence', this, stanza);
         stanza = await api.hook('constructedMUCPresence', this, stanza);
         return stanza;
         return stanza;
@@ -388,18 +395,20 @@ class MUC extends ChatBox {
     }
     }
 
 
     getMessagesCollection () {
     getMessagesCollection () {
-        return new _converse.ChatRoomMessages();
+        return new _converse.exports.ChatRoomMessages();
     }
     }
 
 
     restoreSession () {
     restoreSession () {
-        const id = `muc.session-${_converse.bare_jid}-${this.get('jid')}`;
+        const bare_jid = _converse.session.get('bare_jid');
+        const id = `muc.session-${bare_jid}-${this.get('jid')}`;
         this.session = new MUCSession({ id });
         this.session = new MUCSession({ id });
         initStorage(this.session, id, 'session');
         initStorage(this.session, id, 'session');
         return new Promise(r => this.session.fetch({ 'success': r, 'error': r }));
         return new Promise(r => this.session.fetch({ 'success': r, 'error': r }));
     }
     }
 
 
     initDiscoModels () {
     initDiscoModels () {
-        let id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`;
+        const bare_jid = _converse.session.get('bare_jid');
+        let id = `converse.muc-features-${bare_jid}-${this.get('jid')}`;
         this.features = new Model(
         this.features = new Model(
             Object.assign(
             Object.assign(
                 { id },
                 { id },
@@ -412,15 +421,16 @@ class MUC extends ChatBox {
         this.features.browserStorage = createStore(id, 'session');
         this.features.browserStorage = createStore(id, 'session');
         this.features.listenTo(_converse, 'beforeLogout', () => this.features.browserStorage.flush());
         this.features.listenTo(_converse, 'beforeLogout', () => this.features.browserStorage.flush());
 
 
-        id = `converse.muc-config-${_converse.bare_jid}-${this.get('jid')}`;
+        id = `converse.muc-config-${bare_jid}-${this.get('jid')}`;
         this.config = new Model({ id });
         this.config = new Model({ id });
         this.config.browserStorage = createStore(id, 'session');
         this.config.browserStorage = createStore(id, 'session');
         this.config.listenTo(_converse, 'beforeLogout', () => this.config.browserStorage.flush());
         this.config.listenTo(_converse, 'beforeLogout', () => this.config.browserStorage.flush());
     }
     }
 
 
     initOccupants () {
     initOccupants () {
-        this.occupants = new _converse.ChatRoomOccupants();
-        const id = `converse.occupants-${_converse.bare_jid}${this.get('jid')}`;
+        this.occupants = new _converse.exports.ChatRoomOccupants();
+        const bare_jid = _converse.session.get('bare_jid');
+        const id = `converse.occupants-${bare_jid}${this.get('jid')}`;
         this.occupants.browserStorage = createStore(id, 'session');
         this.occupants.browserStorage = createStore(id, 'session');
         this.occupants.chatroom = this;
         this.occupants.chatroom = this;
         this.occupants.listenTo(_converse, 'beforeLogout', () => this.occupants.browserStorage.flush());
         this.occupants.listenTo(_converse, 'beforeLogout', () => this.occupants.browserStorage.flush());
@@ -464,6 +474,9 @@ class MUC extends ChatBox {
         }
         }
     }
     }
 
 
+    /**
+     * @param {Element} stanza
+     */
     async handleErrorMessageStanza (stanza) {
     async handleErrorMessageStanza (stanza) {
         const { __ } = _converse;
         const { __ } = _converse;
         const attrs = await parseMUCMessage(stanza, this);
         const attrs = await parseMUCMessage(stanza, this);
@@ -568,7 +581,7 @@ class MUC extends ChatBox {
      * Parses an incoming message stanza and queues it for processing.
      * Parses an incoming message stanza and queues it for processing.
      * @private
      * @private
      * @method MUC#handleMessageStanza
      * @method MUC#handleMessageStanza
-     * @param { Element } stanza
+     * @param {Strophe.Builder|Element} stanza
      */
      */
     async handleMessageStanza (stanza) {
     async handleMessageStanza (stanza) {
         stanza = stanza.tree?.() ?? stanza;
         stanza = stanza.tree?.() ?? stanza;
@@ -589,13 +602,6 @@ class MUC extends ChatBox {
         } else if (!type) {
         } else if (!type) {
             return this.handleForwardedMentions(stanza);
             return this.handleForwardedMentions(stanza);
         }
         }
-        /**
-         * @typedef { Object } MUCMessageData
-         * An object containing the parsed {@link MUCMessageAttributes} and
-         * current {@link ChatRoom}.
-         * @property { MUCMessageAttributes } attrs
-         * @property { ChatRoom } chatbox
-         */
         let attrs;
         let attrs;
         try {
         try {
             attrs = await parseMUCMessage(stanza, this);
             attrs = await parseMUCMessage(stanza, this);
@@ -604,10 +610,15 @@ class MUC extends ChatBox {
         }
         }
         const data = { stanza, attrs, 'chatbox': this };
         const data = { stanza, attrs, 'chatbox': this };
         /**
         /**
+         * An object containing the parsed {@link MUCMessageAttributes} and current {@link MUC}.
+         * @typedef {object} MUCMessageData
+         * @property {MUCMessageAttributes} attrs
+         * @property {MUC} chatbox
+         *
          * Triggered when a groupchat message stanza has been received and parsed.
          * Triggered when a groupchat message stanza has been received and parsed.
          * @event _converse#message
          * @event _converse#message
-         * @type { object }
-         * @property { module:converse-muc~MUCMessageData } data
+         * @type {object}
+         * @property {MUCMessageData} data
          */
          */
         api.trigger('message', data);
         api.trigger('message', data);
         return attrs && this.queueMessage(attrs);
         return attrs && this.queueMessage(attrs);
@@ -624,7 +635,10 @@ class MUC extends ChatBox {
         this.removeHandlers();
         this.removeHandlers();
         const connection = api.connection.get();
         const connection = api.connection.get();
         this.presence_handler = connection.addHandler(
         this.presence_handler = connection.addHandler(
-            stanza => this.onPresence(stanza) || true,
+            /** @param {Element} stanza */(stanza) => {
+                this.onPresence(stanza);
+                return true;
+            },
             null,
             null,
             'presence',
             'presence',
             null,
             null,
@@ -634,7 +648,10 @@ class MUC extends ChatBox {
         );
         );
 
 
         this.domain_presence_handler = connection.addHandler(
         this.domain_presence_handler = connection.addHandler(
-            stanza => this.onPresenceFromMUCHost(stanza) || true,
+            /** @param {Element} stanza */(stanza) => {
+                this.onPresenceFromMUCHost(stanza);
+                return true;
+            },
             null,
             null,
             'presence',
             'presence',
             null,
             null,
@@ -643,7 +660,10 @@ class MUC extends ChatBox {
         );
         );
 
 
         this.message_handler = connection.addHandler(
         this.message_handler = connection.addHandler(
-            stanza => !!this.handleMessageStanza(stanza) || true,
+            /** @param {Element} stanza */(stanza) => {
+                this.handleMessageStanza(stanza);
+                return true;
+            },
             null,
             null,
             'message',
             'message',
             null,
             null,
@@ -653,7 +673,10 @@ class MUC extends ChatBox {
         );
         );
 
 
         this.domain_message_handler = connection.addHandler(
         this.domain_message_handler = connection.addHandler(
-            stanza => this.handleMessageFromMUCHost(stanza) || true,
+            /** @param {Element} stanza */(stanza) => {
+                this.handleMessageFromMUCHost(stanza);
+                return true;
+            },
             null,
             null,
             'message',
             'message',
             null,
             null,
@@ -662,7 +685,10 @@ class MUC extends ChatBox {
         );
         );
 
 
         this.affiliation_message_handler = connection.addHandler(
         this.affiliation_message_handler = connection.addHandler(
-            stanza => this.handleAffiliationChangedMessage(stanza) || true,
+            (stanza) => {
+                this.handleAffiliationChangedMessage(stanza);
+                return true;
+            },
             Strophe.NS.MUC_USER,
             Strophe.NS.MUC_USER,
             'message',
             'message',
             null,
             null,
@@ -721,18 +747,17 @@ class MUC extends ChatBox {
      * or error message within a specific timeout period.
      * or error message within a specific timeout period.
      * @private
      * @private
      * @method MUC#sendTimedMessage
      * @method MUC#sendTimedMessage
-     * @param { _converse.Message|Element } message
+     * @param {Strophe.Builder|Element } message
      * @returns { Promise<Element>|Promise<TimeoutError> } Returns a promise
      * @returns { Promise<Element>|Promise<TimeoutError> } Returns a promise
-     *  which resolves with the reflected message stanza or with an error stanza or {@link TimeoutError}.
+     *  which resolves with the reflected message stanza or with an error stanza or
+     *  {@link TimeoutError}.
      */
      */
-    sendTimedMessage (el) {
-        if (typeof el.tree === 'function') {
-            el = el.tree();
-        }
+    sendTimedMessage (message) {
+        const el = message instanceof Element ? message : message.tree();
         let id = el.getAttribute('id');
         let id = el.getAttribute('id');
         if (!id) {
         if (!id) {
             // inject id if not found
             // inject id if not found
-            id = this.getUniqueId('sendIQ');
+            id = getUniqueId('sendIQ');
             el.setAttribute('id', id);
             el.setAttribute('id', id);
         }
         }
         const promise = getOpenPromise();
         const promise = getOpenPromise();
@@ -755,9 +780,8 @@ class MUC extends ChatBox {
 
 
     /**
     /**
      * Retract one of your messages in this groupchat
      * Retract one of your messages in this groupchat
-     * @private
      * @method MUC#retractOwnMessage
      * @method MUC#retractOwnMessage
-     * @param { _converse.Message } message - The message which we're retracting.
+     * @param {MUCMessage} message - The message which we're retracting.
      */
      */
     async retractOwnMessage (message) {
     async retractOwnMessage (message) {
         const __ = _converse.__;
         const __ = _converse.__;
@@ -805,10 +829,9 @@ class MUC extends ChatBox {
 
 
     /**
     /**
      * Retract someone else's message in this groupchat.
      * Retract someone else's message in this groupchat.
-     * @private
      * @method MUC#retractOtherMessage
      * @method MUC#retractOtherMessage
-     * @param { _converse.ChatRoomMessage } message - The message which we're retracting.
-     * @param { string } [reason] - The reason for retracting the message.
+     * @param {MUCMessage} message - The message which we're retracting.
+     * @param {string} [reason] - The reason for retracting the message.
      * @example
      * @example
      *  const room = await api.rooms.get(jid);
      *  const room = await api.rooms.get(jid);
      *  const message = room.messages.findWhere({'body': 'Get rich quick!'});
      *  const message = room.messages.findWhere({'body': 'Get rich quick!'});
@@ -816,10 +839,11 @@ class MUC extends ChatBox {
      */
      */
     async retractOtherMessage (message, reason) {
     async retractOtherMessage (message, reason) {
         const editable = message.get('editable');
         const editable = message.get('editable');
+        const bare_jid = _converse.session.get('bare_jid');
         // Optimistic save
         // Optimistic save
         message.save({
         message.save({
             'moderated': 'retracted',
             'moderated': 'retracted',
-            'moderated_by': _converse.bare_jid,
+            'moderated_by': bare_jid,
             'moderated_id': message.get('msgid'),
             'moderated_id': message.get('msgid'),
             'moderation_reason': reason,
             'moderation_reason': reason,
             'editable': false
             'editable': false
@@ -842,8 +866,8 @@ class MUC extends ChatBox {
      * Sends an IQ stanza to the XMPP server to retract a message in this groupchat.
      * Sends an IQ stanza to the XMPP server to retract a message in this groupchat.
      * @private
      * @private
      * @method MUC#sendRetractionIQ
      * @method MUC#sendRetractionIQ
-     * @param { _converse.ChatRoomMessage } message - The message which we're retracting.
-     * @param { string } [reason] - The reason for retracting the message.
+     * @param {MUCMessage} message - The message which we're retracting.
+     * @param {string} [reason] - The reason for retracting the message.
      */
      */
     sendRetractionIQ (message, reason) {
     sendRetractionIQ (message, reason) {
         const iq = $iq({ 'to': this.get('jid'), 'type': 'set' })
         const iq = $iq({ 'to': this.get('jid'), 'type': 'set' })
@@ -904,7 +928,7 @@ class MUC extends ChatBox {
             );
             );
         }
         }
         // Delete disco entity
         // Delete disco entity
-        const disco_entity = _converse.disco_entities?.get(this.get('jid'));
+        const disco_entity = _converse.state.disco_entities?.get(this.get('jid'));
         if (disco_entity) {
         if (disco_entity) {
             await new Promise(resolve => disco_entity.destroy({
             await new Promise(resolve => disco_entity.destroy({
                 'success': resolve,
                 'success': resolve,
@@ -935,7 +959,7 @@ class MUC extends ChatBox {
                 'error': (_, e) => { log.error(e); resolve(); }
                 'error': (_, e) => { log.error(e); resolve(); }
             })
             })
         );
         );
-        return _converse.ChatBox.prototype.close.call(this);
+        return _converse.exports.ChatBox.prototype.close.call(this);
     }
     }
 
 
     canModerateMessages () {
     canModerateMessages () {
@@ -1062,7 +1086,6 @@ class MUC extends ChatBox {
     /**
     /**
      * Sends a message with the current XEP-0085 chat state of the user
      * Sends a message with the current XEP-0085 chat state of the user
      * as taken from the `chat_state` attribute of the {@link MUC}.
      * as taken from the `chat_state` attribute of the {@link MUC}.
-     * @private
      * @method MUC#sendChatState
      * @method MUC#sendChatState
      */
      */
     sendChatState () {
     sendChatState () {
@@ -1079,7 +1102,7 @@ class MUC extends ChatBox {
             return;
             return;
         }
         }
         const chat_state = this.get('chat_state');
         const chat_state = this.get('chat_state');
-        if (chat_state === _converse.GONE) {
+        if (chat_state === GONE) {
             // <gone/> is not applicable within MUC context
             // <gone/> is not applicable within MUC context
             return;
             return;
         }
         }
@@ -1226,7 +1249,8 @@ class MUC extends ChatBox {
      * we can find a value for it in this rooms config.
      * we can find a value for it in this rooms config.
      * @private
      * @private
      * @method MUC#addFieldValue
      * @method MUC#addFieldValue
-     * @returns { Element }
+     * @param {Element} field
+     * @returns {Element}
      */
      */
     addFieldValue (field) {
     addFieldValue (field) {
         const type = field.getAttribute('type');
         const type = field.getAttribute('type');
@@ -1257,7 +1281,7 @@ class MUC extends ChatBox {
      * 'roomconfig' data.
      * 'roomconfig' data.
      * @private
      * @private
      * @method MUC#autoConfigureChatRoom
      * @method MUC#autoConfigureChatRoom
-     * @returns { Promise<Element> }
+     * @returns {Promise<Element>}
      * Returns a promise which resolves once a response IQ has
      * Returns a promise which resolves once a response IQ has
      * been received.
      * been received.
      */
      */
@@ -1316,7 +1340,8 @@ class MUC extends ChatBox {
         if (!args.startsWith('@')) {
         if (!args.startsWith('@')) {
             args = '@' + args;
             args = '@' + args;
         }
         }
-        const [_text, references] = this.parseTextForReferences(args); // eslint-disable-line no-unused-vars
+        const result = this.parseTextForReferences(args);
+        const references = result[1];
         if (!references.length) {
         if (!references.length) {
             const message = __("Error: couldn't find a groupchat participant based on your arguments");
             const message = __("Error: couldn't find a groupchat participant based on your arguments");
             this.createMessage({ message, 'type': 'error' });
             this.createMessage({ message, 'type': 'error' });
@@ -1355,7 +1380,8 @@ class MUC extends ChatBox {
         if (this.config.get('changesubject') || ['owner', 'admin'].includes(this.getOwnAffiliation())) {
         if (this.config.get('changesubject') || ['owner', 'admin'].includes(this.getOwnAffiliation())) {
             allowed_commands = [...allowed_commands, ...['subject', 'topic']];
             allowed_commands = [...allowed_commands, ...['subject', 'topic']];
         }
         }
-        const occupant = this.occupants.findWhere({ 'jid': _converse.bare_jid });
+        const bare_jid = _converse.session.get('bare_jid');
+        const occupant = this.occupants.findWhere({ 'jid': bare_jid });
         if (this.verifyAffiliations(['owner'], occupant, false)) {
         if (this.verifyAffiliations(['owner'], occupant, false)) {
             allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS);
             allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS);
         } else if (this.verifyAffiliations(['admin'], occupant, false)) {
         } else if (this.verifyAffiliations(['admin'], occupant, false)) {
@@ -1383,7 +1409,8 @@ class MUC extends ChatBox {
         if (!affiliations.length) {
         if (!affiliations.length) {
             return true;
             return true;
         }
         }
-        occupant = occupant || this.occupants.findWhere({ 'jid': _converse.bare_jid });
+        const bare_jid = _converse.session.get('bare_jid');
+        occupant = occupant || this.occupants.findWhere({ 'jid': bare_jid });
         if (occupant) {
         if (occupant) {
             const a = occupant.get('affiliation');
             const a = occupant.get('affiliation');
             if (affiliations.includes(a)) {
             if (affiliations.includes(a)) {
@@ -1405,7 +1432,8 @@ class MUC extends ChatBox {
         if (!roles.length) {
         if (!roles.length) {
             return true;
             return true;
         }
         }
-        occupant = occupant || this.occupants.findWhere({ 'jid': _converse.bare_jid });
+        const bare_jid = _converse.session.get('bare_jid');
+        occupant = occupant || this.occupants.findWhere({ 'jid': bare_jid });
         if (occupant) {
         if (occupant) {
             const role = occupant.get('role');
             const role = occupant.get('role');
             if (roles.includes(role)) {
             if (roles.includes(role)) {
@@ -1426,7 +1454,7 @@ class MUC extends ChatBox {
      * @returns { ('none'|'visitor'|'participant'|'moderator') }
      * @returns { ('none'|'visitor'|'participant'|'moderator') }
      */
      */
     getOwnRole () {
     getOwnRole () {
-        return this.getOwnOccupant()?.attributes?.role;
+        return this.getOwnOccupant()?.get('role');
     }
     }
 
 
     /**
     /**
@@ -1436,14 +1464,14 @@ class MUC extends ChatBox {
      * @returns { ('none'|'outcast'|'member'|'admin'|'owner') }
      * @returns { ('none'|'outcast'|'member'|'admin'|'owner') }
      */
      */
     getOwnAffiliation () {
     getOwnAffiliation () {
-        return this.getOwnOccupant()?.attributes?.affiliation || 'none';
+        return this.getOwnOccupant()?.get('affiliation') || 'none';
     }
     }
 
 
     /**
     /**
-     * Get the {@link _converse.ChatRoomOccupant} instance which
+     * Get the {@link ChatRoomOccupant} instance which
      * represents the current user.
      * represents the current user.
      * @method MUC#getOwnOccupant
      * @method MUC#getOwnOccupant
-     * @returns { _converse.ChatRoomOccupant }
+     * @returns {ChatRoomOccupant}
      */
      */
     getOwnOccupant () {
     getOwnOccupant () {
         return this.occupants.getOwnOccupant();
         return this.occupants.getOwnOccupant();
@@ -1483,13 +1511,12 @@ class MUC extends ChatBox {
 
 
     /**
     /**
      * Send an IQ stanza to modify an occupant's role
      * Send an IQ stanza to modify an occupant's role
-     * @private
      * @method MUC#setRole
      * @method MUC#setRole
-     * @param { _converse.ChatRoomOccupant } occupant
-     * @param { String } role
-     * @param { String } reason
-     * @param { function } onSuccess - callback for a succesful response
-     * @param { function } onError - callback for an error response
+     * @param {ChatRoomOccupant} occupant
+     * @param {string} role
+     * @param {string} reason
+     * @param {function} onSuccess - callback for a succesful response
+     * @param {function} onError - callback for an error response
      */
      */
     setRole (occupant, role, reason, onSuccess, onError) {
     setRole (occupant, role, reason, onSuccess, onError) {
         const item = $build('item', {
         const item = $build('item', {
@@ -1512,10 +1539,9 @@ class MUC extends ChatBox {
     }
     }
 
 
     /**
     /**
-     * @private
      * @method MUC#getOccupant
      * @method MUC#getOccupant
-     * @param { String } nickname_or_jid - The nickname or JID of the occupant to be returned
-     * @returns { _converse.ChatRoomOccupant }
+     * @param {string} nickname_or_jid - The nickname or JID of the occupant to be returned
+     * @returns {ChatRoomOccupant}
      */
      */
     getOccupant (nickname_or_jid) {
     getOccupant (nickname_or_jid) {
         return u.isValidJID(nickname_or_jid)
         return u.isValidJID(nickname_or_jid)
@@ -1525,38 +1551,36 @@ class MUC extends ChatBox {
 
 
     /**
     /**
      * Return an array of occupant models that have the required role
      * Return an array of occupant models that have the required role
-     * @private
      * @method MUC#getOccupantsWithRole
      * @method MUC#getOccupantsWithRole
-     * @param { String } role
-     * @returns { _converse.ChatRoomOccupant[] }
+     * @param {string} role
+     * @returns {{jid: string, nick: string, role: string}[]}
      */
      */
     getOccupantsWithRole (role) {
     getOccupantsWithRole (role) {
         return this.getOccupantsSortedBy('nick')
         return this.getOccupantsSortedBy('nick')
             .filter(o => o.get('role') === role)
             .filter(o => o.get('role') === role)
             .map(item => {
             .map(item => {
                 return {
                 return {
-                    'jid': item.get('jid'),
-                    'nick': item.get('nick'),
-                    'role': item.get('role')
+                    jid: /** @type {string} */item.get('jid'),
+                    nick: /** @type {string} */item.get('nick'),
+                    role: /** @type {string} */item.get('role')
                 };
                 };
             });
             });
     }
     }
 
 
     /**
     /**
      * Return an array of occupant models that have the required affiliation
      * Return an array of occupant models that have the required affiliation
-     * @private
      * @method MUC#getOccupantsWithAffiliation
      * @method MUC#getOccupantsWithAffiliation
-     * @param { String } affiliation
-     * @returns { _converse.ChatRoomOccupant[] }
+     * @param {string} affiliation
+     * @returns {{jid: string, nick: string, affiliation: string}[]}
      */
      */
     getOccupantsWithAffiliation (affiliation) {
     getOccupantsWithAffiliation (affiliation) {
         return this.getOccupantsSortedBy('nick')
         return this.getOccupantsSortedBy('nick')
             .filter(o => o.get('affiliation') === affiliation)
             .filter(o => o.get('affiliation') === affiliation)
             .map(item => {
             .map(item => {
                 return {
                 return {
-                    'jid': item.get('jid'),
-                    'nick': item.get('nick'),
-                    'affiliation': item.get('affiliation')
+                    jid: /** @type {string} */item.get('jid'),
+                    nick: /** @type {string} */item.get('nick'),
+                    affiliation: /** @type {string} */item.get('affiliation')
                 };
                 };
             });
             });
     }
     }
@@ -1565,8 +1589,8 @@ class MUC extends ChatBox {
      * Return an array of occupant models, sorted according to the passed-in attribute.
      * Return an array of occupant models, sorted according to the passed-in attribute.
      * @private
      * @private
      * @method MUC#getOccupantsSortedBy
      * @method MUC#getOccupantsSortedBy
-     * @param { String } attr - The attribute to sort the returned array by
-     * @returns { _converse.ChatRoomOccupant[] }
+     * @param {string} attr - The attribute to sort the returned array by
+     * @returns {ChatRoomOccupant[]}
      */
      */
     getOccupantsSortedBy (attr) {
     getOccupantsSortedBy (attr) {
         return Array.from(this.occupants.models).sort((a, b) =>
         return Array.from(this.occupants.models).sort((a, b) =>
@@ -1581,18 +1605,37 @@ class MUC extends ChatBox {
      * to the XMPP server to update the member list.
      * to the XMPP server to update the member list.
      * @private
      * @private
      * @method MUC#updateMemberLists
      * @method MUC#updateMemberLists
-     * @param { object } members - Map of member jids and affiliations.
-     * @returns { Promise }
+     * @param {object} members - Map of member jids and affiliations.
+     * @returns {Promise}
      *  A promise which is resolved once the list has been
      *  A promise which is resolved once the list has been
      *  updated or once it's been established there's no need
      *  updated or once it's been established there's no need
      *  to update the list.
      *  to update the list.
      */
      */
     async updateMemberLists (members) {
     async updateMemberLists (members) {
         const muc_jid = this.get('jid');
         const muc_jid = this.get('jid');
+        /** @type {Array<NonOutcastAffiliation>} */
         const all_affiliations = ['member', 'admin', 'owner'];
         const all_affiliations = ['member', 'admin', 'owner'];
-        const aff_lists = await Promise.all(all_affiliations.map(a => getAffiliationList(a, muc_jid)));
-        const old_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
-        await setAffiliations(muc_jid, computeAffiliationsDelta(true, false, members, old_members));
+        const aff_lists = await Promise.all(all_affiliations.map((a) => getAffiliationList(a, muc_jid)));
+
+        const old_members = aff_lists.reduce(
+            /**
+             * @param {MemberListItem[]} acc
+             * @param {MemberListItem[]|Error} val
+             * @returns {MemberListItem[]}
+             */
+            (acc, val) => {
+                if (val instanceof Error) {
+                    log.error(val);
+                    return acc;
+                }
+                return [...val, ...acc];
+            }, []
+        );
+
+        await setAffiliations(
+            muc_jid,
+            computeAffiliationsDelta(true, false, members, /** @type {MemberListItem[]} */(old_members))
+        );
         await this.occupants.fetchMembers();
         await this.occupants.fetchMembers();
     }
     }
 
 
@@ -1600,12 +1643,12 @@ class MUC extends ChatBox {
      * Given a nick name, save it to the model state, otherwise, look
      * Given a nick name, save it to the model state, otherwise, look
      * for a server-side reserved nickname or default configured
      * for a server-side reserved nickname or default configured
      * nickname and if found, persist that to the model state.
      * nickname and if found, persist that to the model state.
-     * @private
      * @method MUC#getAndPersistNickname
      * @method MUC#getAndPersistNickname
-     * @returns { Promise<string> } A promise which resolves with the nickname
+     * @param {string} nick
+     * @returns {Promise<string>} A promise which resolves with the nickname
      */
      */
     async getAndPersistNickname (nick) {
     async getAndPersistNickname (nick) {
-        nick = nick || this.get('nick') || (await this.getReservedNick()) || _converse.getDefaultMUCNickname();
+        nick = nick || this.get('nick') || (await this.getReservedNick()) || _converse.exports.getDefaultMUCNickname();
         if (nick) safeSave(this, { nick }, { 'silent': true });
         if (nick) safeSave(this, { nick }, { 'silent': true });
         return nick;
         return nick;
     }
     }
@@ -1628,7 +1671,7 @@ class MUC extends ChatBox {
             'node': 'x-roomuser-item'
             'node': 'x-roomuser-item'
         });
         });
         const result = await api.sendIQ(stanza, null, false);
         const result = await api.sendIQ(stanza, null, false);
-        if (u.isErrorObject(result)) {
+        if (isErrorObject(result)) {
             throw result;
             throw result;
         }
         }
         // Result might be undefined due to a timeout
         // Result might be undefined due to a timeout
@@ -1794,7 +1837,6 @@ class MUC extends ChatBox {
     /**
     /**
      * Given two JIDs, which can be either user JIDs or MUC occupant JIDs,
      * Given two JIDs, which can be either user JIDs or MUC occupant JIDs,
      * determine whether they belong to the same user.
      * determine whether they belong to the same user.
-     * @private
      * @method MUC#isSameUser
      * @method MUC#isSameUser
      * @param { String } jid1
      * @param { String } jid1
      * @param { String } jid2
      * @param { String } jid2
@@ -1915,14 +1957,14 @@ class MUC extends ChatBox {
      * Determines whether the message is from ourselves by checking
      * Determines whether the message is from ourselves by checking
      * the `from` attribute. Doesn't check the `type` attribute.
      * the `from` attribute. Doesn't check the `type` attribute.
      * @method MUC#isOwnMessage
      * @method MUC#isOwnMessage
-     * @param {Object|Element|_converse.Message} msg
+     * @param {Object|Element|MUCMessage} msg
      * @returns {boolean}
      * @returns {boolean}
      */
      */
     isOwnMessage (msg) {
     isOwnMessage (msg) {
         let from;
         let from;
         if (msg instanceof Element) {
         if (msg instanceof Element) {
             from = msg.getAttribute('from');
             from = msg.getAttribute('from');
-        } else if (msg instanceof _converse.Message) {
+        } else if (msg instanceof _converse.exports.MUCMessage) {
             from = msg.get('from');
             from = msg.get('from');
         } else {
         } else {
             from = msg.from;
             from = msg.from;
@@ -1932,7 +1974,7 @@ class MUC extends ChatBox {
 
 
     getUpdatedMessageAttributes (message, attrs) {
     getUpdatedMessageAttributes (message, attrs) {
         const new_attrs = {
         const new_attrs = {
-            ..._converse.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, attrs),
+            ..._converse.exports.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, attrs),
             ...pick(attrs, ['from_muc', 'occupant_id']),
             ...pick(attrs, ['from_muc', 'occupant_id']),
         }
         }
 
 
@@ -1975,7 +2017,7 @@ class MUC extends ChatBox {
      */
      */
     async sendStatusPresence (type, status, child_nodes) {
     async sendStatusPresence (type, status, child_nodes) {
         if (this.session.get('connection_status') === ROOMSTATUS.ENTERED) {
         if (this.session.get('connection_status') === ROOMSTATUS.ENTERED) {
-            const presence = await _converse.xmppstatus.constructPresence(type, this.getRoomJIDAndNick(), status);
+            const presence = await _converse.state.xmppstatus.constructPresence(type, this.getRoomJIDAndNick(), status);
             child_nodes?.map(c => c?.tree() ?? c).forEach(c => presence.cnode(c).up());
             child_nodes?.map(c => c?.tree() ?? c).forEach(c => presence.cnode(c).up());
             api.send(presence);
             api.send(presence);
         }
         }
@@ -1998,8 +2040,8 @@ class MUC extends ChatBox {
     }
     }
 
 
     /**
     /**
-     * @private
      * @method MUC#shouldShowErrorMessage
      * @method MUC#shouldShowErrorMessage
+     * @param {object} attrs
      * @returns {Promise<boolean>}
      * @returns {Promise<boolean>}
      */
      */
     async shouldShowErrorMessage (attrs) {
     async shouldShowErrorMessage (attrs) {
@@ -2013,7 +2055,7 @@ class MUC extends ChatBox {
         } else if (attrs.error_condition === 'not-acceptable' && (await this.rejoinIfNecessary())) {
         } else if (attrs.error_condition === 'not-acceptable' && (await this.rejoinIfNecessary())) {
             return false;
             return false;
         }
         }
-        return _converse.ChatBox.prototype.shouldShowErrorMessage.call(this, attrs);
+        return _converse.exports.ChatBox.prototype.shouldShowErrorMessage.call(this, attrs);
     }
     }
 
 
     /**
     /**
@@ -2025,7 +2067,7 @@ class MUC extends ChatBox {
      * @method MUC#findDanglingModeration
      * @method MUC#findDanglingModeration
      * @param { object } attrs - Attributes representing a received
      * @param { object } attrs - Attributes representing a received
      *  message, as returned by {@link parseMUCMessage}
      *  message, as returned by {@link parseMUCMessage}
-     * @returns { _converse.ChatRoomMessage }
+     * @returns {MUCMessage}
      */
      */
     findDanglingModeration (attrs) {
     findDanglingModeration (attrs) {
         if (!this.messages.length) {
         if (!this.messages.length) {
@@ -2112,7 +2154,7 @@ class MUC extends ChatBox {
                     return `${result}${__('%1$s is typing', actors[0])}\n`;
                     return `${result}${__('%1$s is typing', actors[0])}\n`;
                 } else if (state === 'paused') {
                 } else if (state === 'paused') {
                     return `${result}${__('%1$s has stopped typing', actors[0])}\n`;
                     return `${result}${__('%1$s has stopped typing', actors[0])}\n`;
-                } else if (state === _converse.GONE) {
+                } else if (state === GONE) {
                     return `${result}${__('%1$s has gone away', actors[0])}\n`;
                     return `${result}${__('%1$s has gone away', actors[0])}\n`;
                 } else if (state === 'entered') {
                 } else if (state === 'entered') {
                     return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`;
                     return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`;
@@ -2142,7 +2184,7 @@ class MUC extends ChatBox {
                     return `${result}${__('%1$s are typing', actors_str)}\n`;
                     return `${result}${__('%1$s are typing', actors_str)}\n`;
                 } else if (state === 'paused') {
                 } else if (state === 'paused') {
                     return `${result}${__('%1$s have stopped typing', actors_str)}\n`;
                     return `${result}${__('%1$s have stopped typing', actors_str)}\n`;
-                } else if (state === _converse.GONE) {
+                } else if (state === GONE) {
                     return `${result}${__('%1$s have gone away', actors_str)}\n`;
                     return `${result}${__('%1$s have gone away', actors_str)}\n`;
                 } else if (state === 'entered') {
                 } else if (state === 'entered') {
                     return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`;
                     return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`;
@@ -2236,7 +2278,7 @@ class MUC extends ChatBox {
     /**
     /**
      * Given {@link MessageAttributes} look for XEP-0316 Room Notifications and create info
      * Given {@link MessageAttributes} look for XEP-0316 Room Notifications and create info
      * messages for them.
      * messages for them.
-     * @param { Element } stanza
+     * @param {MessageAttributes} attrs
      */
      */
     handleMEPNotification (attrs) {
     handleMEPNotification (attrs) {
         if (attrs.from !== this.get('jid') || !attrs.activities) {
         if (attrs.from !== this.get('jid') || !attrs.activities) {
@@ -2255,15 +2297,15 @@ class MUC extends ChatBox {
      * Returns an already cached message (if it exists) based on the
      * Returns an already cached message (if it exists) based on the
      * passed in attributes map.
      * passed in attributes map.
      * @method MUC#getDuplicateMessage
      * @method MUC#getDuplicateMessage
-     * @param { object } attrs - Attributes representing a received
+     * @param {object} attrs - Attributes representing a received
      *  message, as returned by {@link parseMUCMessage}
      *  message, as returned by {@link parseMUCMessage}
-     * @returns {Promise<_converse.Message>}
+     * @returns {MUCMessage}
      */
      */
     getDuplicateMessage (attrs) {
     getDuplicateMessage (attrs) {
         if (attrs.activities?.length) {
         if (attrs.activities?.length) {
             return this.messages.findWhere({'type': 'mep', 'msgid': attrs.msgid});
             return this.messages.findWhere({'type': 'mep', 'msgid': attrs.msgid});
         } else {
         } else {
-            return _converse.ChatBox.prototype.getDuplicateMessage.call(this, attrs);
+            return _converse.exports.ChatBox.prototype.getDuplicateMessage.call(this, attrs);
         }
         }
     }
     }
 
 
@@ -2273,11 +2315,11 @@ class MUC extends ChatBox {
      * shouldn't be called directly, instead {@link MUC#queueMessage}
      * shouldn't be called directly, instead {@link MUC#queueMessage}
      * should be called.
      * should be called.
      * @method MUC#onMessage
      * @method MUC#onMessage
-     * @param { MessageAttributes } attrs - A promise which resolves to the message attributes.
+     * @param {MessageAttributes} attrs - A promise which resolves to the message attributes.
      */
      */
     async onMessage (attrs) {
     async onMessage (attrs) {
         attrs = await attrs;
         attrs = await attrs;
-        if (u.isErrorObject(attrs)) {
+        if (isErrorObject(attrs)) {
             attrs.stanza && log.error(attrs.stanza);
             attrs.stanza && log.error(attrs.stanza);
             return log.error(attrs.message);
             return log.error(attrs.message);
         } else if (attrs.type === 'error' && !(await this.shouldShowErrorMessage(attrs))) {
         } else if (attrs.type === 'error' && !(await this.shouldShowErrorMessage(attrs))) {
@@ -2315,6 +2357,9 @@ class MUC extends ChatBox {
         }
         }
     }
     }
 
 
+    /**
+     * @param {Element} pres
+     */
     handleModifyError (pres) {
     handleModifyError (pres) {
         const text = pres.querySelector('error text')?.textContent;
         const text = pres.querySelector('error text')?.textContent;
         if (text) {
         if (text) {
@@ -2341,7 +2386,7 @@ class MUC extends ChatBox {
         if (!x) {
         if (!x) {
             return;
             return;
         }
         }
-        const disconnection_codes = Object.keys(_converse.muc.disconnect_messages);
+        const disconnection_codes = Object.keys(_converse.labels.muc.disconnect_messages);
         const codes = sizzle('status', x)
         const codes = sizzle('status', x)
             .map(s => s.getAttribute('code'))
             .map(s => s.getAttribute('code'))
             .filter(c => disconnection_codes.includes(c));
             .filter(c => disconnection_codes.includes(c));
@@ -2356,7 +2401,7 @@ class MUC extends ChatBox {
         const item = x.querySelector('item');
         const item = x.querySelector('item');
         const reason = item ? item.querySelector('reason')?.textContent : undefined;
         const reason = item ? item.querySelector('reason')?.textContent : undefined;
         const actor = item ? item.querySelector('actor')?.getAttribute('nick') : undefined;
         const actor = item ? item.querySelector('actor')?.getAttribute('nick') : undefined;
-        const message = _converse.muc.disconnect_messages[codes[0]];
+        const message = _converse.labels.muc.disconnect_messages[codes[0]];
         const status = codes.includes('301') ? ROOMSTATUS.BANNED : ROOMSTATUS.DISCONNECTED;
         const status = codes.includes('301') ? ROOMSTATUS.BANNED : ROOMSTATUS.DISCONNECTED;
         this.setDisconnectionState(message, reason, actor, status);
         this.setDisconnectionState(message, reason, actor, status);
     }
     }
@@ -2474,20 +2519,22 @@ class MUC extends ChatBox {
     createInfoMessage (code, stanza, is_self) {
     createInfoMessage (code, stanza, is_self) {
         const __ = _converse.__;
         const __ = _converse.__;
         const data = { 'type': 'info', 'is_ephemeral': true };
         const data = { 'type': 'info', 'is_ephemeral': true };
+        const { info_messages, new_nickname_messages } = _converse.labels.muc;
+
         if (!isInfoVisible(code)) {
         if (!isInfoVisible(code)) {
             return;
             return;
         }
         }
         if (code === '110' || (code === '100' && !is_self)) {
         if (code === '110' || (code === '100' && !is_self)) {
             return;
             return;
-        } else if (code in _converse.muc.info_messages) {
-            data.message = _converse.muc.info_messages[code];
+        } else if (code in info_messages) {
+            data.message = info_messages[code];
         } else if (!is_self && ACTION_INFO_CODES.includes(code)) {
         } else if (!is_self && ACTION_INFO_CODES.includes(code)) {
             const nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
             const nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
             const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, stanza).pop();
             const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, stanza).pop();
             data.actor = item ? item.querySelector('actor')?.getAttribute('nick') : undefined;
             data.actor = item ? item.querySelector('actor')?.getAttribute('nick') : undefined;
             data.reason = item ? item.querySelector('reason')?.textContent : undefined;
             data.reason = item ? item.querySelector('reason')?.textContent : undefined;
             data.message = this.getActionInfoMessage(code, nick, data.actor);
             data.message = this.getActionInfoMessage(code, nick, data.actor);
-        } else if (is_self && code in _converse.muc.new_nickname_messages) {
+        } else if (is_self && code in new_nickname_messages) {
             // XXX: Side-effect of setting the nick. Should ideally be refactored out of this method
             // XXX: Side-effect of setting the nick. Should ideally be refactored out of this method
             let nick;
             let nick;
             if (code === '210') {
             if (code === '210') {
@@ -2496,7 +2543,7 @@ class MUC extends ChatBox {
                 nick = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, stanza).pop().getAttribute('nick');
                 nick = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, stanza).pop().getAttribute('nick');
             }
             }
             this.save('nick', nick);
             this.save('nick', nick);
-            data.message = __(_converse.muc.new_nickname_messages[code], nick);
+            data.message = __(new_nickname_messages[code], nick);
         }
         }
 
 
         if (data.message) {
         if (data.message) {
@@ -2526,11 +2573,11 @@ class MUC extends ChatBox {
     /**
     /**
      * Set parameters regarding disconnection from this room. This helps to
      * Set parameters regarding disconnection from this room. This helps to
      * communicate to the user why they were disconnected.
      * communicate to the user why they were disconnected.
-     * @param { String } message - The disconnection message, as received from (or
+     * @param {string} message - The disconnection message, as received from (or
      *  implied by) the server.
      *  implied by) the server.
-     * @param { String } reason - The reason provided for the disconnection
-     * @param { String } actor - The person (if any) responsible for this disconnection
-     * @param { number } status - The status code (see `ROOMSTATUS`)
+     * @param {string} [reason] - The reason provided for the disconnection
+     * @param {string} [actor] - The person (if any) responsible for this disconnection
+     * @param {number} [status] - The status code (see `ROOMSTATUS`)
      */
      */
     setDisconnectionState (message, reason, actor, status=ROOMSTATUS.DISCONNECTED) {
     setDisconnectionState (message, reason, actor, status=ROOMSTATUS.DISCONNECTED) {
         this.session.save({
         this.session.save({
@@ -2541,11 +2588,14 @@ class MUC extends ChatBox {
         });
         });
     }
     }
 
 
+    /**
+     * @param {Element} presence
+     */
     onNicknameClash (presence) {
     onNicknameClash (presence) {
         const __ = _converse.__;
         const __ = _converse.__;
         if (api.settings.get('muc_nickname_from_jid')) {
         if (api.settings.get('muc_nickname_from_jid')) {
             const nick = presence.getAttribute('from').split('/')[1];
             const nick = presence.getAttribute('from').split('/')[1];
-            if (nick === _converse.getDefaultMUCNickname()) {
+            if (nick === _converse.exports.getDefaultMUCNickname()) {
                 this.join(nick + '-2');
                 this.join(nick + '-2');
             } else {
             } else {
                 const del = nick.lastIndexOf('-');
                 const del = nick.lastIndexOf('-');
@@ -2587,7 +2637,7 @@ class MUC extends ChatBox {
                 this.setDisconnectionState(message, reason);
                 this.setDisconnectionState(message, reason);
             } else if (error.querySelector('forbidden')) {
             } else if (error.querySelector('forbidden')) {
                 this.setDisconnectionState(
                 this.setDisconnectionState(
-                    _converse.muc.disconnect_messages[301],
+                    _converse.labels.muc.disconnect_messages[301],
                     reason,
                     reason,
                     null,
                     null,
                     ROOMSTATUS.BANNED
                     ROOMSTATUS.BANNED
@@ -2684,7 +2734,7 @@ class MUC extends ChatBox {
      * user is the groupchat's owner.
      * user is the groupchat's owner.
      * @private
      * @private
      * @method MUC#onOwnPresence
      * @method MUC#onOwnPresence
-     * @param { Element } pres - The stanza
+     * @param {Element} stanza - The stanza
      */
      */
     async onOwnPresence (stanza) {
     async onOwnPresence (stanza) {
         await this.occupants.fetched;
         await this.occupants.fetched;

+ 47 - 24
src/headless/plugins/muc/occupants.js

@@ -1,12 +1,14 @@
+/**
+ * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem
+ */
 import ChatRoomOccupant from './occupant.js';
 import ChatRoomOccupant from './occupant.js';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
+import log from '../../log';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
-import { Collection } from '@converse/skeletor';
-import { MUC_ROLE_WEIGHTS } from './constants.js';
-import { Model } from '@converse/skeletor';
+import { Collection, Model } from '@converse/skeletor';
 import { Strophe } from 'strophe.js';
 import { Strophe } from 'strophe.js';
 import { getAffiliationList } from './affiliations/utils.js';
 import { getAffiliationList } from './affiliations/utils.js';
-import { getAutoFetchedAffiliationLists } from './utils.js';
+import { getAutoFetchedAffiliationLists, occupantsComparator } from './utils.js';
 import { getUniqueId } from '../../utils/index.js';
 import { getUniqueId } from '../../utils/index.js';
 
 
 const { u } = converse.env;
 const { u } = converse.env;
@@ -19,18 +21,17 @@ const { u } = converse.env;
  * @memberOf _converse
  * @memberOf _converse
  */
  */
 class ChatRoomOccupants extends Collection {
 class ChatRoomOccupants extends Collection {
-    model = ChatRoomOccupant;
-
-    comparator (occupant1, occupant2) { // eslint-disable-line class-methods-use-this
-        const role1 = occupant1.get('role') || 'none';
-        const role2 = occupant2.get('role') || 'none';
-        if (MUC_ROLE_WEIGHTS[role1] === MUC_ROLE_WEIGHTS[role2]) {
-            const nick1 = occupant1.getDisplayName().toLowerCase();
-            const nick2 = occupant2.getDisplayName().toLowerCase();
-            return nick1 < nick2 ? -1 : nick1 > nick2 ? 1 : 0;
-        } else {
-            return MUC_ROLE_WEIGHTS[role1] < MUC_ROLE_WEIGHTS[role2] ? -1 : 1;
-        }
+
+    constructor (attrs, options) {
+        super(
+            attrs,
+            Object.assign({ comparator: occupantsComparator }, options)
+        );
+        this.chatroom = null;
+    }
+
+    get model() {
+        return ChatRoomOccupant;
     }
     }
 
 
     create (attrs, options) {
     create (attrs, options) {
@@ -52,12 +53,31 @@ class ChatRoomOccupants extends Collection {
         }
         }
         const muc_jid = this.chatroom.get('jid');
         const muc_jid = this.chatroom.get('jid');
         const aff_lists = await Promise.all(affiliations.map(a => getAffiliationList(a, muc_jid)));
         const aff_lists = await Promise.all(affiliations.map(a => getAffiliationList(a, muc_jid)));
-        const new_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
+
+
+        const new_members = aff_lists.reduce(
+            /**
+             * @param {MemberListItem[]} acc
+             * @param {MemberListItem[]|Error} val
+             * @returns {MemberListItem[]}
+             */
+            (acc, val) => {
+                if (val instanceof Error) {
+                    log.error(val);
+                    return acc;
+                }
+                return [...val, ...acc];
+            }, []
+        );
+
         const known_affiliations = affiliations.filter(
         const known_affiliations = affiliations.filter(
             a => !u.isErrorObject(aff_lists[affiliations.indexOf(a)])
             a => !u.isErrorObject(aff_lists[affiliations.indexOf(a)])
         );
         );
-        const new_jids = new_members.map(m => m.jid).filter(m => m !== undefined);
-        const new_nicks = new_members.map(m => (!m.jid && m.nick) || undefined).filter(m => m !== undefined);
+        const new_jids = /** @type {MemberListItem[]} */(new_members).map(m => m.jid).filter(m => m !== undefined);
+
+        const new_nicks = /** @type {MemberListItem[]} */(new_members).map(
+            (m) => (!m.jid && m.nick) || undefined).filter(m => m !== undefined);
+
         const removed_members = this.filter(m => {
         const removed_members = this.filter(m => {
             return (
             return (
                 known_affiliations.includes(m.get('affiliation')) &&
                 known_affiliations.includes(m.get('affiliation')) &&
@@ -65,8 +85,10 @@ class ChatRoomOccupants extends Collection {
                 !new_jids.includes(m.get('jid'))
                 !new_jids.includes(m.get('jid'))
             );
             );
         });
         });
+
+        const bare_jid = _converse.session.get('bare_jid');
         removed_members.forEach(occupant => {
         removed_members.forEach(occupant => {
-            if (occupant.get('jid') === _converse.bare_jid) {
+            if (occupant.get('jid') === bare_jid) {
                 return;
                 return;
             } else if (occupant.get('show') === 'offline') {
             } else if (occupant.get('show') === 'offline') {
                 occupant.destroy();
                 occupant.destroy();
@@ -74,7 +96,7 @@ class ChatRoomOccupants extends Collection {
                 occupant.save('affiliation', null);
                 occupant.save('affiliation', null);
             }
             }
         });
         });
-        new_members.forEach(attrs => {
+        /** @type {MemberListItem[]} */(new_members).forEach(attrs => {
             const occupant = this.findOccupant(attrs);
             const occupant = this.findOccupant(attrs);
             occupant ? occupant.save(attrs) : this.create(attrs);
             occupant ? occupant.save(attrs) : this.create(attrs);
         });
         });
@@ -117,14 +139,15 @@ class ChatRoomOccupants extends Collection {
     }
     }
 
 
     /**
     /**
-     * Get the {@link _converse.ChatRoomOccupant} instance which
+     * Get the {@link ChatRoomOccupant} instance which
      * represents the current user.
      * represents the current user.
      * @method _converse.ChatRoomOccupants#getOwnOccupant
      * @method _converse.ChatRoomOccupants#getOwnOccupant
-     * @returns { _converse.ChatRoomOccupant }
+     * @returns {ChatRoomOccupant}
      */
      */
     getOwnOccupant () {
     getOwnOccupant () {
+        const bare_jid = _converse.session.get('bare_jid');
         return this.findOccupant({
         return this.findOccupant({
-            'jid': _converse.bare_jid,
+            'jid': bare_jid,
             'occupant_id': this.chatroom.get('occupant_id')
             'occupant_id': this.chatroom.get('occupant_id')
         });
         });
     }
     }

+ 42 - 40
src/headless/plugins/muc/parsers.js

@@ -1,3 +1,8 @@
+/**
+ * @module:plugin-muc-parsers
+ * @typedef {import('../muc/muc.js').default} MUC
+ * @typedef {module:plugin-muc-parsers.MUCMessageAttributes} MUCMessageAttributes
+ */
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
@@ -72,10 +77,9 @@ function getJIDFromMUCUserData (stanza) {
 
 
 /**
 /**
  * @private
  * @private
- * @param { Element } stanza - The message stanza
- * @param { Element } original_stanza - The original stanza, that contains the
+ * @param {Element} stanza - The message stanza
  *  message stanza, if it was contained, otherwise it's the message stanza itself.
  *  message stanza, if it was contained, otherwise it's the message stanza itself.
- * @returns { Object }
+ * @returns {Object}
  */
  */
 function getModerationAttributes (stanza) {
 function getModerationAttributes (stanza) {
     const fastening = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
     const fastening = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
@@ -121,9 +125,9 @@ function getOccupantID (stanza, chatbox) {
 /**
 /**
  * Determines whether the sender of this MUC message is the current user or
  * Determines whether the sender of this MUC message is the current user or
  * someone else.
  * someone else.
- * @param { MUCMessageAttributes } attrs
- * @param { _converse.ChatRoom } chatbox
- * @returns { 'me'|'them' }
+ * @param {MUCMessageAttributes} attrs
+ * @param {MUC} chatbox
+ * @returns {'me'|'them'}
  */
  */
 function getSender (attrs, chatbox) {
 function getSender (attrs, chatbox) {
     let is_me;
     let is_me;
@@ -132,7 +136,8 @@ function getSender (attrs, chatbox) {
     if (own_occupant_id) {
     if (own_occupant_id) {
         is_me = attrs.occupant_id === own_occupant_id;
         is_me = attrs.occupant_id === own_occupant_id;
     } else if (attrs.from_real_jid) {
     } else if (attrs.from_real_jid) {
-        is_me = Strophe.getBareJidFromJid(attrs.from_real_jid) === _converse.bare_jid;
+        const bare_jid = _converse.session.get('bare_jid');
+        is_me = Strophe.getBareJidFromJid(attrs.from_real_jid) === bare_jid;
     } else {
     } else {
         is_me = attrs.nick === chatbox.get('nick')
         is_me = attrs.nick === chatbox.get('nick')
     }
     }
@@ -141,12 +146,9 @@ function getSender (attrs, chatbox) {
 
 
 /**
 /**
  * Parses a passed in message stanza and returns an object of attributes.
  * Parses a passed in message stanza and returns an object of attributes.
- * @param { Element } stanza - The message stanza
- * @param { Element } original_stanza - The original stanza, that contains the
- *  message stanza, if it was contained, otherwise it's the message stanza itself.
- * @param { _converse.ChatRoom } chatbox
- * @param { _converse } _converse
- * @returns { Promise<MUCMessageAttributes|Error> }
+ * @param {Element} stanza - The message stanza
+ * @param {MUC} chatbox
+ * @returns {Promise<MUCMessageAttributes|Error>}
  */
  */
 export async function parseMUCMessage (stanza, chatbox) {
 export async function parseMUCMessage (stanza, chatbox) {
     throwErrorIfInvalidForward(stanza);
     throwErrorIfInvalidForward(stanza);
@@ -257,7 +259,7 @@ export async function parseMUCMessage (stanza, chatbox) {
         getOpenGraphMetadata(stanza),
         getOpenGraphMetadata(stanza),
         getRetractionAttributes(stanza, original_stanza),
         getRetractionAttributes(stanza, original_stanza),
         getModerationAttributes(stanza),
         getModerationAttributes(stanza),
-        getEncryptionAttributes(stanza, _converse),
+        getEncryptionAttributes(stanza),
     );
     );
 
 
     await api.emojis.initialize();
     await api.emojis.initialize();
@@ -303,20 +305,19 @@ export async function parseMUCMessage (stanza, chatbox) {
 /**
 /**
  * Given an IQ stanza with a member list, create an array of objects containing
  * Given an IQ stanza with a member list, create an array of objects containing
  * known member data (e.g. jid, nick, role, affiliation).
  * known member data (e.g. jid, nick, role, affiliation).
- * @private
+ *
+ * @typedef {Object} MemberListItem
+ * Either the JID or the nickname (or both) will be available.
+ * @property {string} affiliation
+ * @property {string} [role]
+ * @property {string} [jid]
+ * @property {string} [nick]
+ *
  * @method muc_utils#parseMemberListIQ
  * @method muc_utils#parseMemberListIQ
  * @returns { MemberListItem[] }
  * @returns { MemberListItem[] }
  */
  */
 export function parseMemberListIQ (iq) {
 export function parseMemberListIQ (iq) {
     return sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq).map(item => {
     return sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq).map(item => {
-        /**
-         * @typedef {Object} MemberListItem
-         * Either the JID or the nickname (or both) will be available.
-         * @property {string} affiliation
-         * @property {string} [role]
-         * @property {string} [jid]
-         * @property {string} [nick]
-         */
         const data = {
         const data = {
             'affiliation': item.getAttribute('affiliation')
             'affiliation': item.getAttribute('affiliation')
         };
         };
@@ -343,21 +344,28 @@ export function parseMemberListIQ (iq) {
 /**
 /**
  * Parses a passed in MUC presence stanza and returns an object of attributes.
  * Parses a passed in MUC presence stanza and returns an object of attributes.
  * @method parseMUCPresence
  * @method parseMUCPresence
- * @param { Element } stanza - The presence stanza
- * @param { _converse.ChatRoom } chatbox
- * @returns { MUCPresenceAttributes }
+ * @param {Element} stanza - The presence stanza
+ * @param {MUC} chatbox
+ * @returns {MUCPresenceAttributes}
  */
  */
 export function parseMUCPresence (stanza, chatbox) {
 export function parseMUCPresence (stanza, chatbox) {
     /**
     /**
-     * @typedef { Object } MUCPresenceAttributes
+     * Object representing a XEP-0371 Hat
+     * @typedef {Object} MUCHat
+     * @property {string} title
+     * @property {string} uri
+     *
      * The object which {@link parseMUCPresence} returns
      * The object which {@link parseMUCPresence} returns
-     * @property { ("offline|online") } show
-     * @property { Array<MUCHat> } hats - An array of XEP-0317 hats
-     * @property { Array<string> } states
-     * @property { String } from - The sender JID (${muc_jid}/${nick})
-     * @property { String } nick - The nickname of the sender
-     * @property { String } occupant_id - The XEP-0421 occupant ID
-     * @property { String } type - The type of presence
+     * @typedef {Object} MUCPresenceAttributes
+     * @property {string} show
+     * @property {Array<MUCHat>} hats - An array of XEP-0317 hats
+     * @property {Array<string>} states
+     * @property {String} from - The sender JID (${muc_jid}/${nick})
+     * @property {String} nick - The nickname of the sender
+     * @property {String} occupant_id - The XEP-0421 occupant ID
+     * @property {String} type - The type of presence
+     * @property {String} [jid]
+     * @property {boolean} [is_me]
      */
      */
     const from = stanza.getAttribute('from');
     const from = stanza.getAttribute('from');
     const type = stanza.getAttribute('type');
     const type = stanza.getAttribute('type');
@@ -391,12 +399,6 @@ export function parseMUCPresence (stanza, chatbox) {
         } else if (child.matches('x') && child.getAttribute('xmlns') === Strophe.NS.VCARDUPDATE) {
         } else if (child.matches('x') && child.getAttribute('xmlns') === Strophe.NS.VCARDUPDATE) {
             data.image_hash = child.querySelector('photo')?.textContent;
             data.image_hash = child.querySelector('photo')?.textContent;
         } else if (child.matches('hats') && child.getAttribute('xmlns') === Strophe.NS.MUC_HATS) {
         } else if (child.matches('hats') && child.getAttribute('xmlns') === Strophe.NS.MUC_HATS) {
-            /**
-             * @typedef { Object } MUCHat
-             * Object representing a XEP-0371 Hat
-             * @property { String } title
-             * @property { String } uri
-             */
             data['hats'] = Array.from(child.children).map(
             data['hats'] = Array.from(child.children).map(
                 c =>
                 c =>
                     c.matches('hat') && {
                     c.matches('hat') && {

+ 33 - 16
src/headless/plugins/muc/utils.js

@@ -1,9 +1,13 @@
+/**
+ * @typedef {import('@converse/skeletor').Model} Model
+ */
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
 import log from '../../log.js';
 import log from '../../log.js';
-import { ROLES } from './constants.js';
+import { ROLES, MUC_ROLE_WEIGHTS } from './constants.js';
 import { safeSave } from '../../utils/index.js';
 import { safeSave } from '../../utils/index.js';
 import { CHATROOMS_TYPE } from '../../shared/constants.js';
 import { CHATROOMS_TYPE } from '../../shared/constants.js';
+import { getUnloadEvent } from '../../utils/session.js';
 
 
 const { Strophe, sizzle, u } = converse.env;
 const { Strophe, sizzle, u } = converse.env;
 
 
@@ -15,16 +19,27 @@ export function shouldCreateGroupchatMessage (attrs) {
     return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone);
     return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone);
 }
 }
 
 
-
 export function getAutoFetchedAffiliationLists () {
 export function getAutoFetchedAffiliationLists () {
     const affs = api.settings.get('muc_fetch_members');
     const affs = api.settings.get('muc_fetch_members');
     return Array.isArray(affs) ? affs : affs ? ['member', 'admin', 'owner'] : [];
     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';
+    if (MUC_ROLE_WEIGHTS[role1] === MUC_ROLE_WEIGHTS[role2]) {
+        const nick1 = occupant1.getDisplayName().toLowerCase();
+        const nick2 = occupant2.getDisplayName().toLowerCase();
+        return nick1 < nick2 ? -1 : nick1 > nick2 ? 1 : 0;
+    } else {
+        return MUC_ROLE_WEIGHTS[role1] < MUC_ROLE_WEIGHTS[role2] ? -1 : 1;
+    }
+}
+
 /**
 /**
  * Given an occupant model, see which roles may be assigned to that user.
  * Given an occupant model, see which roles may be assigned to that user.
- * @param { Model } occupant
- * @returns { Array<('moderator'|'participant'|'visitor')> } - An array of assignable roles
+ * @param {Model} occupant
+ * @returns {typeof ROLES} - An array of assignable roles
  */
  */
 export function getAssignableRoles (occupant) {
 export function getAssignableRoles (occupant) {
     let disabled = api.settings.get('modtools_disable_assign');
     let disabled = api.settings.get('modtools_disable_assign');
@@ -41,7 +56,7 @@ export function getAssignableRoles (occupant) {
 export function registerDirectInvitationHandler () {
 export function registerDirectInvitationHandler () {
     api.connection.get().addHandler(
     api.connection.get().addHandler(
         message => {
         message => {
-            _converse.onDirectMUCInvitation(message);
+            _converse.exports.onDirectMUCInvitation(message);
             return true;
             return true;
         },
         },
         'jabber:x:conference',
         'jabber:x:conference',
@@ -54,7 +69,7 @@ export function disconnectChatRooms () {
      * disconnected, so that they will be properly entered again
      * disconnected, so that they will be properly entered again
      * when fetched from session storage.
      * when fetched from session storage.
      */
      */
-    return _converse.chatboxes
+    return _converse.state.chatboxes
         .filter(m => m.get('type') === CHATROOMS_TYPE)
         .filter(m => m.get('type') === CHATROOMS_TYPE)
         .forEach(m => m.session.save({ 'connection_status': converse.ROOMSTATUS.DISCONNECTED }));
         .forEach(m => m.session.save({ 'connection_status': converse.ROOMSTATUS.DISCONNECTED }));
 }
 }
@@ -116,7 +131,7 @@ export async function onDirectMUCInvitation (message) {
         result = true;
         result = true;
     } else {
     } else {
         // Invite request might come from someone not your roster list
         // Invite request might come from someone not your roster list
-        const contact = _converse.roster.get(from)?.getDisplayName() ?? from;
+        const contact = _converse.state.roster.get(from)?.getDisplayName() ?? from;
 
 
         /**
         /**
          * *Hook* which is used to gather confirmation whether a direct MUC
          * *Hook* which is used to gather confirmation whether a direct MUC
@@ -133,7 +148,7 @@ export async function onDirectMUCInvitation (message) {
     if (result) {
     if (result) {
         const chatroom = await openChatRoom(room_jid, { 'password': x_el.getAttribute('password') });
         const chatroom = await openChatRoom(room_jid, { 'password': x_el.getAttribute('password') });
         if (chatroom.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
         if (chatroom.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
-            _converse.chatboxes.get(room_jid).rejoin();
+            _converse.state.chatboxes.get(room_jid).rejoin();
         }
         }
     }
     }
 }
 }
@@ -141,16 +156,18 @@ export async function onDirectMUCInvitation (message) {
 export function getDefaultMUCNickname () {
 export function getDefaultMUCNickname () {
     // XXX: if anything changes here, update the docs for the
     // XXX: if anything changes here, update the docs for the
     // locked_muc_nickname setting.
     // locked_muc_nickname setting.
-    if (!_converse.xmppstatus) {
+    const { xmppstatus } = _converse.state;
+    if (!xmppstatus) {
         throw new Error(
         throw new Error(
             "Can't call _converse.getDefaultMUCNickname before the statusInitialized has been fired."
             "Can't call _converse.getDefaultMUCNickname before the statusInitialized has been fired."
         );
         );
     }
     }
-    const nick = _converse.xmppstatus.getNickname();
+    const nick = xmppstatus.getNickname();
     if (nick) {
     if (nick) {
         return nick;
         return nick;
     } else if (api.settings.get('muc_nickname_from_jid')) {
     } else if (api.settings.get('muc_nickname_from_jid')) {
-        return Strophe.unescapeNode(Strophe.getNodeFromJid(_converse.bare_jid));
+        const bare_jid = _converse.session.get('bare_jid');
+        return Strophe.unescapeNode(Strophe.getNodeFromJid(bare_jid));
     }
     }
 }
 }
 
 
@@ -178,7 +195,7 @@ export async function autoJoinRooms () {
     await Promise.all(
     await Promise.all(
         api.settings.get('auto_join_rooms').map(muc => {
         api.settings.get('auto_join_rooms').map(muc => {
             if (typeof muc === 'string') {
             if (typeof muc === 'string') {
-                if (_converse.chatboxes.where({ 'jid': muc }).length) {
+                if (_converse.state.chatboxes.where({ 'jid': muc }).length) {
                     return Promise.resolve();
                     return Promise.resolve();
                 }
                 }
                 return api.rooms.open(muc);
                 return api.rooms.open(muc);
@@ -210,13 +227,13 @@ export function onAddClientFeatures () {
 }
 }
 
 
 export function onBeforeTearDown () {
 export function onBeforeTearDown () {
-    _converse.chatboxes
+    _converse.state.chatboxes
         .where({ 'type': CHATROOMS_TYPE })
         .where({ 'type': CHATROOMS_TYPE })
         .forEach(muc => safeSave(muc.session, { 'connection_status': converse.ROOMSTATUS.DISCONNECTED }));
         .forEach(muc => safeSave(muc.session, { 'connection_status': converse.ROOMSTATUS.DISCONNECTED }));
 }
 }
 
 
 export function onStatusInitialized () {
 export function onStatusInitialized () {
-    window.addEventListener(_converse.unloadevent, () => {
+    window.addEventListener(getUnloadEvent(), () => {
         const using_websocket = api.connection.isType('websocket');
         const using_websocket = api.connection.isType('websocket');
         if (
         if (
             using_websocket &&
             using_websocket &&
@@ -234,9 +251,9 @@ export function onBeforeResourceBinding () {
     api.connection.get().addHandler(
     api.connection.get().addHandler(
         stanza => {
         stanza => {
             const muc_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
             const muc_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
-            if (!_converse.chatboxes.get(muc_jid)) {
+            if (!_converse.state.chatboxes.get(muc_jid)) {
                 api.waitUntil('chatBoxesFetched').then(async () => {
                 api.waitUntil('chatBoxesFetched').then(async () => {
-                    const muc = _converse.chatboxes.get(muc_jid);
+                    const muc = _converse.state.chatboxes.get(muc_jid);
                     if (muc) {
                     if (muc) {
                         await muc.initialized;
                         await muc.initialized;
                         muc.message_handler.run(stanza);
                         muc.message_handler.run(stanza);

+ 6 - 5
src/headless/plugins/ping/api.js

@@ -9,11 +9,11 @@ export default {
     /**
     /**
      * Pings the entity represented by the passed in JID by sending an IQ stanza to it.
      * Pings the entity represented by the passed in JID by sending an IQ stanza to it.
      * @method api.ping
      * @method api.ping
-     * @param { String } [jid] - The JID of the service to ping
+     * @param {string} [jid] - The JID of the service to ping
      *  If the ping is sent out to the user's bare JID and no response is received it will attempt to reconnect.
      *  If the ping is sent out to the user's bare JID and no response is received it will attempt to reconnect.
-     * @param { number } [timeout] - The amount of time in
+     * @param {number} [timeout] - The amount of time in
      *  milliseconds to wait for a response. The default is 10000;
      *  milliseconds to wait for a response. The default is 10000;
-     * @returns { Boolean | null }
+     * @returns {Promise<boolean|null>}
      *  Whether the pinged entity responded with a non-error IQ stanza.
      *  Whether the pinged entity responded with a non-error IQ stanza.
      *  If we already know we're not connected, no ping is sent out and `null` is returned.
      *  If we already know we're not connected, no ping is sent out and `null` is returned.
      */
      */
@@ -27,7 +27,8 @@ export default {
         // However, some servers don't advertise while still responding to pings
         // However, some servers don't advertise while still responding to pings
         // const feature = _converse.disco_entities[_converse.domain].features.findWhere({'var': Strophe.NS.PING});
         // const feature = _converse.disco_entities[_converse.domain].features.findWhere({'var': Strophe.NS.PING});
         setLastStanzaDate(new Date());
         setLastStanzaDate(new Date());
-        jid = jid || Strophe.getDomainFromJid(_converse.bare_jid);
+        const bare_jid = _converse.session.get('bare_jid');
+        jid = jid || Strophe.getDomainFromJid(bare_jid);
         const iq = $iq({
         const iq = $iq({
                 'type': 'get',
                 'type': 'get',
                 'to': jid,
                 'to': jid,
@@ -37,7 +38,7 @@ export default {
         const result = await api.sendIQ(iq, timeout || 10000, false);
         const result = await api.sendIQ(iq, timeout || 10000, false);
         if (result === null) {
         if (result === null) {
             log.warn(`Timeout while pinging ${jid}`);
             log.warn(`Timeout while pinging ${jid}`);
-            if (jid === Strophe.getDomainFromJid(_converse.bare_jid)) {
+            if (jid === Strophe.getDomainFromJid(bare_jid)) {
                 api.connection.reconnect();
                 api.connection.reconnect();
             }
             }
             return false;
             return false;

+ 1 - 1
src/headless/plugins/ping/utils.js

@@ -63,7 +63,7 @@ export function onEverySecond () {
     if (ping_interval > 0) {
     if (ping_interval > 0) {
         const now = new Date();
         const now = new Date();
         lastStanzaDate = lastStanzaDate ?? now;
         lastStanzaDate = lastStanzaDate ?? now;
-        if ((now - lastStanzaDate)/1000 > ping_interval) {
+        if ((now.valueOf() - lastStanzaDate.valueOf())/1000 > ping_interval) {
             api.ping();
             api.ping();
         }
         }
     }
     }

+ 4 - 3
src/headless/plugins/pubsub.js

@@ -2,6 +2,7 @@
  * @module converse-pubsub
  * @module converse-pubsub
  * @copyright The Converse.js contributors
  * @copyright The Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  * @license Mozilla Public License (MPLv2)
+ * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder
  */
  */
 import "./disco/index.js";
 import "./disco/index.js";
 import _converse from '../shared/_converse.js';
 import _converse from '../shared/_converse.js';
@@ -44,8 +45,9 @@ converse.plugins.add('converse-pubsub', {
                  *      the publish options precondication cannot be met.
                  *      the publish options precondication cannot be met.
                  */
                  */
                 async 'publish' (jid, node, item, options, strict_options=true) {
                 async 'publish' (jid, node, item, options, strict_options=true) {
+                    const bare_jid = _converse.session.get('bare_jid');
                     const stanza = $iq({
                     const stanza = $iq({
-                        'from': _converse.bare_jid,
+                        'from': bare_jid,
                         'type': 'set',
                         'type': 'set',
                         'to': jid
                         'to': jid
                     }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
                     }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
@@ -53,7 +55,7 @@ converse.plugins.add('converse-pubsub', {
                             .cnode(item.tree()).up().up();
                             .cnode(item.tree()).up().up();
 
 
                     if (options) {
                     if (options) {
-                        jid = jid || _converse.bare_jid;
+                        jid = jid || bare_jid;
                         if (await api.disco.supports(Strophe.NS.PUBSUB + '#publish-options', jid)) {
                         if (await api.disco.supports(Strophe.NS.PUBSUB + '#publish-options', jid)) {
                             stanza.c('publish-options')
                             stanza.c('publish-options')
                                 .c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'})
                                 .c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'})
@@ -86,6 +88,5 @@ converse.plugins.add('converse-pubsub', {
                 }
                 }
             }
             }
         });
         });
-        /************************ END API ************************/
     }
     }
 });
 });

+ 4 - 3
src/headless/plugins/roster/api.js

@@ -43,9 +43,10 @@ export default {
          */
          */
         async get (jids) {
         async get (jids) {
             await api.waitUntil('rosterContactsFetched');
             await api.waitUntil('rosterContactsFetched');
-            const _getter = jid => _converse.roster.get(Strophe.getBareJidFromJid(jid));
+            const { roster } = _converse.state;
+            const _getter = jid => roster.get(Strophe.getBareJidFromJid(jid));
             if (jids === undefined) {
             if (jids === undefined) {
-                jids = _converse.roster.pluck('jid');
+                jids = roster.pluck('jid');
             } else if (typeof jids === 'string') {
             } else if (typeof jids === 'string') {
                 return _getter(jids);
                 return _getter(jids);
             }
             }
@@ -68,7 +69,7 @@ export default {
             if (typeof jid !== 'string' || !jid.includes('@')) {
             if (typeof jid !== 'string' || !jid.includes('@')) {
                 throw new TypeError('contacts.add: invalid jid');
                 throw new TypeError('contacts.add: invalid jid');
             }
             }
-            return _converse.roster.addAndSubscribe(jid, name);
+            return _converse.state.roster.addAndSubscribe(jid, name);
         }
         }
     }
     }
 }
 }

+ 6 - 6
src/headless/plugins/roster/contact.js

@@ -24,7 +24,7 @@ class RosterContact extends Model {
     }
     }
 
 
     async initialize (attributes) {
     async initialize (attributes) {
-        super.initialize(attributes);
+        super.initialize();
         this.initialized = getOpenPromise();
         this.initialized = getOpenPromise();
         this.setPresence();
         this.setPresence();
         const { jid } = attributes;
         const { jid } = attributes;
@@ -39,7 +39,7 @@ class RosterContact extends Model {
          * When a contact's presence status has changed.
          * When a contact's presence status has changed.
          * The presence status is either `online`, `offline`, `dnd`, `away` or `xa`.
          * The presence status is either `online`, `offline`, `dnd`, `away` or `xa`.
          * @event _converse#contactPresenceChanged
          * @event _converse#contactPresenceChanged
-         * @type { _converse.RosterContact }
+         * @type {RosterContact}
          * @example _converse.api.listen.on('contactPresenceChanged', contact => { ... });
          * @example _converse.api.listen.on('contactPresenceChanged', contact => { ... });
          */
          */
         this.listenTo(this.presence, 'change:show', () => api.trigger('contactPresenceChanged', this));
         this.listenTo(this.presence, 'change:show', () => api.trigger('contactPresenceChanged', this));
@@ -47,7 +47,7 @@ class RosterContact extends Model {
         /**
         /**
          * Synchronous event which provides a hook for further initializing a RosterContact
          * Synchronous event which provides a hook for further initializing a RosterContact
          * @event _converse#rosterContactInitialized
          * @event _converse#rosterContactInitialized
-         * @param { _converse.RosterContact } contact
+         * @param {RosterContact} contact
          */
          */
         await api.trigger('rosterContactInitialized', this, {'Synchronous': true});
         await api.trigger('rosterContactInitialized', this, {'Synchronous': true});
         this.initialized.resolve();
         this.initialized.resolve();
@@ -55,12 +55,12 @@ class RosterContact extends Model {
 
 
     setPresence () {
     setPresence () {
         const jid = this.get('jid');
         const jid = this.get('jid');
-        this.presence = _converse.presences.findWhere(jid) || _converse.presences.create({ jid });
+        const { presences } = _converse.state;
+        this.presence = presences.findWhere(jid) || presences.create({ jid });
     }
     }
 
 
     openChat () {
     openChat () {
-        const attrs = this.attributes;
-        api.chats.open(attrs.jid, attrs, true);
+        api.chats.open(this.get('jid'), this.attributes, true);
     }
     }
 
 
     /**
     /**

+ 27 - 21
src/headless/plugins/roster/contacts.js

@@ -2,8 +2,7 @@ import RosterContact from './contact.js';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
 import log from "../../log.js";
 import log from "../../log.js";
-import { Collection } from "@converse/skeletor";
-import { Model } from "@converse/skeletor";
+import { Collection, Model } from "@converse/skeletor";
 import { initStorage } from '../../utils/storage.js';
 import { initStorage } from '../../utils/storage.js';
 import { rejectPresenceSubscription } from './utils.js';
 import { rejectPresenceSubscription } from './utils.js';
 
 
@@ -13,10 +12,12 @@ class RosterContacts extends Collection {
     constructor () {
     constructor () {
         super();
         super();
         this.model = RosterContact;
         this.model = RosterContact;
+        this.data = null;
     }
     }
 
 
     initialize () {
     initialize () {
-        const id = `roster.state-${_converse.bare_jid}-${this.get('jid')}`;
+        const bare_jid = _converse.session.get('bare_jid');
+        const id = `roster.state-${bare_jid}-${this.get('jid')}`;
         this.state = new Model({ id, 'collapsed_groups': [] });
         this.state = new Model({ id, 'collapsed_groups': [] });
         initStorage(this.state, id);
         initStorage(this.state, id);
         this.state.fetch();
         this.state.fetch();
@@ -39,7 +40,7 @@ class RosterContacts extends Collection {
         // Register a handler for roster IQ "set" stanzas, which update
         // Register a handler for roster IQ "set" stanzas, which update
         // roster contacts.
         // roster contacts.
         api.connection.get().addHandler(iq => {
         api.connection.get().addHandler(iq => {
-            _converse.roster.onRosterPush(iq);
+            _converse.state.roster.onRosterPush(iq);
             return true;
             return true;
         }, Strophe.NS.ROSTER, 'iq', "set");
         }, Strophe.NS.ROSTER, 'iq', "set");
     }
     }
@@ -54,9 +55,10 @@ class RosterContacts extends Collection {
         const connection = api.connection.get();
         const connection = api.connection.get();
         connection.addHandler(
         connection.addHandler(
             function (msg) {
             function (msg) {
+                const { roster } = _converse.state;
                 window.setTimeout(function () {
                 window.setTimeout(function () {
-                    _converse.connection.flush();
-                    _converse.roster.subscribeToSuggestedItems.bind(_converse.roster)(msg);
+                    api.connection.get().flush();
+                    roster.subscribeToSuggestedItems(msg);
                 }, t);
                 }, t);
                 t += msg.querySelectorAll('item').length * 250;
                 t += msg.querySelectorAll('item').length * 250;
                 return true;
                 return true;
@@ -98,18 +100,19 @@ class RosterContacts extends Collection {
              */
              */
             api.trigger('cachedRoster', result);
             api.trigger('cachedRoster', result);
         } else {
         } else {
-            _converse.send_initial_presence = true;
-            return _converse.roster.fetchFromServer();
+            api.connection.get().send_initial_presence = true;
+            return _converse.state.roster.fetchFromServer();
         }
         }
     }
     }
 
 
     // eslint-disable-next-line class-methods-use-this
     // eslint-disable-next-line class-methods-use-this
     subscribeToSuggestedItems (msg) {
     subscribeToSuggestedItems (msg) {
+        const { xmppstatus } = _converse.state;
         Array.from(msg.querySelectorAll('item')).forEach((item) => {
         Array.from(msg.querySelectorAll('item')).forEach((item) => {
             if (item.getAttribute('action') === 'add') {
             if (item.getAttribute('action') === 'add') {
-                _converse.roster.addAndSubscribe(
+                _converse.state.roster.addAndSubscribe(
                     item.getAttribute('jid'),
                     item.getAttribute('jid'),
-                    _converse.xmppstatus.getNickname() || _converse.xmppstatus.getFullname()
+                    xmppstatus.getNickname() || xmppstatus.getFullname()
                 );
                 );
             }
             }
         });
         });
@@ -133,7 +136,7 @@ class RosterContacts extends Collection {
      */
      */
     async addAndSubscribe (jid, name, groups, message, attributes) {
     async addAndSubscribe (jid, name, groups, message, attributes) {
         const contact = await this.addContactToRoster(jid, name, groups, attributes);
         const contact = await this.addContactToRoster(jid, name, groups, attributes);
-        if (contact instanceof _converse.RosterContact) {
+        if (contact instanceof _converse.exports.RosterContact) {
             contact.subscribe(message);
             contact.subscribe(message);
         }
         }
     }
     }
@@ -192,13 +195,14 @@ class RosterContacts extends Collection {
 
 
     async subscribeBack (bare_jid, presence) {
     async subscribeBack (bare_jid, presence) {
         const contact = this.get(bare_jid);
         const contact = this.get(bare_jid);
-        if (contact instanceof _converse.RosterContact) {
+        const { RosterContact } = _converse.exports;
+        if (contact instanceof RosterContact) {
             contact.authorize().subscribe();
             contact.authorize().subscribe();
         } else {
         } else {
             // Can happen when a subscription is retried or roster was deleted
             // Can happen when a subscription is retried or roster was deleted
             const nickname = sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop()?.textContent || null;
             const nickname = sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop()?.textContent || null;
             const contact = await this.addContactToRoster(bare_jid, nickname, [], { 'subscription': 'from' });
             const contact = await this.addContactToRoster(bare_jid, nickname, [], { 'subscription': 'from' });
-            if (contact instanceof _converse.RosterContact) {
+            if (contact instanceof RosterContact) {
                 contact.authorize().subscribe();
                 contact.authorize().subscribe();
             }
             }
         }
         }
@@ -213,7 +217,8 @@ class RosterContacts extends Collection {
     onRosterPush (iq) {
     onRosterPush (iq) {
         const id = iq.getAttribute('id');
         const id = iq.getAttribute('id');
         const from = iq.getAttribute('from');
         const from = iq.getAttribute('from');
-        if (from && from !== _converse.bare_jid) {
+        const bare_jid = _converse.session.get('bare_jid');
+        if (from && from !== bare_jid) {
             // https://tools.ietf.org/html/rfc6121#page-15
             // https://tools.ietf.org/html/rfc6121#page-15
             //
             //
             // A receiving client MUST ignore the stanza unless it has no 'from'
             // A receiving client MUST ignore the stanza unless it has no 'from'
@@ -342,7 +347,7 @@ class RosterContacts extends Collection {
         /**
         /**
          * Triggered when someone has requested to subscribe to your presence (i.e. to be your contact).
          * Triggered when someone has requested to subscribe to your presence (i.e. to be your contact).
          * @event _converse#contactRequest
          * @event _converse#contactRequest
-         * @type { _converse.RosterContact }
+         * @type {RosterContact}
          * @example _converse.api.listen.on('contactRequest', contact => { ... });
          * @example _converse.api.listen.on('contactRequest', contact => { ... });
          */
          */
         api.trigger('contactRequest', this.create(user_data));
         api.trigger('contactRequest', this.create(user_data));
@@ -378,9 +383,10 @@ class RosterContacts extends Collection {
 
 
     // eslint-disable-next-line class-methods-use-this
     // eslint-disable-next-line class-methods-use-this
     handleOwnPresence (presence) {
     handleOwnPresence (presence) {
-        const jid = presence.getAttribute('from'),
-            resource = Strophe.getResourceFromJid(jid),
-            presence_type = presence.getAttribute('type');
+        const jid = presence.getAttribute('from');
+        const resource = Strophe.getResourceFromJid(jid);
+        const presence_type = presence.getAttribute('type');
+        const { xmppstatus } = _converse.state;
 
 
         if ((api.connection.get().jid !== jid) &&
         if ((api.connection.get().jid !== jid) &&
                 (presence_type !== 'unavailable') &&
                 (presence_type !== 'unavailable') &&
@@ -390,12 +396,12 @@ class RosterContacts extends Collection {
             // synchronize_availability option set to update,
             // synchronize_availability option set to update,
             // we'll update ours as well.
             // we'll update ours as well.
             const show = presence.querySelector('show')?.textContent || 'online';
             const show = presence.querySelector('show')?.textContent || 'online';
-            _converse.xmppstatus.save({ 'status': show }, { 'silent': true });
+            xmppstatus.save({ 'status': show }, { 'silent': true });
 
 
             const status_message = presence.querySelector('status')?.textContent;
             const status_message = presence.querySelector('status')?.textContent;
-            if (status_message) _converse.xmppstatus.save({ status_message });
+            if (status_message) xmppstatus.save({ status_message });
         }
         }
-        if (_converse.jid === jid && presence_type === 'unavailable') {
+        if (_converse.session.get('jid') === jid && presence_type === 'unavailable') {
             // XXX: We've received an "unavailable" presence from our
             // XXX: We've received an "unavailable" presence from our
             // own resource. Apparently this happens due to a
             // own resource. Apparently this happens due to a
             // Prosody bug, whereby we send an IQ stanza to remove
             // Prosody bug, whereby we send an IQ stanza to remove

+ 13 - 10
src/headless/plugins/roster/index.js

@@ -36,16 +36,19 @@ converse.plugins.add('converse-roster', {
         Object.assign(_converse.api, roster_api);
         Object.assign(_converse.api, roster_api);
 
 
         const { __ } = _converse;
         const { __ } = _converse;
-        _converse.HEADER_CURRENT_CONTACTS = __('My contacts');
-        _converse.HEADER_PENDING_CONTACTS = __('Pending contacts');
-        _converse.HEADER_REQUESTING_CONTACTS = __('Contact requests');
-        _converse.HEADER_UNGROUPED = __('Ungrouped');
-        _converse.HEADER_UNREAD = __('New messages');
-
-        _converse.Presence = Presence;
-        _converse.Presences = Presences;
-        _converse.RosterContact = RosterContact;
-        _converse.RosterContacts = RosterContacts;
+        const labels = {
+            HEADER_CURRENT_CONTACTS: __('My contacts'),
+            HEADER_PENDING_CONTACTS: __('Pending contacts'),
+            HEADER_REQUESTING_CONTACTS: __('Contact requests'),
+            HEADER_UNGROUPED: __('Ungrouped'),
+            HEADER_UNREAD: __('New messages'),
+        };
+        Object.assign(_converse, labels); // XXX DEPRECATED
+        Object.assign(_converse.labels, labels);
+
+        const exports = { Presence, Presences, RosterContact, RosterContacts };
+        Object.assign(_converse, exports);  // XXX DEPRECATED
+        Object.assign(_converse.exports, exports);
 
 
         api.listen.on('beforeTearDown', () => unregisterPresenceHandler());
         api.listen.on('beforeTearDown', () => unregisterPresenceHandler());
         api.listen.on('chatBoxesInitialized', onChatBoxesInitialized);
         api.listen.on('chatBoxesInitialized', onChatBoxesInitialized);

+ 68 - 47
src/headless/plugins/roster/utils.js

@@ -13,16 +13,23 @@ const { $pres } = converse.env;
 
 
 function initRoster () {
 function initRoster () {
     // Initialize the collections that represent the roster contacts and groups
     // Initialize the collections that represent the roster contacts and groups
-    const roster = _converse.roster = new _converse.RosterContacts();
-    let id = `converse.contacts-${_converse.bare_jid}`;
+    const roster = new _converse.exports.RosterContacts();
+    Object.assign(_converse, { roster }); // XXX Deprecated
+    Object.assign(_converse.state, { roster });
+
+    const bare_jid = _converse.session.get('bare_jid');
+    let id = `converse.contacts-${bare_jid}`;
     initStorage(roster, id);
     initStorage(roster, id);
 
 
-    const filter = _converse.roster_filter = new RosterFilter();
-    filter.id = `_converse.rosterfilter-${_converse.bare_jid}`;
-    initStorage(filter, filter.id);
-    filter.fetch();
+    const roster_filter = new RosterFilter();
+    Object.assign(_converse, { roster_filter }); // XXX Deprecated
+    Object.assign(_converse.state, { roster_filter });
+
+    roster_filter.id = `_converse.rosterfilter-${bare_jid}`;
+    initStorage(roster_filter, roster_filter.id);
+    roster_filter.fetch();
 
 
-    id = `converse-roster-model-${_converse.bare_jid}`;
+    id = `converse-roster-model-${bare_jid}`;
     roster.data = new Model();
     roster.data = new Model();
     roster.data.id = id;
     roster.data.id = id;
     initStorage(roster.data, id);
     initStorage(roster.data, id);
@@ -42,50 +49,52 @@ function initRoster () {
 /**
 /**
  * Fetch all the roster groups, and then the roster contacts.
  * Fetch all the roster groups, and then the roster contacts.
  * Emit an event after fetching is done in each case.
  * Emit an event after fetching is done in each case.
- * @private
- * @param { Bool } ignore_cache - If set to to true, the local cache
+ * @param {boolean} ignore_cache - If set to to true, the local cache
  *      will be ignored it's guaranteed that the XMPP server
  *      will be ignored it's guaranteed that the XMPP server
  *      will be queried for the roster.
  *      will be queried for the roster.
  */
  */
 async function populateRoster (ignore_cache=false) {
 async function populateRoster (ignore_cache=false) {
+    const connection = api.connection.get();
     if (ignore_cache) {
     if (ignore_cache) {
-        _converse.send_initial_presence = true;
+        connection.send_initial_presence = true;
     }
     }
     try {
     try {
-        await _converse.roster.fetchRosterContacts();
+        await _converse.state.roster.fetchRosterContacts();
         api.trigger('rosterContactsFetched');
         api.trigger('rosterContactsFetched');
     } catch (reason) {
     } catch (reason) {
         log.error(reason);
         log.error(reason);
     } finally {
     } finally {
-        _converse.send_initial_presence && api.user.presence.send();
+        connection.send_initial_presence && api.user.presence.send();
     }
     }
 }
 }
 
 
 
 
 function updateUnreadCounter (chatbox) {
 function updateUnreadCounter (chatbox) {
-    const contact = _converse.roster?.get(chatbox.get('jid'));
+    const contact = _converse.state.roster?.get(chatbox.get('jid'));
     contact?.save({'num_unread': chatbox.get('num_unread')});
     contact?.save({'num_unread': chatbox.get('num_unread')});
 }
 }
 
 
+let presence_ref;
+
 function registerPresenceHandler () {
 function registerPresenceHandler () {
     unregisterPresenceHandler();
     unregisterPresenceHandler();
     const connection = api.connection.get();
     const connection = api.connection.get();
-    _converse.presence_ref = connection.addHandler(presence => {
-            _converse.roster.presenceHandler(presence);
+    presence_ref = connection.addHandler(presence => {
+            _converse.state.roster.presenceHandler(presence);
             return true;
             return true;
         }, null, 'presence', null);
         }, null, 'presence', null);
 }
 }
 
 
 export function unregisterPresenceHandler () {
 export function unregisterPresenceHandler () {
-    if (_converse.presence_ref !== undefined) {
+    if (presence_ref) {
         const connection = api.connection.get();
         const connection = api.connection.get();
-        connection.deleteHandler(_converse.presence_ref);
-        delete _converse.presence_ref;
+        connection.deleteHandler(presence_ref);
+        presence_ref = null;
     }
     }
 }
 }
 
 
 async function clearPresences () {
 async function clearPresences () {
-    await _converse.presences?.clearStore();
+    await _converse.state.presences?.clearStore();
 }
 }
 
 
 
 
@@ -95,14 +104,12 @@ async function clearPresences () {
 export async function onClearSession () {
 export async function onClearSession () {
     await clearPresences();
     await clearPresences();
     if (shouldClearCache()) {
     if (shouldClearCache()) {
-        if (_converse.rostergroups) {
-            await _converse.rostergroups.clearStore();
-            delete _converse.rostergroups;
-        }
-        if (_converse.roster) {
-            _converse.roster.data?.destroy();
-            await _converse.roster.clearStore();
-            delete _converse.roster;
+        const { roster } = _converse.state;
+        if (roster) {
+            roster.data?.destroy();
+            await roster.clearStore();
+            delete _converse.state.roster;
+            Object.assign(_converse, { roster: undefined }); // XXX DEPRECATED
         }
         }
     }
     }
 }
 }
@@ -125,7 +132,7 @@ export function onPresencesInitialized (reconnecting) {
     } else {
     } else {
         initRoster();
         initRoster();
     }
     }
-    _converse.roster.onConnected();
+    _converse.state.roster.onConnected();
     registerPresenceHandler();
     registerPresenceHandler();
     populateRoster(!api.connection.get().restored);
     populateRoster(!api.connection.get().restored);
 }
 }
@@ -142,12 +149,17 @@ export async function onStatusInitialized (reconnecting) {
          // and we'll receive new presence updates
          // and we'll receive new presence updates
          !api.connection.get().hasResumed() && (await clearPresences());
          !api.connection.get().hasResumed() && (await clearPresences());
      } else {
      } else {
-         _converse.presences = new _converse.Presences();
-         const id = `converse.presences-${_converse.bare_jid}`;
-         initStorage(_converse.presences, id, 'session');
+         const presences = new _converse.exports.Presences();
+         Object.assign(_converse, { presences });
+         Object.assign(_converse.state, { presences });
+
+         const bare_jid = _converse.session.get('bare_jid');
+         const id = `converse.presences-${bare_jid}`;
+
+         initStorage(presences, id, 'session');
          // We might be continuing an existing session, so we fetch
          // We might be continuing an existing session, so we fetch
          // cached presence data.
          // cached presence data.
-         _converse.presences.fetch();
+         presences.fetch();
      }
      }
      /**
      /**
       * Triggered once the _converse.Presences collection has been
       * Triggered once the _converse.Presences collection has been
@@ -155,7 +167,7 @@ export async function onStatusInitialized (reconnecting) {
       * Returns a boolean indicating whether this event has fired due to
       * Returns a boolean indicating whether this event has fired due to
       * Converse having reconnected.
       * Converse having reconnected.
       * @event _converse#presencesInitialized
       * @event _converse#presencesInitialized
-      * @type { bool }
+      * @type {boolean}
       * @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... });
       * @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... });
       */
       */
      api.trigger('presencesInitialized', reconnecting);
      api.trigger('presencesInitialized', reconnecting);
@@ -166,9 +178,10 @@ export async function onStatusInitialized (reconnecting) {
  * Roster specific event handler for the chatBoxesInitialized event
  * Roster specific event handler for the chatBoxesInitialized event
  */
  */
 export function onChatBoxesInitialized () {
 export function onChatBoxesInitialized () {
-    _converse.chatboxes.on('change:num_unread', updateUnreadCounter);
+    const { chatboxes } = _converse.state;
+    chatboxes.on('change:num_unread', updateUnreadCounter);
 
 
-    _converse.chatboxes.on('add', chatbox => {
+    chatboxes.on('add', chatbox => {
         if (chatbox.get('type') === PRIVATE_CHAT_TYPE) {
         if (chatbox.get('type') === PRIVATE_CHAT_TYPE) {
             chatbox.setRosterContact(chatbox.get('jid'));
             chatbox.setRosterContact(chatbox.get('jid'));
         }
         }
@@ -180,10 +193,10 @@ export function onChatBoxesInitialized () {
  * Roster specific handler for the rosterContactsFetched promise
  * Roster specific handler for the rosterContactsFetched promise
  */
  */
 export function onRosterContactsFetched () {
 export function onRosterContactsFetched () {
-    _converse.roster.on('add', contact => {
+    _converse.state.roster.on('add', contact => {
         // When a new contact is added, check if we already have a
         // When a new contact is added, check if we already have a
         // chatbox open for it, and if so attach it to the chatbox.
         // chatbox open for it, and if so attach it to the chatbox.
-        const chatbox = _converse.chatboxes.findWhere({ 'jid': contact.get('jid') });
+        const chatbox = _converse.state.chatboxes.findWhere({ 'jid': contact.get('jid') });
         chatbox?.setRosterContact(contact.get('jid'));
         chatbox?.setRosterContact(contact.get('jid'));
     });
     });
 }
 }
@@ -214,11 +227,19 @@ export function contactsComparator (contact1, contact2) {
 
 
 export function groupsComparator (a, b) {
 export function groupsComparator (a, b) {
     const HEADER_WEIGHTS = {};
     const HEADER_WEIGHTS = {};
-    HEADER_WEIGHTS[_converse.HEADER_UNREAD] = 0;
-    HEADER_WEIGHTS[_converse.HEADER_REQUESTING_CONTACTS] = 1;
-    HEADER_WEIGHTS[_converse.HEADER_CURRENT_CONTACTS]    = 2;
-    HEADER_WEIGHTS[_converse.HEADER_UNGROUPED]           = 3;
-    HEADER_WEIGHTS[_converse.HEADER_PENDING_CONTACTS]    = 4;
+    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 WEIGHTS =  HEADER_WEIGHTS;
     const special_groups = Object.keys(HEADER_WEIGHTS);
     const special_groups = Object.keys(HEADER_WEIGHTS);
@@ -229,22 +250,22 @@ export function groupsComparator (a, b) {
     } else if (a_is_special && b_is_special) {
     } else if (a_is_special && b_is_special) {
         return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0);
         return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0);
     } else if (!a_is_special && b_is_special) {
     } else if (!a_is_special && b_is_special) {
-        const a_header = _converse.HEADER_CURRENT_CONTACTS;
+        const a_header = HEADER_CURRENT_CONTACTS;
         return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0);
         return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0);
     } else if (a_is_special && !b_is_special) {
     } else if (a_is_special && !b_is_special) {
-        const b_header = _converse.HEADER_CURRENT_CONTACTS;
+        const b_header = HEADER_CURRENT_CONTACTS;
         return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0);
         return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0);
     }
     }
 }
 }
 
 
 export function getGroupsAutoCompleteList () {
 export function getGroupsAutoCompleteList () {
-    const { roster } = _converse;
+    const { roster } = _converse.state;
     const groups = roster.reduce((groups, contact) => groups.concat(contact.get('groups')), []);
     const groups = roster.reduce((groups, contact) => groups.concat(contact.get('groups')), []);
     return [...new Set(groups.filter(i => i))];
     return [...new Set(groups.filter(i => i))];
 }
 }
 
 
 export function getJIDsAutoCompleteList () {
 export function getJIDsAutoCompleteList () {
-    return [...new Set(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))];
+    return [...new Set(_converse.state.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))];
 }
 }
 
 
 
 
@@ -253,7 +274,7 @@ export function getJIDsAutoCompleteList () {
  */
  */
 export async function getNamesAutoCompleteList (query) {
 export async function getNamesAutoCompleteList (query) {
     const options = {
     const options = {
-        'mode': 'cors',
+        'mode': /** @type {RequestMode} */('cors'),
         'headers': {
         'headers': {
             'Accept': 'text/json'
             'Accept': 'text/json'
         }
         }

+ 19 - 7
src/headless/plugins/smacks/tests/smacks.js

@@ -213,6 +213,8 @@ describe("XEP-0198 Stream Management", function () {
             },
             },
             async function (_converse) {
             async function (_converse) {
 
 
+        const { api } = _converse;
+
         const key = "converse-test-session/converse.session-romeo@montague.lit-converse.session-romeo@montague.lit";
         const key = "converse-test-session/converse.session-romeo@montague.lit-converse.session-romeo@montague.lit";
         sessionStorage.setItem(
         sessionStorage.setItem(
             key,
             key,
@@ -251,15 +253,26 @@ describe("XEP-0198 Stream Management", function () {
             })
             })
         );
         );
 
 
-        _converse.no_connection_on_bind = true; // XXX Don't trigger CONNECTED in MockConnection
-        await _converse.api.user.login('romeo@montague.lit', 'secret');
+        const proto = Object.getPrototypeOf(api.connection.get())
+        const _changeConnectStatus = proto._changeConnectStatus;
+        let count = 0;
+        spyOn(proto, '_changeConnectStatus').and.callFake((status) => {
+            if (status === Strophe.Status.CONNECTED && count === 0) {
+                // Don't trigger CONNECTED
+                count++;
+                return;
+            }
+            _changeConnectStatus.call(api.connection.get(), status);
+        });
 
 
-        const sent_stanzas = _converse.api.connection.get().sent_stanzas;
+        await api.user.login('romeo@montague.lit', 'secret');
+
+        const sent_stanzas = api.connection.get().sent_stanzas;
         const stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop());
         const stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop());
         expect(Strophe.serialize(stanza)).toEqual('<resume h="580" previd="some-long-sm-id" xmlns="urn:xmpp:sm:3"/>');
         expect(Strophe.serialize(stanza)).toEqual('<resume h="580" previd="some-long-sm-id" xmlns="urn:xmpp:sm:3"/>');
 
 
         const result = u.toStanza(`<resumed xmlns="urn:xmpp:sm:3" h="another-sequence-number" previd="some-long-sm-id"/>`);
         const result = u.toStanza(`<resumed xmlns="urn:xmpp:sm:3" h="another-sequence-number" previd="some-long-sm-id"/>`);
-        _converse.api.connection.get()._dataRecv(mock.createRequest(result));
+        api.connection.get()._dataRecv(mock.createRequest(result));
         expect(_converse.session.get('smacks_enabled')).toBe(true);
         expect(_converse.session.get('smacks_enabled')).toBe(true);
 
 
         const nick = 'romeo';
         const nick = 'romeo';
@@ -278,9 +291,9 @@ describe("XEP-0198 Stream Management", function () {
                 type: 'groupchat'
                 type: 'groupchat'
             }).c('body').t('First message').tree();
             }).c('body').t('First message').tree();
 
 
-        _converse.api.connection.get()._dataRecv(mock.createRequest(msg));
+        api.connection.get()._dataRecv(mock.createRequest(msg));
 
 
-        await _converse.api.waitUntil('chatBoxesFetched');
+        await api.waitUntil('chatBoxesFetched');
         const muc = _converse.chatboxes.get(muc_jid);
         const muc = _converse.chatboxes.get(muc_jid);
         await mock.getRoomFeatures(_converse, muc_jid);
         await mock.getRoomFeatures(_converse, muc_jid);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
@@ -288,6 +301,5 @@ describe("XEP-0198 Stream Management", function () {
         await muc.messages.fetched;
         await muc.messages.fetched;
         await u.waitUntil(() => muc.messages.length);
         await u.waitUntil(() => muc.messages.length);
         expect(muc.messages.at(0).get('message')).toBe('First message')
         expect(muc.messages.at(0).get('message')).toBe('First message')
-        delete _converse.no_connection_on_bind;
     }));
     }));
 });
 });

+ 4 - 4
src/headless/plugins/status/api.js

@@ -18,7 +18,7 @@ export default {
          */
          */
         async get () {
         async get () {
             await api.waitUntil('statusInitialized');
             await api.waitUntil('statusInitialized');
-            return _converse.xmppstatus.get('status');
+            return _converse.state.xmppstatus.get('status');
         },
         },
 
 
         /**
         /**
@@ -43,7 +43,7 @@ export default {
                 data.status_message = message;
                 data.status_message = message;
             }
             }
             await api.waitUntil('statusInitialized');
             await api.waitUntil('statusInitialized');
-            _converse.xmppstatus.save(data);
+            _converse.state.xmppstatus.save(data);
         },
         },
 
 
         /**
         /**
@@ -61,7 +61,7 @@ export default {
              */
              */
             async get () {
             async get () {
                 await api.waitUntil('statusInitialized');
                 await api.waitUntil('statusInitialized');
-                return _converse.xmppstatus.get('status_message');
+                return _converse.state.xmppstatus.get('status_message');
             },
             },
             /**
             /**
              * @async
              * @async
@@ -71,7 +71,7 @@ export default {
              */
              */
             async set (status) {
             async set (status) {
                 await api.waitUntil('statusInitialized');
                 await api.waitUntil('statusInitialized');
-                _converse.xmppstatus.save({ status_message: status });
+                _converse.state.xmppstatus.save({ status_message: status });
             }
             }
         }
         }
     }
     }

+ 10 - 14
src/headless/plugins/status/index.js

@@ -13,6 +13,7 @@ import {
     onEverySecond,
     onEverySecond,
     onUserActivity,
     onUserActivity,
     registerIntervalHandler,
     registerIntervalHandler,
+    tearDown,
     sendCSI
     sendCSI
 } from './utils.js';
 } from './utils.js';
 
 
@@ -35,28 +36,23 @@ converse.plugins.add('converse-status', {
         });
         });
         api.promises.add(['statusInitialized']);
         api.promises.add(['statusInitialized']);
 
 
-        _converse.XMPPStatus = XMPPStatus;
-        _converse.onUserActivity = onUserActivity;
-        _converse.onEverySecond = onEverySecond;
-        _converse.sendCSI = sendCSI;
-        _converse.registerIntervalHandler = registerIntervalHandler;
-
+        const exports = { XMPPStatus, onUserActivity, onEverySecond, sendCSI, registerIntervalHandler };
+        Object.assign(_converse, exports); // Deprecated
+        Object.assign(_converse.exports, exports);
         Object.assign(_converse.api.user, status_api);
         Object.assign(_converse.api.user, status_api);
 
 
         if (api.settings.get("idle_presence_timeout") > 0) {
         if (api.settings.get("idle_presence_timeout") > 0) {
             api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.IDLE));
             api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.IDLE));
         }
         }
 
 
-        api.listen.on('presencesInitialized', (reconnecting) => {
-            if (!reconnecting) {
-                _converse.registerIntervalHandler();
-            }
-        });
+        api.listen.on('presencesInitialized', (reconnecting) => (!reconnecting && registerIntervalHandler()));
+        api.listen.on('beforeTearDown', tearDown);
 
 
         api.listen.on('clearSession', () => {
         api.listen.on('clearSession', () => {
-            if (shouldClearCache() && _converse.xmppstatus) {
-                _converse.xmppstatus.destroy();
-                delete _converse.xmppstatus;
+            if (shouldClearCache() && _converse.state.xmppstatus) {
+                _converse.state.xmppstatus.destroy();
+                delete _converse.state.xmppstatus;
+                Object.assign(_converse, { xmppstatus: undefined }); // XXX DEPRECATED
                 api.promises.add(['statusInitialized']);
                 api.promises.add(['statusInitialized']);
             }
             }
         });
         });

+ 8 - 8
src/headless/plugins/status/status.js

@@ -1,12 +1,13 @@
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
 import { Model } from '@converse/skeletor';
 import { Model } from '@converse/skeletor';
+import { isIdle, getIdleSeconds } from './utils.js';
 
 
 const { Strophe, $pres } = converse.env;
 const { Strophe, $pres } = converse.env;
 
 
 export default class XMPPStatus extends Model {
 export default class XMPPStatus extends Model {
 
 
-    defaults () { // eslint-disable-line class-methods-use-this
+    defaults () {
         return { "status":  api.settings.get("default_state") }
         return { "status":  api.settings.get("default_state") }
     }
     }
 
 
@@ -22,14 +23,14 @@ export default class XMPPStatus extends Model {
     }
     }
 
 
     getDisplayName () {
     getDisplayName () {
-        return this.getFullname() || this.getNickname() || _converse.bare_jid;
+        return this.getFullname() || this.getNickname() || _converse.session.get('bare_jid');
     }
     }
 
 
-    getNickname () { // eslint-disable-line class-methods-use-this
+    getNickname () {
         return api.settings.get('nickname');
         return api.settings.get('nickname');
     }
     }
 
 
-    getFullname () { // eslint-disable-line class-methods-use-this
+    getFullname () {
         return ''; // Gets overridden in converse-vcard
         return ''; // Gets overridden in converse-vcard
     }
     }
 
 
@@ -46,7 +47,7 @@ export default class XMPPStatus extends Model {
 
 
         if (type === 'subscribe') {
         if (type === 'subscribe') {
             presence = $pres({ to, type });
             presence = $pres({ to, type });
-            const { xmppstatus } = _converse;
+            const { xmppstatus } = _converse.state;
             const nick = xmppstatus.getNickname();
             const nick = xmppstatus.getNickname();
             if (nick) presence.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
             if (nick) presence.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
 
 
@@ -73,10 +74,9 @@ export default class XMPPStatus extends Model {
         const priority = api.settings.get("priority");
         const priority = api.settings.get("priority");
         presence.c('priority').t(Number.isNaN(Number(priority)) ? 0 : priority).up();
         presence.c('priority').t(Number.isNaN(Number(priority)) ? 0 : priority).up();
 
 
-        const { idle, idle_seconds } = _converse;
-        if (idle) {
+        if (isIdle()) {
             const idle_since = new Date();
             const idle_since = new Date();
-            idle_since.setSeconds(idle_since.getSeconds() - idle_seconds);
+            idle_since.setSeconds(idle_since.getSeconds() - getIdleSeconds());
             presence.c('idle', { xmlns: Strophe.NS.IDLE, since: idle_since.toISOString() });
             presence.c('idle', { xmlns: Strophe.NS.IDLE, since: idle_since.toISOString() });
         }
         }
 
 

+ 76 - 42
src/headless/plugins/status/utils.js

@@ -1,6 +1,8 @@
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
 import { initStorage } from '../../utils/storage.js';
 import { initStorage } from '../../utils/storage.js';
+import { getUnloadEvent } from '../../utils/session.js';
+import { ACTIVE, INACTIVE } from '../../shared/constants.js';
 
 
 const { Strophe, $build } = converse.env;
 const { Strophe, $build } = converse.env;
 
 
@@ -17,14 +19,15 @@ function onStatusInitialized (reconnecting) {
 export function initStatus (reconnecting) {
 export function initStatus (reconnecting) {
     // If there's no xmppstatus obj, then we were never connected to
     // If there's no xmppstatus obj, then we were never connected to
     // begin with, so we set reconnecting to false.
     // begin with, so we set reconnecting to false.
-    reconnecting = _converse.xmppstatus === undefined ? false : reconnecting;
+    reconnecting = _converse.state.xmppstatus === undefined ? false : reconnecting;
     if (reconnecting) {
     if (reconnecting) {
         onStatusInitialized(reconnecting);
         onStatusInitialized(reconnecting);
     } else {
     } else {
-        const id = `converse.xmppstatus-${_converse.bare_jid}`;
-        _converse.xmppstatus = new _converse.XMPPStatus({ id });
-        initStorage(_converse.xmppstatus, id, 'session');
-        _converse.xmppstatus.fetch({
+        const id = `converse.xmppstatus-${_converse.session.get('bare_jid')}`;
+        _converse.state.xmppstatus = new _converse.exports.XMPPStatus({ id });
+        Object.assign(_converse, { xmppstatus: _converse.state.xmppstatus });
+        initStorage(_converse.state.xmppstatus, id, 'session');
+        _converse.state.xmppstatus.fetch({
             'success': () => onStatusInitialized(reconnecting),
             'success': () => onStatusInitialized(reconnecting),
             'error': () => onStatusInitialized(reconnecting),
             'error': () => onStatusInitialized(reconnecting),
             'silent': true
             'silent': true
@@ -32,28 +35,43 @@ export function initStatus (reconnecting) {
     }
     }
 }
 }
 
 
+let idle_seconds = 0;
+let idle = false;
+let auto_changed_status = false;
+let inactive = false;
+
+export function isIdle () {
+    return idle;
+}
+
+export function getIdleSeconds () {
+    return idle_seconds;
+}
+
+/**
+ * Resets counters and flags relating to CSI and auto_away/auto_xa
+ */
 export function onUserActivity () {
 export function onUserActivity () {
-    /* Resets counters and flags relating to CSI and auto_away/auto_xa */
-    if (_converse.idle_seconds > 0) {
-        _converse.idle_seconds = 0;
+    if (idle_seconds > 0) {
+        idle_seconds = 0;
     }
     }
     if (!api.connection.get()?.authenticated) {
     if (!api.connection.get()?.authenticated) {
         // We can't send out any stanzas when there's no authenticated connection.
         // We can't send out any stanzas when there's no authenticated connection.
         // This can happen when the connection reconnects.
         // This can happen when the connection reconnects.
         return;
         return;
     }
     }
-    if (_converse.inactive) {
-        _converse.sendCSI(_converse.ACTIVE);
-    }
-    if (_converse.idle) {
-        _converse.idle = false;
+    if (inactive) sendCSI(ACTIVE);
+
+    if (idle) {
+        idle = false;
         api.user.presence.send();
         api.user.presence.send();
     }
     }
-    if (_converse.auto_changed_status === true) {
-        _converse.auto_changed_status = false;
+
+    if (auto_changed_status === true) {
+        auto_changed_status = false;
         // XXX: we should really remember the original state here, and
         // XXX: we should really remember the original state here, and
         // then set it back to that...
         // then set it back to that...
-        _converse.xmppstatus.set('status', api.settings.get("default_state"));
+        _converse.state.xmppstatus.set('status', api.settings.get("default_state"));
     }
     }
 }
 }
 
 
@@ -66,29 +84,30 @@ export function onEverySecond () {
         // This can happen when the connection reconnects.
         // This can happen when the connection reconnects.
         return;
         return;
     }
     }
-    const stat = _converse.xmppstatus.get('status');
-    _converse.idle_seconds++;
+    const { xmppstatus } = _converse.state;
+    const stat = xmppstatus.get('status');
+    idle_seconds++;
     if (api.settings.get("csi_waiting_time") > 0 &&
     if (api.settings.get("csi_waiting_time") > 0 &&
-            _converse.idle_seconds > api.settings.get("csi_waiting_time") &&
-            !_converse.inactive) {
-        _converse.sendCSI(_converse.INACTIVE);
+            idle_seconds > api.settings.get("csi_waiting_time") &&
+            !inactive) {
+        sendCSI(INACTIVE);
     }
     }
     if (api.settings.get("idle_presence_timeout") > 0 &&
     if (api.settings.get("idle_presence_timeout") > 0 &&
-            _converse.idle_seconds > api.settings.get("idle_presence_timeout") &&
-            !_converse.idle) {
-        _converse.idle = true;
+            idle_seconds > api.settings.get("idle_presence_timeout") &&
+            !idle) {
+        idle = true;
         api.user.presence.send();
         api.user.presence.send();
     }
     }
     if (api.settings.get("auto_away") > 0 &&
     if (api.settings.get("auto_away") > 0 &&
-            _converse.idle_seconds > api.settings.get("auto_away") &&
+            idle_seconds > api.settings.get("auto_away") &&
             stat !== 'away' && stat !== 'xa' && stat !== 'dnd') {
             stat !== 'away' && stat !== 'xa' && stat !== 'dnd') {
-        _converse.auto_changed_status = true;
-        _converse.xmppstatus.set('status', 'away');
+        auto_changed_status = true;
+        xmppstatus.set('status', 'away');
     } else if (api.settings.get("auto_xa") > 0 &&
     } else if (api.settings.get("auto_xa") > 0 &&
-            _converse.idle_seconds > api.settings.get("auto_xa") &&
+            idle_seconds > api.settings.get("auto_xa") &&
             stat !== 'xa' && stat !== 'dnd') {
             stat !== 'xa' && stat !== 'dnd') {
-        _converse.auto_changed_status = true;
-        _converse.xmppstatus.set('status', 'xa');
+        auto_changed_status = true;
+        xmppstatus.set('status', 'xa');
     }
     }
 }
 }
 
 
@@ -99,9 +118,11 @@ export function onEverySecond () {
  */
  */
 export function sendCSI (stat) {
 export function sendCSI (stat) {
     api.send($build(stat, {xmlns: Strophe.NS.CSI}));
     api.send($build(stat, {xmlns: Strophe.NS.CSI}));
-    _converse.inactive = (stat === _converse.INACTIVE) ? true : false;
+    inactive = (stat === INACTIVE) ? true : false;
 }
 }
 
 
+let everySecondTrigger;
+
 /**
 /**
  * Set an interval of one second and register a handler for it.
  * Set an interval of one second and register a handler for it.
  * Required for the auto_away, auto_xa and csi_waiting_time features.
  * Required for the auto_away, auto_xa and csi_waiting_time features.
@@ -116,20 +137,33 @@ export function registerIntervalHandler () {
         // Waiting time of less then one second means features aren't used.
         // Waiting time of less then one second means features aren't used.
         return;
         return;
     }
     }
-    _converse.idle_seconds = 0;
-    _converse.auto_changed_status = false; // Was the user's status changed by Converse?
-
-    const { unloadevent } = _converse;
-    window.addEventListener('click', _converse.onUserActivity);
-    window.addEventListener('focus', _converse.onUserActivity);
-    window.addEventListener('keypress', _converse.onUserActivity);
-    window.addEventListener('mousemove', _converse.onUserActivity);
-    window.addEventListener(unloadevent, _converse.onUserActivity, {'once': true, 'passive': true});
-    _converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000);
+    idle_seconds = 0;
+    auto_changed_status = false; // Was the user's status changed by Converse?
+
+    const { onUserActivity, onEverySecond } = _converse.exports;
+    window.addEventListener('click', onUserActivity);
+    window.addEventListener('focus', onUserActivity);
+    window.addEventListener('keypress', onUserActivity);
+    window.addEventListener('mousemove', onUserActivity);
+    window.addEventListener(getUnloadEvent(), onUserActivity, {'once': true, 'passive': true});
+    everySecondTrigger = window.setInterval(onEverySecond, 1000);
+}
+
+export function tearDown () {
+    const { onUserActivity } = _converse.exports;
+    window.removeEventListener('click', onUserActivity);
+    window.removeEventListener('focus', onUserActivity);
+    window.removeEventListener('keypress', onUserActivity);
+    window.removeEventListener('mousemove', onUserActivity);
+    window.removeEventListener(getUnloadEvent(), onUserActivity);
+    if (everySecondTrigger) {
+        window.clearInterval(everySecondTrigger);
+        everySecondTrigger = null;
+    }
 }
 }
 
 
 export function addStatusToMUCJoinPresence (_, stanza) {
 export function addStatusToMUCJoinPresence (_, stanza) {
-    const { xmppstatus } = _converse;
+    const { xmppstatus } = _converse.state;
 
 
     const status = xmppstatus.get('status');
     const status = xmppstatus.get('status');
     if (['away', 'chat', 'dnd', 'xa'].includes(status)) {
     if (['away', 'chat', 'dnd', 'xa'].includes(status)) {

+ 9 - 6
src/headless/plugins/vcard/api.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('@converse/skeletor').Model} Model
+ */
 import log from "../../log.js";
 import log from "../../log.js";
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
@@ -23,8 +26,8 @@ export default {
          * for the passed in JID.
          * for the passed in JID.
          *
          *
          * @method _converse.api.vcard.set
          * @method _converse.api.vcard.set
-         * @param { string } jid The JID for which the VCard should be set
-         * @param { object } data A map of VCard keys and values
+         * @param {string} jid The JID for which the VCard should be set
+         * @param {object} data A map of VCard keys and values
          * @example
          * @example
          * let jid = _converse.bare_jid;
          * let jid = _converse.bare_jid;
          * _converse.api.vcard.set( jid, {
          * _converse.api.vcard.set( jid, {
@@ -68,7 +71,7 @@ export default {
          * @param {Model|string} model Either a `Model` instance, or a string JID.
          * @param {Model|string} model Either a `Model` instance, or a string JID.
          *     If a `Model` instance is passed in, then it must have either a `jid`
          *     If a `Model` instance is passed in, then it must have either a `jid`
          *     attribute or a `muc_jid` attribute.
          *     attribute or a `muc_jid` attribute.
-         * @param { boolean } [force] A boolean indicating whether the vcard should be
+         * @param {boolean} [force] A boolean indicating whether the vcard should be
          *     fetched from the server even if it's been fetched before.
          *     fetched from the server even if it's been fetched before.
          * @returns {promise} A Promise which resolves with the VCard data for a particular JID or for
          * @returns {promise} A Promise which resolves with the VCard data for a particular JID or for
          *     a `Model` instance which represents an entity with a JID (such as a roster contact,
          *     a `Model` instance which represents an entity with a JID (such as a roster contact,
@@ -107,8 +110,8 @@ export default {
          * returned VCard data.
          * returned VCard data.
          *
          *
          * @method _converse.api.vcard.update
          * @method _converse.api.vcard.update
-         * @param { Model } model A `Model` instance
-         * @param { boolean } [force] A boolean indicating whether the vcard should be
+         * @param {Model} model A `Model` instance
+         * @param {boolean} [force] A boolean indicating whether the vcard should be
          *     fetched again even if it's been fetched before.
          *     fetched again even if it's been fetched before.
          * @returns {promise} A promise which resolves once the update has completed.
          * @returns {promise} A promise which resolves once the update has completed.
          * @example
          * @example
@@ -120,7 +123,7 @@ export default {
          */
          */
         async update (model, force) {
         async update (model, force) {
             const data = await this.get(model, force);
             const data = await this.get(model, force);
-            model = typeof model === 'string' ? _converse.vcards.get(model) : model;
+            model = typeof model === 'string' ? _converse.exports.vcards.get(model) : model;
             if (!model) {
             if (!model) {
                 log.error(`Could not find a VCard model for ${model}`);
                 log.error(`Could not find a VCard model for ${model}`);
                 return;
                 return;

+ 3 - 2
src/headless/plugins/vcard/index.js

@@ -72,8 +72,9 @@ converse.plugins.add('converse-vcard', {
     initialize () {
     initialize () {
         api.promises.add('VCardsInitialized');
         api.promises.add('VCardsInitialized');
 
 
-        _converse.VCard = VCard;
-        _converse.VCards = VCards;
+        const exports = { VCard, VCards };
+        Object.assign(_converse, exports); // XXX DEPRECATED
+        Object.assign(_converse.exports, exports);
 
 
         api.listen.on('chatRoomInitialized', (m) => {
         api.listen.on('chatRoomInitialized', (m) => {
             setVCardOnModel(m)
             setVCardOnModel(m)

+ 47 - 28
src/headless/plugins/vcard/utils.js

@@ -1,3 +1,7 @@
+/**
+ * @typedef {import('../muc/occupant.js').default} ChatRoomOccupant
+ * @typedef {import('../chat/model-with-contact.js').default} ModelWithContact
+ */
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
 import log from "../../log.js";
 import log from "../../log.js";
@@ -7,7 +11,10 @@ import { shouldClearCache } from '../../utils/session.js';
 const { Strophe, $iq, u } = converse.env;
 const { Strophe, $iq, u } = converse.env;
 
 
 
 
-async function onVCardData (jid, iq) {
+/**
+ * @param {Element} iq
+ */
+async function onVCardData (iq) {
     const vcard = iq.querySelector('vCard');
     const vcard = iq.querySelector('vCard');
     let result = {};
     let result = {};
     if (vcard !== null) {
     if (vcard !== null) {
@@ -21,7 +28,8 @@ async function onVCardData (jid, iq) {
             'role': vcard.querySelector('ROLE')?.textContent,
             'role': vcard.querySelector('ROLE')?.textContent,
             'email': vcard.querySelector('EMAIL USERID')?.textContent,
             'email': vcard.querySelector('EMAIL USERID')?.textContent,
             'vcard_updated': (new Date()).toISOString(),
             'vcard_updated': (new Date()).toISOString(),
-            'vcard_error': undefined
+            'vcard_error': undefined,
+            image_hash: undefined,
         };
         };
     }
     }
     if (result.image) {
     if (result.image) {
@@ -44,20 +52,26 @@ export function createStanza (type, jid, vcard_el) {
 }
 }
 
 
 
 
+/**
+ * @param {ChatRoomOccupant} occupant
+ */
 export function onOccupantAvatarChanged (occupant) {
 export function onOccupantAvatarChanged (occupant) {
     const hash = occupant.get('image_hash');
     const hash = occupant.get('image_hash');
     const vcards = [];
     const vcards = [];
     if (occupant.get('jid')) {
     if (occupant.get('jid')) {
-        vcards.push(_converse.vcards.get(occupant.get('jid')));
+        vcards.push(_converse.state.vcards.get(occupant.get('jid')));
     }
     }
-    vcards.push(_converse.vcards.get(occupant.get('from')));
+    vcards.push(_converse.state.vcards.get(occupant.get('from')));
     vcards.forEach(v => (hash && v?.get('image_hash') !== hash) && api.vcard.update(v, true));
     vcards.forEach(v => (hash && v?.get('image_hash') !== hash) && api.vcard.update(v, true));
 }
 }
 
 
 
 
+/**
+ * @param {ModelWithContact} model
+ */
 export async function setVCardOnModel (model) {
 export async function setVCardOnModel (model) {
     let jid;
     let jid;
-    if (model instanceof _converse.Message) {
+    if (model instanceof _converse.exports.Message) {
         if (['error', 'info'].includes(model.get('type'))) {
         if (['error', 'info'].includes(model.get('type'))) {
             return;
             return;
         }
         }
@@ -72,22 +86,24 @@ export async function setVCardOnModel (model) {
     }
     }
 
 
     await api.waitUntil('VCardsInitialized');
     await api.waitUntil('VCardsInitialized');
-    model.vcard = _converse.vcards.get(jid) || _converse.vcards.create({ jid });
+    const { vcards } = _converse.state;
+    model.vcard = vcards.get(jid) || vcards.create({ jid });
     model.vcard.on('change', () => model.trigger('vcard:change'));
     model.vcard.on('change', () => model.trigger('vcard:change'));
     model.trigger('vcard:add');
     model.trigger('vcard:add');
 }
 }
 
 
 
 
 function getVCardForOccupant (occupant) {
 function getVCardForOccupant (occupant) {
+    const { vcards, xmppstatus } = _converse.state;
     const muc = occupant?.collection?.chatroom;
     const muc = occupant?.collection?.chatroom;
     const nick = occupant.get('nick');
     const nick = occupant.get('nick');
 
 
     if (nick && muc?.get('nick') === nick) {
     if (nick && muc?.get('nick') === nick) {
-        return _converse.xmppstatus.vcard;
+        return xmppstatus.vcard;
     } else {
     } else {
         const jid = occupant.get('jid') || occupant.get('from');
         const jid = occupant.get('jid') || occupant.get('from');
         if (jid) {
         if (jid) {
-            return _converse.vcards.get(jid) || _converse.vcards.create({ jid });
+            return vcards.get(jid) || vcards.create({ jid });
         } else {
         } else {
             log.warn(`Could not get VCard for occupant because no JID found!`);
             log.warn(`Could not get VCard for occupant because no JID found!`);
             return;
             return;
@@ -106,15 +122,16 @@ export async function setVCardOnOccupant (occupant) {
 
 
 
 
 function getVCardForMUCMessage (message) {
 function getVCardForMUCMessage (message) {
+    const { vcards, xmppstatus } = _converse.state;
     const muc = message?.collection?.chatbox;
     const muc = message?.collection?.chatbox;
     const nick = Strophe.getResourceFromJid(message.get('from'));
     const nick = Strophe.getResourceFromJid(message.get('from'));
 
 
     if (nick && muc?.get('nick') === nick) {
     if (nick && muc?.get('nick') === nick) {
-        return _converse.xmppstatus.vcard;
+        return xmppstatus.vcard;
     } else {
     } else {
         const jid = message.occupant?.get('jid') || message.get('from');
         const jid = message.occupant?.get('jid') || message.get('from');
         if (jid) {
         if (jid) {
-            return _converse.vcards.get(jid) || _converse.vcards.create({ jid });
+            return vcards.get(jid) || vcards.create({ jid });
         } else {
         } else {
             log.warn(`Could not get VCard for message because no JID found! msgid: ${message.get('msgid')}`);
             log.warn(`Could not get VCard for message because no JID found! msgid: ${message.get('msgid')}`);
             return;
             return;
@@ -137,24 +154,24 @@ export async function setVCardOnMUCMessage (message) {
 
 
 
 
 export async function initVCardCollection () {
 export async function initVCardCollection () {
-    _converse.vcards = new _converse.VCards();
-    const id = `${_converse.bare_jid}-converse.vcards`;
-    initStorage(_converse.vcards, id);
+    const vcards = new _converse.exports.VCards();
+    _converse.state.vcards = vcards;
+    Object.assign(_converse, { vcards }); // XXX DEPRECATED
+
+    const bare_jid = _converse.session.get('bare_jid');
+    const id = `${bare_jid}-converse.vcards`;
+    initStorage(vcards, id);
     await new Promise(resolve => {
     await new Promise(resolve => {
-        _converse.vcards.fetch({
+        vcards.fetch({
             'success': resolve,
             'success': resolve,
             'error': resolve
             'error': resolve
         }, {'silent': true});
         }, {'silent': true});
     });
     });
-    const vcards = _converse.vcards;
-    if (_converse.session) {
-        const jid = _converse.session.get('bare_jid');
-        const status = _converse.xmppstatus;
-        status.vcard = vcards.get(jid) || vcards.create({'jid': jid});
-        if (status.vcard) {
-            status.vcard.on('change', () => status.trigger('vcard:change'));
-            status.trigger('vcard:add');
-        }
+    const { xmppstatus } = _converse.state;
+    xmppstatus.vcard = vcards.get(bare_jid) || vcards.create({'jid': bare_jid});
+    if (xmppstatus.vcard) {
+        xmppstatus.vcard.on('change', () => xmppstatus.trigger('vcard:change'));
+        xmppstatus.trigger('vcard:add');
     }
     }
     /**
     /**
      * Triggered as soon as the `_converse.vcards` collection has been initialized and populated from cache.
      * Triggered as soon as the `_converse.vcards` collection has been initialized and populated from cache.
@@ -167,15 +184,17 @@ export async function initVCardCollection () {
 export function clearVCardsSession () {
 export function clearVCardsSession () {
     if (shouldClearCache()) {
     if (shouldClearCache()) {
         api.promises.add('VCardsInitialized');
         api.promises.add('VCardsInitialized');
-        if (_converse.vcards) {
-            _converse.vcards.clearStore();
-            delete _converse.vcards;
+        if (_converse.state.vcards) {
+            _converse.state.vcards.clearStore();
+            Object.assign(_converse, { vcards: undefined }); // XXX DEPRECATED
+            delete _converse.state.vcards;
         }
         }
     }
     }
 }
 }
 
 
 export async function getVCard (jid) {
 export async function getVCard (jid) {
-    const to = Strophe.getBareJidFromJid(jid) === _converse.bare_jid ? null : jid;
+    const bare_jid = _converse.session.get('bare_jid');
+    const to = Strophe.getBareJidFromJid(jid) === bare_jid ? null : jid;
     let iq;
     let iq;
     try {
     try {
         iq = await api.sendIQ(createStanza("get", to))
         iq = await api.sendIQ(createStanza("get", to))
@@ -186,5 +205,5 @@ export async function getVCard (jid) {
             'vcard_error': (new Date()).toISOString()
             'vcard_error': (new Date()).toISOString()
         }
         }
     }
     }
-    return onVCardData(jid, iq);
+    return onVCardData(iq);
 }
 }

+ 27 - 6
src/headless/shared/_converse.js

@@ -1,11 +1,13 @@
 /**
 /**
  * @module:shared.converse
  * @module:shared.converse
+ * @typedef {import('@converse/skeletor/src/storage.js').Storage} Storage
  */
  */
 import log from '../log.js';
 import log from '../log.js';
 import i18n from './i18n.js';
 import i18n from './i18n.js';
 import pluggable from 'pluggable.js/src/pluggable.js';
 import pluggable from 'pluggable.js/src/pluggable.js';
 import { EventEmitter, Model } from '@converse/skeletor';
 import { EventEmitter, Model } from '@converse/skeletor';
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
+import { isTestEnv } from '../utils/session.js';
 
 
 import {
 import {
     ACTIVE,
     ACTIVE,
@@ -61,7 +63,7 @@ class ConversePrivateGlobal extends EventEmitter(Object) {
         super();
         super();
         const proxy = new Proxy(this, {
         const proxy = new Proxy(this, {
             get: (target, key) => {
             get: (target, key) => {
-                if (typeof key === 'string') {
+                if (!isTestEnv() && typeof key === 'string') {
                     if (Object.keys(DEPRECATED_ATTRS).includes(key)) {
                     if (Object.keys(DEPRECATED_ATTRS).includes(key)) {
                         log.warn(`Accessing ${key} on _converse is DEPRECATED`);
                         log.warn(`Accessing ${key} on _converse is DEPRECATED`);
                     }
                     }
@@ -76,8 +78,14 @@ class ConversePrivateGlobal extends EventEmitter(Object) {
     initialize () {
     initialize () {
         this.VERSION_NAME = VERSION_NAME;
         this.VERSION_NAME = VERSION_NAME;
 
 
+        this.strict_plugin_dependencies = false;
+
+        this.pluggable = null;
+
         this.templates = {};
         this.templates = {};
 
 
+        this.storage = /** @type {Record<string, Storage.LocalForage>} */{};
+
         this.promises = {
         this.promises = {
             'initialized': getOpenPromise(),
             'initialized': getOpenPromise(),
         };
         };
@@ -96,6 +104,15 @@ class ConversePrivateGlobal extends EventEmitter(Object) {
 
 
         this.api = /** @type {module:shared-api.APIEndpoint} */ null;
         this.api = /** @type {module:shared-api.APIEndpoint} */ null;
 
 
+        /**
+         * Namespace for storing translated strings.
+         */
+        this.labels =
+            /**
+             * @typedef {Record<string, string>} UserMessage
+             * @typedef {Record<string, string|UserMessage>} UserMessage
+             * @type {UserMessages} */{};
+
         /**
         /**
          * Namespace for storing code that might be useful to 3rd party
          * Namespace for storing code that might be useful to 3rd party
          * plugins. We want to make it possible for 3rd party plugins to have
          * plugins. We want to make it possible for 3rd party plugins to have
@@ -117,11 +134,15 @@ class ConversePrivateGlobal extends EventEmitter(Object) {
         this.session?.destroy();
         this.session?.destroy();
         this.session = new Model();
         this.session = new Model();
 
 
-        // TODO: DEPRECATED
-        delete this.jid;
-        delete this.bare_jid;
-        delete this.domain;
-        delete this.resource;
+        // XXX DEPRECATED
+        Object.assign(
+            this, {
+                jid: undefined,
+                bare_jid: undefined,
+                domain: undefined,
+                resource: undefined
+            }
+        );
     }
     }
 
 
     /**
     /**

+ 7 - 7
src/headless/shared/api/events.js

@@ -10,21 +10,21 @@ export default {
      *
      *
      * Some events also double as promises and can be waited on via {@link _converse.api.waitUntil}.
      * Some events also double as promises and can be waited on via {@link _converse.api.waitUntil}.
      *
      *
-     * @method _converse.api.trigger
-     * @param { string } name - The event name
-     * @param {...any} [argument] - Argument to be passed to the event handler
-     * @param { object } [options]
-     * @param { boolean } [options.synchronous] - Whether the event is synchronous or not.
+     * @typedef {object} Options
+     * @property {boolean} [Options.synchronous] - Whether the event is synchronous or not.
      *  When a synchronous event is fired, a promise will be returned
      *  When a synchronous event is fired, a promise will be returned
      *  by {@link _converse.api.trigger} which resolves once all the
      *  by {@link _converse.api.trigger} which resolves once all the
      *  event handlers' promises have been resolved.
      *  event handlers' promises have been resolved.
+     *
+     * @method _converse.api.trigger
+     * @param { string } name - The event name
      */
      */
     async trigger (name) {
     async trigger (name) {
         if (!_converse._events) {
         if (!_converse._events) {
             return;
             return;
         }
         }
         const args = Array.from(arguments);
         const args = Array.from(arguments);
-        const options = args.pop();
+        const options = /** @type {Options} */(args.pop());
         if (options && options.synchronous) {
         if (options && options.synchronous) {
             const events = _converse._events[name] || [];
             const events = _converse._events[name] || [];
             const event_args = args.splice(1);
             const event_args = args.splice(1);
@@ -118,7 +118,7 @@ export default {
             } else {
             } else {
                 options = options || {};
                 options = options || {};
             }
             }
-            api.connection.get().addHandler(
+            _converse.api.connection.get().addHandler(
                 handler,
                 handler,
                 options.ns,
                 options.ns,
                 name,
                 name,

+ 4 - 1
src/headless/shared/api/presence.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder
+ */
 import _converse from '../_converse.js';
 import _converse from '../_converse.js';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 
 
@@ -21,7 +24,7 @@ export default {
             if (child_nodes && !Array.isArray(child_nodes)) {
             if (child_nodes && !Array.isArray(child_nodes)) {
                 child_nodes = [child_nodes];
                 child_nodes = [child_nodes];
             }
             }
-            const model = _converse.xmppstatus
+            const model = _converse.state.xmppstatus
             const presence = await model.constructPresence(type, to, status);
             const presence = await model.constructPresence(type, to, status);
             child_nodes?.map(c => c?.tree() ?? c).forEach(c => presence.cnode(c).up());
             child_nodes?.map(c => c?.tree() ?? c).forEach(c => presence.cnode(c).up());
             api.send(presence);
             api.send(presence);

+ 3 - 3
src/headless/shared/api/promise.js

@@ -43,8 +43,8 @@ export default {
          * via {@link _converse.api.listen}).
          * via {@link _converse.api.listen}).
          *
          *
          * @method _converse.api.promises.add
          * @method _converse.api.promises.add
-         * @param {string|array} [name|names] The name or an array of names for the promise(s) to be added
-         * @param { boolean } [replace=true] Whether this promise should be replaced with a new one when the user logs out.
+         * @param {string|array} [promises] The name or an array of names for the promise(s) to be added
+         * @param {boolean} [replace=true] Whether this promise should be replaced with a new one when the user logs out.
          * @example _converse.api.promises.add('foo-completed');
          * @example _converse.api.promises.add('foo-completed');
          */
          */
         add (promises, replace=true) {
         add (promises, replace=true) {
@@ -67,7 +67,7 @@ export default {
      */
      */
     waitUntil (condition) {
     waitUntil (condition) {
         if (isFunction(condition)) {
         if (isFunction(condition)) {
-            return waitUntil(condition);
+            return waitUntil(/** @type {Function} */(condition));
         } else {
         } else {
             const promise = _converse.promises[condition];
             const promise = _converse.promises[condition];
             if (promise === undefined) {
             if (promise === undefined) {

+ 11 - 15
src/headless/shared/api/public.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {module:shared-api-public.ConversePrivateGlobal} ConversePrivateGlobal
+ */
 import ConnectionFeedback from './../connection/feedback.js';
 import ConnectionFeedback from './../connection/feedback.js';
 import URI from 'urijs';
 import URI from 'urijs';
 import _converse from '../_converse.js';
 import _converse from '../_converse.js';
@@ -7,9 +10,8 @@ import log from '../../log.js';
 import sizzle from 'sizzle';
 import sizzle from 'sizzle';
 import u, { setLogLevelFromRoute } from '../../utils/index.js';
 import u, { setLogLevelFromRoute } from '../../utils/index.js';
 import { ANONYMOUS, CHAT_STATES, KEYCODES, VERSION_NAME } from '../constants.js';
 import { ANONYMOUS, CHAT_STATES, KEYCODES, VERSION_NAME } from '../constants.js';
-import { setUnloadEvent, isTestEnv } from '../../utils/session.js';
-import { Collection } from "@converse/skeletor";
-import { Model } from '@converse/skeletor';
+import { isTestEnv } from '../../utils/session.js';
+import { Collection, Model } from "@converse/skeletor";
 import { Strophe, $build, $iq, $msg, $pres, stx } from 'strophe.js';
 import { Strophe, $build, $iq, $msg, $pres, stx } from 'strophe.js';
 import { TimeoutError } from '../errors.js';
 import { TimeoutError } from '../errors.js';
 import { filesize } from 'filesize';
 import { filesize } from 'filesize';
@@ -26,6 +28,8 @@ import {
 } from '../../utils/init.js';
 } from '../../utils/init.js';
 
 
 /**
 /**
+ * @typedef {Window & {converse: ConversePrivateGlobal} } window
+ *
  * ### The Public API
  * ### The Public API
  *
  *
  * This namespace contains public API methods which are are
  * This namespace contains public API methods which are are
@@ -38,7 +42,7 @@ import {
  * @global
  * @global
  * @namespace converse
  * @namespace converse
  */
  */
-export const converse = Object.assign(window.converse || {}, {
+export const converse = Object.assign(/** @type {ConversePrivateGlobal} */(window).converse || {}, {
 
 
     CHAT_STATES,
     CHAT_STATES,
 
 
@@ -68,7 +72,6 @@ export const converse = Object.assign(window.converse || {}, {
         const { api } = _converse;
         const { api } = _converse;
         await cleanup(_converse);
         await cleanup(_converse);
 
 
-        setUnloadEvent();
         initAppSettings(settings);
         initAppSettings(settings);
         _converse.strict_plugin_dependencies = settings.strict_plugin_dependencies; // Needed by pluggable.js
         _converse.strict_plugin_dependencies = settings.strict_plugin_dependencies; // Needed by pluggable.js
         log.setLogLevel(api.settings.get("loglevel"));
         log.setLogLevel(api.settings.get("loglevel"));
@@ -84,16 +87,9 @@ export const converse = Object.assign(window.converse || {}, {
         setLogLevelFromRoute();
         setLogLevelFromRoute();
         addEventListener('hashchange', setLogLevelFromRoute);
         addEventListener('hashchange', setLogLevelFromRoute);
 
 
-        _converse.connfeedback = new ConnectionFeedback();
-
-        /* When reloading the page:
-         * For new sessions, we need to send out a presence stanza to notify
-         * the server/network that we're online.
-         * When re-attaching to an existing session we don't need to again send out a presence stanza,
-         * because it's as if "we never left" (see onConnectStatusChanged).
-         * https://github.com/conversejs/converse.js/issues/521
-         */
-        _converse.send_initial_presence = true;
+        const connfeedback = new ConnectionFeedback();
+        Object.assign(_converse, { connfeedback }); // XXX: DEPRECATED
+        Object.assign(_converse.state, { connfeedback });
 
 
         await initSessionStorage(_converse);
         await initSessionStorage(_converse);
         await initClientConfig(_converse);
         await initClientConfig(_converse);

+ 9 - 6
src/headless/shared/api/send.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('strophe.js/src/builder.js').Builder} Strophe.Builder
+ */
 import _converse from '../_converse.js';
 import _converse from '../_converse.js';
 import log from '../../log.js';
 import log from '../../log.js';
 import { Strophe, toStanza } from 'strophe.js';
 import { Strophe, toStanza } from 'strophe.js';
@@ -7,8 +10,8 @@ export default {
     /**
     /**
      * Allows you to send XML stanzas.
      * Allows you to send XML stanzas.
      * @method _converse.api.send
      * @method _converse.api.send
-     * @param { Element | Stanza } stanza
-     * @return { void }
+     * @param {Element|Strophe.Builder} stanza
+     * @return {void}
      * @example
      * @example
      * const msg = converse.env.$msg({
      * const msg = converse.env.$msg({
      *     'from': 'juliet@example.com/balcony',
      *     'from': 'juliet@example.com/balcony',
@@ -41,12 +44,12 @@ export default {
     /**
     /**
      * Send an IQ stanza
      * Send an IQ stanza
      * @method _converse.api.sendIQ
      * @method _converse.api.sendIQ
-     * @param { Element } stanza
-     * @param { number } [timeout] - The default timeout value is taken from
+     * @param {Element|Strophe.Builder} stanza
+     * @param {number} [timeout] - The default timeout value is taken from
      *  the `stanza_timeout` configuration setting.
      *  the `stanza_timeout` configuration setting.
-     * @param { Boolean } [reject=true] - Whether an error IQ should cause the promise
+     * @param {boolean} [reject=true] - Whether an error IQ should cause the promise
      *  to be rejected. If `false`, the promise will resolve instead of being rejected.
      *  to be rejected. If `false`, the promise will resolve instead of being rejected.
-     * @returns { Promise } A promise which resolves (or potentially rejected) once we
+     * @returns {Promise} A promise which resolves (or potentially rejected) once we
      *  receive a `result` or `error` stanza or once a timeout is reached.
      *  receive a `result` or `error` stanza or once a timeout is reached.
      *  If the IQ stanza being sent is of type `result` or `error`, there's
      *  If the IQ stanza being sent is of type `result` or `error`, there's
      *  nothing to wait for, so an already resolved promise is returned.
      *  nothing to wait for, so an already resolved promise is returned.

+ 26 - 15
src/headless/shared/chat/utils.js

@@ -1,8 +1,19 @@
+/**
+ * @module:headless-shared-chat-utils
+ * @typedef {import('../../plugins/muc/muc.js').default} MUC
+ * @typedef {import('../../plugins/chat/model.js').default} ChatBox
+ * @typedef {import('../../plugins/chat/message.js').default} Message
+ * @typedef {module:headless-shared-parsers.MediaURLMetadata} MediaURLMetadata
+ * @typedef {module:headless-shared-chat-utils.MediaURLData} MediaURLData
+ */
 import debounce from 'lodash-es/debounce.js';
 import debounce from 'lodash-es/debounce.js';
 import api, { converse } from '../../shared/api/index.js';
 import api, { converse } from '../../shared/api/index.js';
 
 
 const { u } = converse.env;
 const { u } = converse.env;
 
 
+/**
+ * @param {ChatBox|MUC} model
+ */
 export function pruneHistory (model) {
 export function pruneHistory (model) {
     const max_history = api.settings.get('prune_messages_above');
     const max_history = api.settings.get('prune_messages_above');
     if (max_history && typeof max_history === 'number') {
     if (max_history && typeof max_history === 'number') {
@@ -17,7 +28,7 @@ export function pruneHistory (model) {
                  * once older messages have been removed to keep the
                  * once older messages have been removed to keep the
                  * number of messages below the value set in `prune_messages_above`.
                  * number of messages below the value set in `prune_messages_above`.
                  * @event _converse#historyPruned
                  * @event _converse#historyPruned
-                 * @type { _converse.ChatBox | _converse.ChatRoom }
+                 * @type { ChatBox | MUC }
                  * @example _converse.api.listen.on('historyPruned', this => { ... });
                  * @example _converse.api.listen.on('historyPruned', this => { ... });
                  */
                  */
                 api.trigger('historyPruned', model);
                 api.trigger('historyPruned', model);
@@ -29,20 +40,20 @@ export function pruneHistory (model) {
 /**
 /**
  * Given an array of {@link MediaURLMetadata} objects and text, return an
  * Given an array of {@link MediaURLMetadata} objects and text, return an
  * array of {@link MediaURL} objects.
  * array of {@link MediaURL} objects.
- * @param { Array<MediaURLMetadata> } arr
- * @param { String } text
- * @returns{ Array<MediaURL> }
+ * @param {Array<MediaURLMetadata>} arr
+ * @param {String} text
+ * @returns{Array<MediaURLData>}
  */
  */
 export function getMediaURLs (arr, text, offset=0) {
 export function getMediaURLs (arr, text, offset=0) {
     /**
     /**
-     * @typedef { Object } MediaURLData
+     * @typedef {Object} MediaURLData
      * An object representing a URL found in a chat message
      * 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
+     * @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 => {
     return arr.map(o => {
         const start = o.start - offset;
         const start = o.start - offset;
@@ -63,11 +74,11 @@ export function getMediaURLs (arr, text, offset=0) {
  * Determines whether the given attributes of an incoming message
  * Determines whether the given attributes of an incoming message
  * represent a XEP-0308 correction and, if so, handles it appropriately.
  * represent a XEP-0308 correction and, if so, handles it appropriately.
  * @private
  * @private
- * @method _converse.ChatBox#handleCorrection
- * @param { _converse.ChatBox | _converse.ChatRoom }
- * @param { object } attrs - Attributes representing a received
+ * @method ChatBox#handleCorrection
+ * @param {ChatBox|MUC} model
+ * @param {object} attrs - Attributes representing a received
  *  message, as returned by {@link parseMessage}
  *  message, as returned by {@link parseMessage}
- * @returns { _converse.Message|undefined } Returns the corrected
+ * @returns {Promise<Message|void>} Returns the corrected
  *  message or `undefined` if not applicable.
  *  message or `undefined` if not applicable.
  */
  */
 export async function handleCorrection (model, attrs) {
 export async function handleCorrection (model, attrs) {

+ 1 - 1
src/headless/shared/connection/api.js

@@ -23,7 +23,7 @@ export default {
      * @method api.connection.init
      * @method api.connection.init
      * @memberOf api.connection
      * @memberOf api.connection
      * @param {string} [jid]
      * @param {string} [jid]
-     * @return {Connection | MockConnection}
+     * @return {Connection|MockConnection}
      */
      */
     init (jid) {
     init (jid) {
         if (jid && connection?.jid && isSameDomain(connection.jid, jid)) return connection;
         if (jid && connection?.jid && isSameDomain(connection.jid, jid)) return connection;

+ 1 - 1
src/headless/shared/connection/feedback.js

@@ -15,7 +15,7 @@ class Feedback extends Model {
     initialize () {
     initialize () {
         super.initialize();
         super.initialize();
         const { api } = _converse;
         const { api } = _converse;
-        this.on('change', () => api.trigger('connfeedback', _converse.connfeedback));
+        this.on('change', () => api.trigger('connfeedback', _converse.state.connfeedback));
     }
     }
 }
 }
 
 

+ 47 - 25
src/headless/shared/connection/index.js

@@ -23,6 +23,11 @@ export class Connection extends Strophe.Connection {
 
 
     constructor (service, options) {
     constructor (service, options) {
         super(service, options);
         super(service, options);
+        // For new sessions, we need to send out a presence stanza to notify
+        // the server/network that we're online.
+        // When re-attaching to an existing session we don't need to again send out a presence stanza,
+        // because it's as if "we never left" (see onConnectStatusChanged).
+        this.send_initial_presence = true;
         this.debouncedReconnect = debounce(this.reconnect, 3000);
         this.debouncedReconnect = debounce(this.reconnect, 3000);
     }
     }
 
 
@@ -68,11 +73,12 @@ export class Connection extends Strophe.Connection {
      * connection of their own XMPP server instead of a proxy provided by the
      * connection of their own XMPP server instead of a proxy provided by the
      * host of Converse.js.
      * host of Converse.js.
      * @method Connnection.discoverConnectionMethods
      * @method Connnection.discoverConnectionMethods
+     * @param {string} domain
      */
      */
     async discoverConnectionMethods (domain) {
     async discoverConnectionMethods (domain) {
         // Use XEP-0156 to check whether this host advertises websocket or BOSH connection methods.
         // Use XEP-0156 to check whether this host advertises websocket or BOSH connection methods.
         const options = {
         const options = {
-            'mode': 'cors',
+            'mode': /** @type {RequestMode} */('cors'),
             'headers': {
             'headers': {
                 'Accept': 'application/xrd+xml, text/xml'
                 'Accept': 'application/xrd+xml, text/xml'
             }
             }
@@ -97,9 +103,9 @@ export class Connection extends Strophe.Connection {
      * Establish a new XMPP session by logging in with the supplied JID and
      * Establish a new XMPP session by logging in with the supplied JID and
      * password.
      * password.
      * @method Connnection.connect
      * @method Connnection.connect
-     * @param { String } jid
-     * @param { String } password
-     * @param { Funtion } callback
+     * @param {String} jid
+     * @param {String} password
+     * @param {Function} callback
      */
      */
     async connect (jid, password, callback) {
     async connect (jid, password, callback) {
         if (api.settings.get("discover_connection_methods")) {
         if (api.settings.get("discover_connection_methods")) {
@@ -112,6 +118,14 @@ export class Connection extends Strophe.Connection {
         super.connect(jid, password, callback || this.onConnectStatusChanged, BOSH_WAIT);
         super.connect(jid, password, callback || this.onConnectStatusChanged, BOSH_WAIT);
     }
     }
 
 
+    /**
+     * @param {string} reason
+     */
+    disconnect(reason) {
+        super.disconnect(reason);
+        this.send_initial_presence = true;
+    }
+
     /**
     /**
      * Switch to a different transport if a service URL is available for it.
      * Switch to a different transport if a service URL is available for it.
      *
      *
@@ -123,8 +137,9 @@ export class Connection extends Strophe.Connection {
      * for the old transport are removed.
      * for the old transport are removed.
      */
      */
     async switchTransport () {
     async switchTransport () {
+        const bare_jid = _converse.session.get('bare_jid');
         if (api.connection.isType('websocket') && api.settings.get('bosh_service_url')) {
         if (api.connection.isType('websocket') && api.settings.get('bosh_service_url')) {
-            await setUserJID(_converse.bare_jid);
+            await setUserJID(bare_jid);
             this._proto._doDisconnect();
             this._proto._doDisconnect();
             this._proto = new Strophe.Bosh(this);
             this._proto = new Strophe.Bosh(this);
             this.service = api.settings.get('bosh_service_url');
             this.service = api.settings.get('bosh_service_url');
@@ -135,7 +150,7 @@ export class Connection extends Strophe.Connection {
                 // (now failed) session.
                 // (now failed) session.
                 await setUserJID(api.settings.get("jid"));
                 await setUserJID(api.settings.get("jid"));
             } else {
             } else {
-                await setUserJID(_converse.bare_jid);
+                await setUserJID(bare_jid);
             }
             }
             this._proto._doDisconnect();
             this._proto._doDisconnect();
             this._proto = new Strophe.Websocket(this);
             this._proto = new Strophe.Websocket(this);
@@ -148,7 +163,7 @@ export class Connection extends Strophe.Connection {
         this.reconnecting = true;
         this.reconnecting = true;
         await tearDown();
         await tearDown();
 
 
-        const conn_status = _converse.connfeedback.get('connection_status');
+        const conn_status = _converse.state.connfeedback.get('connection_status');
         if (conn_status === Strophe.Status.CONNFAIL) {
         if (conn_status === Strophe.Status.CONNFAIL) {
             this.switchTransport();
             this.switchTransport();
         } else if (conn_status === Strophe.Status.AUTHFAIL && api.settings.get("authentication") === ANONYMOUS) {
         } else if (conn_status === Strophe.Status.AUTHFAIL && api.settings.get("authentication") === ANONYMOUS) {
@@ -168,14 +183,15 @@ export class Connection extends Strophe.Connection {
         if (api.settings.get("authentication") === ANONYMOUS) {
         if (api.settings.get("authentication") === ANONYMOUS) {
             await clearSession();
             await clearSession();
         }
         }
-        return api.user.login(_converse.jid);
+        const jid = _converse.session.get('jid');
+        return api.user.login(jid);
     }
     }
 
 
     /**
     /**
      * Called as soon as a new connection has been established, either
      * Called as soon as a new connection has been established, either
      * by logging in or by attaching to an existing BOSH session.
      * by logging in or by attaching to an existing BOSH session.
      * @method Connection.onConnected
      * @method Connection.onConnected
-     * @param { Boolean } reconnecting - Whether Converse.js reconnected from an earlier dropped session.
+     * @param {Boolean} [reconnecting] - Whether Converse.js reconnected from an earlier dropped session.
      */
      */
     async onConnected (reconnecting) {
     async onConnected (reconnecting) {
         delete this.reconnecting;
         delete this.reconnecting;
@@ -184,8 +200,9 @@ export class Connection extends Strophe.Connection {
 
 
         // Save the current JID in persistent storage so that we can attempt to
         // Save the current JID in persistent storage so that we can attempt to
         // recreate the session from SCRAM keys
         // recreate the session from SCRAM keys
-        if (_converse.config.get('trusted')) {
-            localStorage.setItem('conversejs-session-jid', _converse.bare_jid);
+        if (_converse.state.config.get('trusted')) {
+            const bare_jid = _converse.session.get('bare_jid');
+            localStorage.setItem('conversejs-session-jid', bare_jid);
         }
         }
 
 
         /**
         /**
@@ -218,10 +235,10 @@ export class Connection extends Strophe.Connection {
      * Used to keep track of why we got disconnected, so that we can
      * Used to keep track of why we got disconnected, so that we can
      * decide on what the next appropriate action is (in onDisconnected)
      * decide on what the next appropriate action is (in onDisconnected)
      * @method Connection.setDisconnectionCause
      * @method Connection.setDisconnectionCause
-     * @param { Number } cause - The status number as received from Strophe.
-     * @param { String } [reason] - An optional user-facing message as to why
+     * @param {Number|'logout'} [cause] - The status number as received from Strophe.
+     * @param {String} [reason] - An optional user-facing message as to why
      *  there was a disconnection.
      *  there was a disconnection.
-     * @param { Boolean } [override] - An optional flag to replace any previous
+     * @param {Boolean} [override] - An optional flag to replace any previous
      *  disconnection cause and reason.
      *  disconnection cause and reason.
      */
      */
     setDisconnectionCause (cause, reason, override) {
     setDisconnectionCause (cause, reason, override) {
@@ -234,9 +251,13 @@ export class Connection extends Strophe.Connection {
         }
         }
     }
     }
 
 
+    /**
+     * @param {Number} [status] - The status number as received from Strophe.
+     * @param {String} [message] - An optional user-facing message
+     */
     setConnectionStatus (status, message) {
     setConnectionStatus (status, message) {
         this.status = status;
         this.status = status;
-        _converse.connfeedback.set({'connection_status': status, message });
+        _converse.state.connfeedback.set({'connection_status': status, message });
     }
     }
 
 
     async finishDisconnection () {
     async finishDisconnection () {
@@ -306,8 +327,8 @@ export class Connection extends Strophe.Connection {
      * Callback method called by Strophe as the Connection goes
      * Callback method called by Strophe as the Connection goes
      * through various states while establishing or tearing down a
      * through various states while establishing or tearing down a
      * connection.
      * connection.
-     * @param { Number } status
-     * @param { String } message
+     * @param {Number} status
+     * @param {String} message
      */
      */
     onConnectStatusChanged (status, message) {
     onConnectStatusChanged (status, message) {
         const { __ } = _converse;
         const { __ } = _converse;
@@ -324,8 +345,6 @@ export class Connection extends Strophe.Connection {
             this.setConnectionStatus(status);
             this.setConnectionStatus(status);
             this.worker_attach_promise?.resolve(true);
             this.worker_attach_promise?.resolve(true);
 
 
-            // By default we always want to send out an initial presence stanza.
-            _converse.send_initial_presence = true;
             this.setDisconnectionCause();
             this.setDisconnectionCause();
             if (this.reconnecting) {
             if (this.reconnecting) {
                 log.debug(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached');
                 log.debug(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached');
@@ -335,7 +354,7 @@ export class Connection extends Strophe.Connection {
                 if (this.restored) {
                 if (this.restored) {
                     // No need to send an initial presence stanza when
                     // No need to send an initial presence stanza when
                     // we're restoring an existing session.
                     // we're restoring an existing session.
-                    _converse.send_initial_presence = false;
+                    this.send_initial_presence = false;
                 }
                 }
                 this.onConnected();
                 this.onConnected();
             }
             }
@@ -375,6 +394,9 @@ export class Connection extends Strophe.Connection {
         }
         }
     }
     }
 
 
+    /**
+     * @param {string} type
+     */
     isType (type) {
     isType (type) {
         if (type.toLowerCase() === 'websocket') {
         if (type.toLowerCase() === 'websocket') {
             return this._proto instanceof Strophe.Websocket;
             return this._proto instanceof Strophe.Websocket;
@@ -385,7 +407,7 @@ export class Connection extends Strophe.Connection {
 
 
     hasResumed () {
     hasResumed () {
         if (api.settings.get("connection_options")?.worker || this.isType('bosh')) {
         if (api.settings.get("connection_options")?.worker || this.isType('bosh')) {
-            return _converse.connfeedback.get('connection_status') === Strophe.Status.ATTACHED;
+            return _converse.state.connfeedback.get('connection_status') === Strophe.Status.ATTACHED;
         } else {
         } else {
             // Not binding means that the session was resumed.
             // Not binding means that the session was resumed.
             return !this.do_bind;
             return !this.do_bind;
@@ -425,10 +447,12 @@ export class MockConnection extends Connection {
                 '<session xmlns="urn:ietf:params:xml:ns:xmpp-session">'+
                 '<session xmlns="urn:ietf:params:xml:ns:xmpp-session">'+
                     '<optional/>'+
                     '<optional/>'+
                 '</session>'+
                 '</session>'+
-            '</stream:features>').firstChild;
+            '</stream:features>').firstElementChild;
 
 
+        // eslint-disable-next-line @typescript-eslint/no-empty-function
         this._proto._processRequest = () => {};
         this._proto._processRequest = () => {};
         this._proto._disconnect = () => this._onDisconnectTimeout();
         this._proto._disconnect = () => this._onDisconnectTimeout();
+        // eslint-disable-next-line @typescript-eslint/no-empty-function
         this._proto._onDisconnectTimeout = () => {};
         this._proto._onDisconnectTimeout = () => {};
         this._proto._connect = () => {
         this._proto._connect = () => {
             this.connected = true;
             this.connected = true;
@@ -460,8 +484,6 @@ export class MockConnection extends Connection {
     async bind () {
     async bind () {
         await api.trigger('beforeResourceBinding', {'synchronous': true});
         await api.trigger('beforeResourceBinding', {'synchronous': true});
         this.authenticated = true;
         this.authenticated = true;
-        if (!_converse.no_connection_on_bind) {
-            this._changeConnectStatus(Strophe.Status.CONNECTED);
-        }
+        this._changeConnectStatus(Strophe.Status.CONNECTED);
     }
     }
 }
 }

+ 10 - 1
src/headless/shared/errors.js

@@ -2,4 +2,13 @@
  * Custom error for indicating timeouts
  * Custom error for indicating timeouts
  * @namespace converse.env
  * @namespace converse.env
  */
  */
-export class TimeoutError extends Error {}
+export class TimeoutError extends Error {
+
+    /**
+     * @param  {string} message
+     */
+    constructor (message) {
+        super(message);
+        this.retry_event_id = null;
+    }
+}

+ 1 - 1
src/headless/shared/i18n.js

@@ -4,6 +4,7 @@ import { sprintf } from 'sprintf-js';
  * @namespace i18n
  * @namespace i18n
  */
  */
 export default {
 export default {
+    // eslint-disable-next-line @typescript-eslint/no-empty-function
     initialize () {},
     initialize () {},
 
 
     /**
     /**
@@ -18,7 +19,6 @@ export default {
      * @method __
      * @method __
      * @private
      * @private
      * @memberOf i18n
      * @memberOf i18n
-     * @param { String } str
      */
      */
     __ (...args) {
     __ (...args) {
         return sprintf(...args);
         return sprintf(...args);

+ 35 - 14
src/headless/shared/parsers.js

@@ -1,3 +1,8 @@
+/**
+ * @module:headless-shared-parsers
+ * @typedef {module:headless-shared-parsers.MediaURLMetadata} MediaURLMetadata
+ * @typedef {module:headless-shared-parsers.Reference} Reference
+ */
 import URI from 'urijs';
 import URI from 'urijs';
 import _converse from './_converse.js';
 import _converse from './_converse.js';
 import api from './api/index.js';
 import api from './api/index.js';
@@ -18,8 +23,12 @@ import {
 const { NS } = Strophe;
 const { NS } = Strophe;
 
 
 export class StanzaParseError extends Error {
 export class StanzaParseError extends Error {
+    /**
+     * @param {string} message
+     * @param {Element} stanza
+     */
     constructor (message, stanza) {
     constructor (message, stanza) {
-        super(message, stanza);
+        super(message);
         this.name = 'StanzaParseError';
         this.name = 'StanzaParseError';
         this.stanza = stanza;
         this.stanza = stanza;
     }
     }
@@ -28,9 +37,10 @@ export class StanzaParseError extends Error {
 /**
 /**
  * Extract the XEP-0359 stanza IDs from the passed in stanza
  * Extract the XEP-0359 stanza IDs from the passed in stanza
  * and return a map containing them.
  * and return a map containing them.
- * @private
- * @param { Element } stanza - The message stanza
- * @returns { Object }
+ * @param {Element} stanza - The message stanza
+ * @param {Element} original_stanza - The encapsulating stanza which contains
+ *      the message stanza.
+ * @returns {Object}
  */
  */
 export function getStanzaIDs (stanza, original_stanza) {
 export function getStanzaIDs (stanza, original_stanza) {
     const attrs = {};
     const attrs = {};
@@ -45,7 +55,8 @@ export function getStanzaIDs (stanza, original_stanza) {
     // Store the archive id
     // Store the archive id
     const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
     const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
     if (result) {
     if (result) {
-        const by_jid = original_stanza.getAttribute('from') || _converse.bare_jid;
+        const bare_jid = _converse.session.get('bare_jid');
+        const by_jid = original_stanza.getAttribute('from') || bare_jid;
         attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
         attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
     }
     }
 
 
@@ -57,6 +68,9 @@ export function getStanzaIDs (stanza, original_stanza) {
     return attrs;
     return attrs;
 }
 }
 
 
+/**
+ * @param {Element} stanza
+ */
 export function getEncryptionAttributes (stanza) {
 export function getEncryptionAttributes (stanza) {
     const eme_tag = sizzle(`encryption[xmlns="${Strophe.NS.EME}"]`, stanza).pop();
     const eme_tag = sizzle(`encryption[xmlns="${Strophe.NS.EME}"]`, stanza).pop();
     const namespace = eme_tag?.getAttribute('namespace');
     const namespace = eme_tag?.getAttribute('namespace');
@@ -245,7 +259,7 @@ export function getErrorAttributes (stanza) {
 
 
 /**
 /**
  * Given a message stanza, find and return any XEP-0372 references
  * Given a message stanza, find and return any XEP-0372 references
- * @param { Element } stana - The message stanza
+ * @param {Element} stanza - The message stanza
  * @returns { Reference }
  * @returns { Reference }
  */
  */
 export function getReferences (stanza) {
 export function getReferences (stanza) {
@@ -276,6 +290,9 @@ export function getReferences (stanza) {
     }).filter(r => r);
     }).filter(r => r);
 }
 }
 
 
+/**
+ * @param {Element} stanza
+ */
 export function getReceiptId (stanza) {
 export function getReceiptId (stanza) {
     const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop();
     const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop();
     return receipt?.getAttribute('id');
     return receipt?.getAttribute('id');
@@ -337,10 +354,9 @@ export function throwErrorIfInvalidForward (stanza) {
 
 
 /**
 /**
  * Determines whether the passed in stanza is a XEP-0333 Chat Marker
  * Determines whether the passed in stanza is a XEP-0333 Chat Marker
- * @private
  * @method getChatMarker
  * @method getChatMarker
- * @param { Element } stanza - The message stanza
- * @returns { Boolean }
+ * @param {Element} stanza - The message stanza
+ * @returns {Element}
  */
  */
 export function getChatMarker (stanza) {
 export function getChatMarker (stanza) {
     // If we receive more than one marker (which shouldn't happen), we take
     // If we receive more than one marker (which shouldn't happen), we take
@@ -353,10 +369,16 @@ export function getChatMarker (stanza) {
     ).pop();
     ).pop();
 }
 }
 
 
+/**
+ * @param {Element} stanza
+ */
 export function isHeadline (stanza) {
 export function isHeadline (stanza) {
     return stanza.getAttribute('type') === 'headline';
     return stanza.getAttribute('type') === 'headline';
 }
 }
 
 
+/**
+ * @param {Element} stanza
+ */
 export function isServerMessage (stanza) {
 export function isServerMessage (stanza) {
     if (sizzle(`mentions[xmlns="${Strophe.NS.MENTIONS}"]`, stanza).pop()) {
     if (sizzle(`mentions[xmlns="${Strophe.NS.MENTIONS}"]`, stanza).pop()) {
         return false;
         return false;
@@ -374,10 +396,9 @@ export function isServerMessage (stanza) {
 
 
 /**
 /**
  * Determines whether the passed in stanza is a XEP-0313 MAM stanza
  * Determines whether the passed in stanza is a XEP-0313 MAM stanza
- * @private
  * @method isArchived
  * @method isArchived
- * @param { Element } stanza - The message stanza
- * @returns { Boolean }
+ * @param {Element} original_stanza - The message stanza
+ * @returns {boolean}
  */
  */
 export function isArchived (original_stanza) {
 export function isArchived (original_stanza) {
     return !!sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
     return !!sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
@@ -387,8 +408,8 @@ export function isArchived (original_stanza) {
 /**
 /**
  * Returns an object containing all attribute names and values for a particular element.
  * Returns an object containing all attribute names and values for a particular element.
  * @method getAttributes
  * @method getAttributes
- * @param { Element } stanza
- * @returns { Object }
+ * @param {Element} stanza
+ * @returns {object}
  */
  */
 export function getAttributes (stanza) {
 export function getAttributes (stanza) {
     return stanza.getAttributeNames().reduce((acc, name) => {
     return stanza.getAttributeNames().reduce((acc, name) => {

+ 5 - 6
src/headless/shared/rsm.js

@@ -6,7 +6,6 @@
  *   Some code taken from the Strophe RSM plugin, licensed under the MIT License
  *   Some code taken from the Strophe RSM plugin, licensed under the MIT License
  *   Copyright 2006-2017 Strophe (https://github.com/strophe/strophejs)
  *   Copyright 2006-2017 Strophe (https://github.com/strophe/strophejs)
  */
  */
-import _converse from './_converse.js';
 import { converse } from './api/index.js';
 import { converse } from './api/index.js';
 import pick from 'lodash-es/pick';
 import pick from 'lodash-es/pick';
 
 
@@ -16,12 +15,12 @@ Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
 
 
 
 
 /**
 /**
- * @typedef { Object } RSMQueryParameters
+ * @typedef {Object} RSMQueryParameters
  * [XEP-0059 RSM](https://xmpp.org/extensions/xep-0059.html) Attributes that can be used to filter query results
  * [XEP-0059 RSM](https://xmpp.org/extensions/xep-0059.html) Attributes that can be used to filter query results
- * @property { String } [after] - The XEP-0359 stanza ID of a message after which messages should be returned. Implies forward paging.
- * @property { String } [before] - The XEP-0359 stanza ID of a message before which messages should be returned. Implies backward paging.
- * @property { number } [index=0] - The index of the results page to return.
- * @property { number } [max] - The maximum number of items to return.
+ * @property {String} [after] - The XEP-0359 stanza ID of a message after which messages should be returned. Implies forward paging.
+ * @property {String} [before] - The XEP-0359 stanza ID of a message before which messages should be returned. Implies backward paging.
+ * @property {number} [index=0] - The index of the results page to return.
+ * @property {number} [max] - The maximum number of items to return.
  */
  */
 
 
 const RSM_QUERY_PARAMETERS = ['after', 'before', 'index', 'max'];
 const RSM_QUERY_PARAMETERS = ['after', 'before', 'index', 'max'];

+ 3 - 0
src/headless/shared/settings/api.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('@converse/skeletor').Model} Model
+ */
 import log from '../../log.js';
 import log from '../../log.js';
 import {
 import {
     clearUserSettings,
     clearUserSettings,

+ 2 - 1
src/headless/shared/settings/constants.js

@@ -38,17 +38,18 @@ export const DEFAULT_SETTINGS = {
     assets_path: '/dist',
     assets_path: '/dist',
     authentication: 'login', // Available values are "login", "prebind", "anonymous" and "external".
     authentication: 'login', // Available values are "login", "prebind", "anonymous" and "external".
     auto_login: false, // Currently only used in connection with anonymous login
     auto_login: false, // Currently only used in connection with anonymous login
-    reuse_scram_keys: false,
     auto_reconnect: true,
     auto_reconnect: true,
     blacklisted_plugins: [],
     blacklisted_plugins: [],
     clear_cache_on_logout: false,
     clear_cache_on_logout: false,
     connection_options: {},
     connection_options: {},
     credentials_url: null, // URL from where login credentials can be fetched
     credentials_url: null, // URL from where login credentials can be fetched
+    disable_effects: false, // Disabled UI transition effects. Mainly used for tests.
     discover_connection_methods: true,
     discover_connection_methods: true,
     geouri_regex: /https\:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([\-0-9.]+)\/([\-0-9.]+)\S*/g,
     geouri_regex: /https\:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([\-0-9.]+)\/([\-0-9.]+)\S*/g,
     geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2',
     geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2',
     i18n: undefined,
     i18n: undefined,
     jid: undefined,
     jid: undefined,
+    reuse_scram_keys: false,
     keepalive: true,
     keepalive: true,
     loglevel: 'info',
     loglevel: 'info',
     locales: [
     locales: [

+ 9 - 7
src/headless/shared/settings/utils.js

@@ -95,21 +95,22 @@ export function updateAppSettings (key, val) {
 }
 }
 
 
 /**
 /**
- * @async
+ * @returns {Promise<void>|void} A promise when the user settings object
+ *  is created anew and it's contents fetched from storage.
  */
  */
 function initUserSettings () {
 function initUserSettings () {
-    if (!_converse.bare_jid) {
+    const bare_jid = _converse.session.get('bare_jid');
+    if (!bare_jid) {
         const msg = "No JID to fetch user settings for";
         const msg = "No JID to fetch user settings for";
         log.error(msg);
         log.error(msg);
         throw Error(msg);
         throw Error(msg);
     }
     }
-    if (!user_settings?.fetched) {
-        const id = `converse.user-settings.${_converse.bare_jid}`;
+    const id = `converse.user-settings.${bare_jid}`;
+    if (user_settings?.get('id') !== id) {
         user_settings = new Model({id});
         user_settings = new Model({id});
         initStorage(user_settings, id);
         initStorage(user_settings, id);
-        user_settings.fetched = user_settings.fetch({'promise': true});
+        return user_settings.fetch({'promise': true});
     }
     }
-    return user_settings.fetched;
 }
 }
 
 
 export async function getUserSettings () {
 export async function getUserSettings () {
@@ -123,7 +124,8 @@ export async function updateUserSettings (data, options) {
 }
 }
 
 
 export async function clearUserSettings () {
 export async function clearUserSettings () {
-    if (_converse.bare_jid) {
+    const bare_jid = _converse.session.get('bare_jid');
+    if (bare_jid) {
         await initUserSettings();
         await initUserSettings();
         return user_settings.clear();
         return user_settings.clear();
     }
     }

+ 8 - 21
src/headless/tests/converse.js

@@ -9,25 +9,23 @@ describe("Converse", function() {
         it("are sent out when the client becomes or stops being idle",
         it("are sent out when the client becomes or stops being idle",
             mock.initConverse(['discoInitialized'], {}, (_converse) => {
             mock.initConverse(['discoInitialized'], {}, (_converse) => {
 
 
-            spyOn(_converse, 'sendCSI').and.callThrough();
-            let sent_stanza;
-            spyOn(_converse.api.connection.get(), 'send').and.callFake(function (stanza) {
+            let i = 0;
+            const domain = _converse.session.get('domain');
+            _converse.disco_entities.get(domain).features['urn:xmpp:csi:0'] = true; // Mock that the server supports CSI
+
+            let sent_stanza = null;
+            spyOn(_converse.api.connection.get(), 'send').and.callFake((stanza) => {
                 sent_stanza = stanza;
                 sent_stanza = stanza;
             });
             });
-            let i = 0;
-            _converse.idle_seconds = 0; // Usually initialized by registerIntervalHandler
-            _converse.disco_entities.get(_converse.domain).features['urn:xmpp:csi:0'] = true; // Mock that the server supports CSI
 
 
             _converse.api.settings.set('csi_waiting_time', 3);
             _converse.api.settings.set('csi_waiting_time', 3);
             while (i <= _converse.api.settings.get("csi_waiting_time")) {
             while (i <= _converse.api.settings.get("csi_waiting_time")) {
-                expect(_converse.sendCSI).not.toHaveBeenCalled();
-                _converse.onEverySecond();
+                expect(sent_stanza).toBe(null);
+                _converse.exports.onEverySecond();
                 i++;
                 i++;
             }
             }
-            expect(_converse.sendCSI).toHaveBeenCalledWith('inactive');
             expect(Strophe.serialize(sent_stanza)).toBe('<inactive xmlns="urn:xmpp:csi:0"/>');
             expect(Strophe.serialize(sent_stanza)).toBe('<inactive xmlns="urn:xmpp:csi:0"/>');
             _converse.onUserActivity();
             _converse.onUserActivity();
-            expect(_converse.sendCSI).toHaveBeenCalledWith('active');
             expect(Strophe.serialize(sent_stanza)).toBe('<active xmlns="urn:xmpp:csi:0"/>');
             expect(Strophe.serialize(sent_stanza)).toBe('<active xmlns="urn:xmpp:csi:0"/>');
         }));
         }));
     });
     });
@@ -40,8 +38,6 @@ describe("Converse", function() {
             const { api } = _converse;
             const { api } = _converse;
             let i = 0;
             let i = 0;
             // Usually initialized by registerIntervalHandler
             // Usually initialized by registerIntervalHandler
-            _converse.idle_seconds = 0;
-            _converse.auto_changed_status = false;
             _converse.api.settings.set('auto_away', 3);
             _converse.api.settings.set('auto_away', 3);
             _converse.api.settings.set('auto_xa', 6);
             _converse.api.settings.set('auto_xa', 6);
 
 
@@ -49,7 +45,6 @@ describe("Converse", function() {
             while (i <= _converse.api.settings.get("auto_away")) {
             while (i <= _converse.api.settings.get("auto_away")) {
                 _converse.onEverySecond(); i++;
                 _converse.onEverySecond(); i++;
             }
             }
-            expect(_converse.auto_changed_status).toBe(true);
 
 
             while (i <= api.settings.get('auto_xa')) {
             while (i <= api.settings.get('auto_xa')) {
                 expect(await _converse.api.user.status.get()).toBe('away');
                 expect(await _converse.api.user.status.get()).toBe('away');
@@ -57,11 +52,9 @@ describe("Converse", function() {
                 i++;
                 i++;
             }
             }
             expect(await _converse.api.user.status.get()).toBe('xa');
             expect(await _converse.api.user.status.get()).toBe('xa');
-            expect(_converse.auto_changed_status).toBe(true);
 
 
             _converse.onUserActivity();
             _converse.onUserActivity();
             expect(await _converse.api.user.status.get()).toBe('online');
             expect(await _converse.api.user.status.get()).toBe('online');
-            expect(_converse.auto_changed_status).toBe(false);
 
 
             // Check that it also works for the chat feature
             // Check that it also works for the chat feature
             await _converse.api.user.status.set('chat')
             await _converse.api.user.status.set('chat')
@@ -70,18 +63,15 @@ describe("Converse", function() {
                 _converse.onEverySecond();
                 _converse.onEverySecond();
                 i++;
                 i++;
             }
             }
-            expect(_converse.auto_changed_status).toBe(true);
             while (i <= api.settings.get('auto_xa')) {
             while (i <= api.settings.get('auto_xa')) {
                 expect(await _converse.api.user.status.get()).toBe('away');
                 expect(await _converse.api.user.status.get()).toBe('away');
                 _converse.onEverySecond();
                 _converse.onEverySecond();
                 i++;
                 i++;
             }
             }
             expect(await _converse.api.user.status.get()).toBe('xa');
             expect(await _converse.api.user.status.get()).toBe('xa');
-            expect(_converse.auto_changed_status).toBe(true);
 
 
             _converse.onUserActivity();
             _converse.onUserActivity();
             expect(await _converse.api.user.status.get()).toBe('online');
             expect(await _converse.api.user.status.get()).toBe('online');
-            expect(_converse.auto_changed_status).toBe(false);
 
 
             // Check that it doesn't work for 'dnd'
             // Check that it doesn't work for 'dnd'
             await _converse.api.user.status.set('dnd');
             await _converse.api.user.status.set('dnd');
@@ -91,18 +81,15 @@ describe("Converse", function() {
                 i++;
                 i++;
             }
             }
             expect(await _converse.api.user.status.get()).toBe('dnd');
             expect(await _converse.api.user.status.get()).toBe('dnd');
-            expect(_converse.auto_changed_status).toBe(false);
             while (i <= api.settings.get('auto_xa')) {
             while (i <= api.settings.get('auto_xa')) {
                 expect(await _converse.api.user.status.get()).toBe('dnd');
                 expect(await _converse.api.user.status.get()).toBe('dnd');
                 _converse.onEverySecond();
                 _converse.onEverySecond();
                 i++;
                 i++;
             }
             }
             expect(await _converse.api.user.status.get()).toBe('dnd');
             expect(await _converse.api.user.status.get()).toBe('dnd');
-            expect(_converse.auto_changed_status).toBe(false);
 
 
             _converse.onUserActivity();
             _converse.onUserActivity();
             expect(await _converse.api.user.status.get()).toBe('dnd');
             expect(await _converse.api.user.status.get()).toBe('dnd');
-            expect(_converse.auto_changed_status).toBe(false);
         }));
         }));
     });
     });
 
 

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

@@ -15,7 +15,7 @@ export function arrayBufferToString (ab) {
 }
 }
 
 
 export function stringToArrayBuffer (string) {
 export function stringToArrayBuffer (string) {
-    const bytes = new TextEncoder("utf-8").encode(string);
+    const bytes = new TextEncoder().encode(string);
     return bytes.buffer;
     return bytes.buffer;
 }
 }
 
 

+ 1 - 0
src/headless/utils/index.js

@@ -51,6 +51,7 @@ import {
 /**
 /**
  * The utils object
  * The utils object
  * @namespace u
  * @namespace u
+ * @type {Record<string, Function>}
  */
  */
 const u = {};
 const u = {};
 
 

+ 31 - 26
src/headless/utils/init.js

@@ -15,7 +15,7 @@ import { Strophe } from 'strophe.js';
 import { createStore, initStorage } from './storage.js';
 import { createStore, initStorage } from './storage.js';
 import { getConnectionServiceURL } from '../shared/connection/utils';
 import { getConnectionServiceURL } from '../shared/connection/utils';
 import { isValidJID } from './jid.js';
 import { isValidJID } from './jid.js';
-import { isTestEnv } from './session.js';
+import { getUnloadEvent, isTestEnv } from './session.js';
 
 
 
 
 /**
 /**
@@ -77,9 +77,13 @@ export async function initClientConfig (_converse) {
      * user sessions.
      * user sessions.
      */
      */
     const id = 'converse.client-config';
     const id = 'converse.client-config';
-    _converse.config = new Model({ id, 'trusted': true });
-    _converse.config.browserStorage = createStore(id, "session");
-    await new Promise(r => _converse.config.fetch({'success': r, 'error': r}));
+    const config = new Model({ id, 'trusted': true });
+    config.browserStorage = createStore(id, "session");
+
+    Object.assign(_converse, { config }); // XXX DEPRECATED
+    Object.assign(_converse.state, { config });
+
+    await new Promise(r => config.fetch({'success': r, 'error': r}));
     /**
     /**
      * Triggered once the XMPP-client configuration has been initialized.
      * Triggered once the XMPP-client configuration has been initialized.
      * The client configuration is independent of any particular and its values
      * The client configuration is independent of any particular and its values
@@ -98,13 +102,11 @@ export async function initClientConfig (_converse) {
  */
  */
 export async function initSessionStorage (_converse) {
 export async function initSessionStorage (_converse) {
     await Storage.sessionStorageInitialized;
     await Storage.sessionStorageInitialized;
-    _converse.storage = {
-        'session': Storage.localForage.createInstance({
-            'name': isTestEnv() ? 'converse-test-session' : 'converse-session',
-            'description': 'sessionStorage instance',
-            'driver': ['sessionStorageWrapper']
-        })
-    };
+    _converse.storage['session'] = Storage.localForage.createInstance({
+        'name': isTestEnv() ? 'converse-test-session' : 'converse-session',
+        'description': 'sessionStorage instance',
+        'driver': ['sessionStorageWrapper']
+    });
 }
 }
 
 
 
 
@@ -224,7 +226,7 @@ export async function initSession (_converse, jid) {
         saveJIDtoSession(_converse, jid);
         saveJIDtoSession(_converse, jid);
 
 
         // Set `active` flag to false when the tab gets reloaded
         // Set `active` flag to false when the tab gets reloaded
-        window.addEventListener(_converse.unloadevent, () => _converse.session?.save('active', false));
+        window.addEventListener(getUnloadEvent(), () => _converse.session?.save('active', false));
 
 
         /**
         /**
          * Triggered once the user's session has been initialized. The session is a
          * Triggered once the user's session has been initialized. The session is a
@@ -372,6 +374,7 @@ export async function attemptNonPreboundSession (credentials, automatic) {
     const { api } = _converse;
     const { api } = _converse;
 
 
     if (api.settings.get("authentication") === LOGIN) {
     if (api.settings.get("authentication") === LOGIN) {
+        const jid = _converse.session.get('jid');
         // XXX: If EITHER ``keepalive`` or ``auto_login`` is ``true`` and
         // XXX: If EITHER ``keepalive`` or ``auto_login`` is ``true`` and
         // ``authentication`` is set to ``login``, then Converse will try to log the user in,
         // ``authentication`` is set to ``login``, then Converse will try to log the user in,
         // since we don't have a way to distinguish between wether we're
         // since we don't have a way to distinguish between wether we're
@@ -384,7 +387,7 @@ export async function attemptNonPreboundSession (credentials, automatic) {
             // We give credentials_url preference, because
             // We give credentials_url preference, because
             // connection.pass might be an expired token.
             // connection.pass might be an expired token.
             return connect(await getLoginCredentialsFromURL());
             return connect(await getLoginCredentialsFromURL());
-        } else if (_converse.jid && (api.settings.get("password") || api.connection.get().pass)) {
+        } else if (jid && (api.settings.get("password") || api.connection.get().pass)) {
             return connect();
             return connect();
         }
         }
 
 
@@ -437,9 +440,10 @@ export async function savedLoginInfo (jid) {
  */
  */
 async function connect (credentials) {
 async function connect (credentials) {
     const { api } = _converse;
     const { api } = _converse;
+    const jid = _converse.session.get('jid');
     const connection = api.connection.get();
     const connection = api.connection.get();
     if ([ANONYMOUS, EXTERNAL].includes(api.settings.get("authentication"))) {
     if ([ANONYMOUS, EXTERNAL].includes(api.settings.get("authentication"))) {
-        if (!_converse.jid) {
+        if (!jid) {
             throw new Error("Config Error: when using anonymous login " +
             throw new Error("Config Error: when using anonymous login " +
                 "you need to provide the server's domain via the 'jid' option. " +
                 "you need to provide the server's domain via the 'jid' option. " +
                 "Either when calling converse.initialize, or when calling " +
                 "Either when calling converse.initialize, or when calling " +
@@ -448,7 +452,7 @@ async function connect (credentials) {
         if (!connection.reconnecting) {
         if (!connection.reconnecting) {
             connection.reset();
             connection.reset();
         }
         }
-        connection.connect(_converse.jid.toLowerCase());
+        connection.connect(jid.toLowerCase());
     } else if (api.settings.get("authentication") === LOGIN) {
     } else if (api.settings.get("authentication") === LOGIN) {
         const password = credentials?.password ?? (connection?.pass || api.settings.get("password"));
         const password = credentials?.password ?? (connection?.pass || api.settings.get("password"));
         if (!password) {
         if (!password) {
@@ -466,24 +470,25 @@ async function connect (credentials) {
         }
         }
 
 
         let callback;
         let callback;
-
         // Save the SCRAM data if we're not already logged in with SCRAM
         // Save the SCRAM data if we're not already logged in with SCRAM
         if (
         if (
-            _converse.config.get('trusted') &&
-            _converse.jid &&
+            _converse.state.config.get('trusted') &&
+            jid &&
             api.settings.get("reuse_scram_keys") &&
             api.settings.get("reuse_scram_keys") &&
             !password?.ck
             !password?.ck
         ) {
         ) {
             // Store scram keys in scram storage
             // Store scram keys in scram storage
-            const login_info = await savedLoginInfo(_converse.jid);
-
-            callback = (status) => {
-                const { scram_keys } = connection;
-                if (scram_keys) login_info.save({ scram_keys });
-                connection.onConnectStatusChanged(status);
-            };
+            const login_info = await savedLoginInfo(jid);
+
+            callback =
+                /** @param {string} status */
+                (status) => {
+                    const { scram_keys } = connection;
+                    if (scram_keys) login_info.save({ scram_keys });
+                    connection.onConnectStatusChanged(status);
+                };
         }
         }
 
 
-        connection.connect(_converse.jid, password, callback);
+        connection.connect(jid, password, callback);
     }
     }
 }
 }

+ 3 - 0
src/headless/utils/jid.js

@@ -25,6 +25,9 @@ export function isSameDomain (jid1, jid2) {
     return Strophe.getDomainFromJid(jid1).toLowerCase() === Strophe.getDomainFromJid(jid2).toLowerCase();
     return Strophe.getDomainFromJid(jid1).toLowerCase() === Strophe.getDomainFromJid(jid2).toLowerCase();
 }
 }
 
 
+/**
+ * @param {string} jid
+ */
 export function getJIDFromURI (jid) {
 export function getJIDFromURI (jid) {
     return jid.startsWith('xmpp:') && jid.endsWith('?join')
     return jid.startsWith('xmpp:') && jid.endsWith('?join')
         ? jid.replace(/^xmpp:/, '').replace(/\?join$/, '')
         ? jid.replace(/^xmpp:/, '').replace(/\?join$/, '')

+ 5 - 12
src/headless/utils/session.js

@@ -18,18 +18,17 @@ export function isTestEnv () {
     return getInitSettings()['bosh_service_url'] === 'montague.lit/http-bind';
     return getInitSettings()['bosh_service_url'] === 'montague.lit/http-bind';
 }
 }
 
 
-export function setUnloadEvent () {
+export function getUnloadEvent () {
     if ('onpagehide' in window) {
     if ('onpagehide' in window) {
         // Pagehide gets thrown in more cases than unload. Specifically it
         // Pagehide gets thrown in more cases than unload. Specifically it
         // gets thrown when the page is cached and not just
         // gets thrown when the page is cached and not just
         // closed/destroyed. It's the only viable event on mobile Safari.
         // closed/destroyed. It's the only viable event on mobile Safari.
         // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
         // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
-        _converse.unloadevent = 'pagehide';
+        return 'pagehide';
     } else if ('onbeforeunload' in window) {
     } else if ('onbeforeunload' in window) {
-        _converse.unloadevent = 'beforeunload';
-    } else if ('onunload' in window) {
-        _converse.unloadevent = 'unload';
+        return 'beforeunload';
     }
     }
+    return 'unload';
 }
 }
 
 
 export function replacePromise (name) {
 export function replacePromise (name) {
@@ -48,7 +47,7 @@ export function replacePromise (name) {
 
 
 export function shouldClearCache () {
 export function shouldClearCache () {
     const { api } = _converse;
     const { api } = _converse;
-    return !_converse.config.get('trusted') ||
+    return !_converse.state.config.get('trusted') ||
         api.settings.get('clear_cache_on_logout') ||
         api.settings.get('clear_cache_on_logout') ||
         isTestEnv();
         isTestEnv();
 }
 }
@@ -57,12 +56,6 @@ export function shouldClearCache () {
 export async function tearDown () {
 export async function tearDown () {
     const { api } = _converse;
     const { api } = _converse;
     await api.trigger('beforeTearDown', {'synchronous': true});
     await api.trigger('beforeTearDown', {'synchronous': true});
-    window.removeEventListener('click', _converse.onUserActivity);
-    window.removeEventListener('focus', _converse.onUserActivity);
-    window.removeEventListener('keypress', _converse.onUserActivity);
-    window.removeEventListener('mousemove', _converse.onUserActivity);
-    window.removeEventListener(_converse.unloadevent, _converse.onUserActivity);
-    window.clearInterval(_converse.everySecondTrigger);
     api.trigger('afterTearDown');
     api.trigger('afterTearDown');
     return _converse;
     return _converse;
 }
 }

+ 5 - 3
src/headless/utils/storage.js

@@ -1,9 +1,10 @@
 import Storage from '@converse/skeletor/src/storage.js';
 import Storage from '@converse/skeletor/src/storage.js';
 import _converse from '../shared/_converse.js';
 import _converse from '../shared/_converse.js';
 import { settings_api } from '../shared/settings/api.js';
 import { settings_api } from '../shared/settings/api.js';
+import { getUnloadEvent } from './session.js';
 
 
 export function getDefaultStore () {
 export function getDefaultStore () {
-    if (_converse.config.get('trusted')) {
+    if (_converse.state.config.get('trusted')) {
         const is_non_persistent = settings_api.get('persistent_store') === 'sessionStorage';
         const is_non_persistent = settings_api.get('persistent_store') === 'sessionStorage';
         return is_non_persistent ? 'session': 'persistent';
         return is_non_persistent ? 'session': 'persistent';
     } else {
     } else {
@@ -29,8 +30,9 @@ export function initStorage (model, id, type) {
     model.browserStorage = createStore(id, store);
     model.browserStorage = createStore(id, store);
     if (storeUsesIndexedDB(store)) {
     if (storeUsesIndexedDB(store)) {
         const flush = () => model.browserStorage.flush();
         const flush = () => model.browserStorage.flush();
-        window.addEventListener(_converse.unloadevent, flush);
-        model.on('destroy', () => window.removeEventListener(_converse.unloadevent, flush));
+        const unloadevent = getUnloadEvent();
+        window.addEventListener(unloadevent, flush);
+        model.on('destroy', () => window.removeEventListener(unloadevent, flush));
         model.listenTo(_converse, 'beforeLogout', flush);
         model.listenTo(_converse, 'beforeLogout', flush);
     }
     }
 }
 }

+ 5 - 2
src/headless/utils/url.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {module:headless-shared-chat-utils.MediaURLData} MediaURLData
+ */
 import URI from 'urijs';
 import URI from 'urijs';
 import log from '../log.js';
 import log from '../log.js';
 import api from '../shared/api/index.js';
 import api from '../shared/api/index.js';
@@ -107,9 +110,9 @@ export function isDomainAllowed (url, setting) {
 }
 }
 
 
 /**
 /**
- * Accepts a {@link MediaURL} object and then checks whether its domain is
+ * Accepts a {@link MediaURLData} object and then checks whether its domain is
  * allowed for rendering in the chat.
  * allowed for rendering in the chat.
- * @param {MediaURL} o
+ * @param {MediaURLData} o
  * @returns {boolean}
  * @returns {boolean}
  */
  */
 export function isMediaURLDomainAllowed (o) {
 export function isMediaURLDomainAllowed (o) {

+ 16 - 7
src/plugins/adhoc-views/adhoc-commands.js

@@ -32,15 +32,24 @@ export default class AdHocCommands extends CustomElement {
         return tplAdhoc(this)
         return tplAdhoc(this)
     }
     }
 
 
+    /**
+     * @param {SubmitEvent} ev
+     */
     async fetchCommands (ev) {
     async fetchCommands (ev) {
         ev.preventDefault();
         ev.preventDefault();
-        delete this.alert_type;
-        delete this.alert;
+
+        if (!(ev.target instanceof HTMLFormElement)) {
+            this.alert_type = 'danger';
+            this.alert = 'Form could not be submitted';
+            return;
+        }
 
 
         this.fetching = true;
         this.fetching = true;
+        delete this.alert_type;
+        delete this.alert;
 
 
         const form_data = new FormData(ev.target);
         const form_data = new FormData(ev.target);
-        const jid = form_data.get('jid').trim();
+        const jid = /** @type {string} */(form_data.get('jid')).trim();
         let supported;
         let supported;
         try {
         try {
             supported = await api.disco.supports(Strophe.NS.ADHOC, jid);
             supported = await api.disco.supports(Strophe.NS.ADHOC, jid);
@@ -109,8 +118,8 @@ export default class AdHocCommands extends CustomElement {
 
 
     async runCommand (form, action) {
     async runCommand (form, action) {
         const form_data = new FormData(form);
         const form_data = new FormData(form);
-        const jid = form_data.get('command_jid').trim();
-        const node = form_data.get('command_node').trim();
+        const jid = /** @type {string} */(form_data.get('command_jid')).trim();
+        const node = /** @type {string} */(form_data.get('command_node')).trim();
 
 
         const cmd = this.commands.filter(c => c.node === node)[0];
         const cmd = this.commands.filter(c => c.node === node)[0];
         delete cmd.alert;
         delete cmd.alert;
@@ -159,8 +168,8 @@ export default class AdHocCommands extends CustomElement {
         this.requestUpdate();
         this.requestUpdate();
 
 
         const form_data = new FormData(ev.target.form);
         const form_data = new FormData(ev.target.form);
-        const jid = form_data.get('command_jid').trim();
-        const node = form_data.get('command_node').trim();
+        const jid = /** @type {string} */(form_data.get('command_jid')).trim();
+        const node = /** @type {string} */(form_data.get('command_node')).trim();
 
 
         const cmd = this.commands.filter(c => c.node === node)[0];
         const cmd = this.commands.filter(c => c.node === node)[0];
         delete cmd.alert;
         delete cmd.alert;

+ 5 - 0
src/plugins/bookmark-views/components/bookmark-form.js

@@ -5,6 +5,11 @@ import { _converse, api } from "@converse/headless";
 
 
 class MUCBookmarkForm extends CustomElement {
 class MUCBookmarkForm extends CustomElement {
 
 
+    constructor () {
+        super();
+        this.jid = null;
+    }
+
     static get properties () {
     static get properties () {
         return {
         return {
             'jid': { type: String }
             'jid': { type: String }

+ 5 - 0
src/plugins/bookmark-views/modals/bookmark-form.js

@@ -6,6 +6,11 @@ import { api } from "@converse/headless";
 
 
 export default class BookmarkFormModal extends BaseModal {
 export default class BookmarkFormModal extends BaseModal {
 
 
+    constructor (options) {
+        super(options);
+        this.jid = null;
+    }
+
     renderModal () {
     renderModal () {
         return html`
         return html`
             <converse-muc-bookmark-form class="muc-form-container" jid="${this.jid}">
             <converse-muc-bookmark-form class="muc-form-container" jid="${this.jid}">

+ 3 - 1
src/plugins/chatboxviews/index.js

@@ -24,7 +24,9 @@ converse.plugins.add('converse-chatboxviews', {
         // configuration settings.
         // configuration settings.
         api.settings.extend({ 'animate': true });
         api.settings.extend({ 'animate': true });
 
 
-        _converse.chatboxviews = new ChatBoxViews();
+        const chatboxviews = new ChatBoxViews();
+        Object.assign(_converse, { chatboxviews }); // XXX DEPRECATED
+        Object.assign(_converse.state, { chatboxviews });
 
 
         /************************ BEGIN Event Handlers ************************/
         /************************ BEGIN Event Handlers ************************/
         api.listen.on('chatBoxesInitialized', () => {
         api.listen.on('chatBoxesInitialized', () => {

+ 9 - 13
src/plugins/chatview/bottom-panel.js

@@ -1,3 +1,8 @@
+/**
+ * @typedef {import('shared/chat/emoji-picker.js').default} EmojiPicker
+ * @typedef {import('shared/chat/emoji-dropdown.js').default} EmojiDropdown
+ * @typedef {import('./message-form.js').default} MessageForm
+ */
 import './message-form.js';
 import './message-form.js';
 import tplBottomPanel from './templates/bottom-panel.js';
 import tplBottomPanel from './templates/bottom-panel.js';
 import { CustomElement } from 'shared/components/element.js';
 import { CustomElement } from 'shared/components/element.js';
@@ -38,7 +43,8 @@ export default class ChatBottomPanel extends CustomElement {
 
 
     sendButtonClicked (ev) {
     sendButtonClicked (ev) {
         if (ev.delegateTarget?.dataset.action === 'sendMessage') {
         if (ev.delegateTarget?.dataset.action === 'sendMessage') {
-            this.querySelector('converse-message-form')?.onFormSubmitted(ev);
+            const form = /** @type {MessageForm} */(this.querySelector('converse-message-form'));
+            form?.onFormSubmitted(ev);
         }
         }
     }
     }
 
 
@@ -55,16 +61,6 @@ export default class ChatBottomPanel extends CustomElement {
         _converse.chatboxviews.get(this.getAttribute('jid'))?.emitBlurred(ev);
         _converse.chatboxviews.get(this.getAttribute('jid'))?.emitBlurred(ev);
     }
     }
 
 
-    onDrop (evt) {
-        if (evt.dataTransfer.files.length == 0) {
-            // There are no files to be dropped, so this isn’t a file
-            // transfer operation.
-            return;
-        }
-        evt.preventDefault();
-        this.model.sendFiles(evt.dataTransfer.files);
-    }
-
     onDragOver (ev) { // eslint-disable-line class-methods-use-this
     onDragOver (ev) { // eslint-disable-line class-methods-use-this
         ev.preventDefault();
         ev.preventDefault();
     }
     }
@@ -76,14 +72,14 @@ export default class ChatBottomPanel extends CustomElement {
 
 
     async autocompleteInPicker (input, value) {
     async autocompleteInPicker (input, value) {
         await api.emojis.initialize();
         await api.emojis.initialize();
-        const emoji_picker = this.querySelector('converse-emoji-picker');
+        const emoji_picker = /** @type {EmojiPicker} */(this.querySelector('converse-emoji-picker'));
         if (emoji_picker) {
         if (emoji_picker) {
             emoji_picker.model.set({
             emoji_picker.model.set({
                 'ac_position': input.selectionStart,
                 'ac_position': input.selectionStart,
                 'autocompleting': value,
                 'autocompleting': value,
                 'query': value
                 'query': value
             });
             });
-            const emoji_dropdown = this.querySelector('converse-emoji-dropdown');
+            const emoji_dropdown = /** @type {EmojiDropdown} */(this.querySelector('converse-emoji-dropdown'));
             emoji_dropdown?.showMenu();
             emoji_dropdown?.showMenu();
         }
         }
     }
     }

+ 20 - 13
src/plugins/chatview/heading.js

@@ -1,3 +1,17 @@
+/**
+ * @typedef { Object } HeadingButtonAttributes
+ * An object representing a chat heading button
+ * @property { Boolean } standalone
+ *  True if shown on its own, false if it must be in the dropdown menu.
+ * @property { Function } handler
+ *  A handler function to be called when the button is clicked.
+ * @property { String } a_class - HTML classes to show on the button
+ * @property { String } i18n_text - The user-visiible name of the button
+ * @property { String } i18n_title - The tooltip text for this button
+ * @property { String } icon_class - What kind of CSS class to use for the icon
+ * @property { String } name - The internal name of the button
+ */
+
 import 'shared/modals/user-details.js';
 import 'shared/modals/user-details.js';
 import tplChatboxHead from './templates/chat-head.js';
 import tplChatboxHead from './templates/chat-head.js';
 import { CustomElement } from 'shared/components/element.js';
 import { CustomElement } from 'shared/components/element.js';
@@ -9,6 +23,11 @@ import './styles/chat-head.scss';
 
 
 export default class ChatHeading extends CustomElement {
 export default class ChatHeading extends CustomElement {
 
 
+    constructor () {
+        super();
+        this.jid = null;
+    }
+
     static get properties () {
     static get properties () {
         return {
         return {
             'jid': { type: String },
             'jid': { type: String },
@@ -54,19 +73,7 @@ export default class ChatHeading extends CustomElement {
      */
      */
     getHeadingButtons () {
     getHeadingButtons () {
         const buttons = [
         const buttons = [
-            /**
-             * @typedef { Object } HeadingButtonAttributes
-             * An object representing a chat heading button
-             * @property { Boolean } standalone
-             *  True if shown on its own, false if it must be in the dropdown menu.
-             * @property { Function } handler
-             *  A handler function to be called when the button is clicked.
-             * @property { String } a_class - HTML classes to show on the button
-             * @property { String } i18n_text - The user-visiible name of the button
-             * @property { String } i18n_title - The tooltip text for this button
-             * @property { String } icon_class - What kind of CSS class to use for the icon
-             * @property { String } name - The internal name of the button
-             */
+            /** @type {HeadingButtonAttributes} */
             {
             {
                 'a_class': 'show-user-details-modal',
                 'a_class': 'show-user-details-modal',
                 'handler': ev => this.showUserDetailsModal(ev),
                 'handler': ev => this.showUserDetailsModal(ev),

+ 23 - 11
src/plugins/chatview/message-form.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('shared/chat/emoji-dropdown.js').default} EmojiDropdown
+ */
 import tplMessageForm from './templates/message-form.js';
 import tplMessageForm from './templates/message-form.js';
 import { CustomElement } from 'shared/components/element.js';
 import { CustomElement } from 'shared/components/element.js';
 import { __ } from 'i18n';
 import { __ } from 'i18n';
@@ -16,7 +19,7 @@ export default class MessageForm extends CustomElement {
         this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
         this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
         this.listenTo(this.model, 'change:composing_spoiler', () => this.requestUpdate());
         this.listenTo(this.model, 'change:composing_spoiler', () => this.requestUpdate());
 
 
-        this.handleEmojiSelection = ({ detail }) => {
+        this.handleEmojiSelection = (/** @type { CustomEvent } */{ detail }) => {
             if (this.model.get('jid') === detail.jid) {
             if (this.model.get('jid') === detail.jid) {
                 this.insertIntoTextArea(detail.value, detail.autocompleting, false, detail.ac_position);
                 this.insertIntoTextArea(detail.value, detail.autocompleting, false, detail.ac_position);
             }
             }
@@ -34,13 +37,12 @@ export default class MessageForm extends CustomElement {
         return tplMessageForm(
         return tplMessageForm(
             Object.assign(this.model.toJSON(), {
             Object.assign(this.model.toJSON(), {
                 'onDrop': ev => this.onDrop(ev),
                 'onDrop': ev => this.onDrop(ev),
-                'hint_value': this.querySelector('.spoiler-hint')?.value,
-                'message_value': this.querySelector('.chat-textarea')?.value,
+                'hint_value': /** @type {HTMLInputElement} */(this.querySelector('.spoiler-hint'))?.value,
+                'message_value': /** @type {HTMLTextAreaElement} */(this.querySelector('.chat-textarea'))?.value,
                 'onChange': ev => this.model.set({'draft': ev.target.value}),
                 'onChange': ev => this.model.set({'draft': ev.target.value}),
                 'onKeyDown': ev => this.onKeyDown(ev),
                 'onKeyDown': ev => this.onKeyDown(ev),
                 'onKeyUp': ev => this.onKeyUp(ev),
                 'onKeyUp': ev => this.onKeyUp(ev),
-                'onPaste': ev => this.onPaste(ev),
-                'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
+                'onPaste': ev => this.onPaste(ev)
             })
             })
         );
         );
     }
     }
@@ -56,7 +58,7 @@ export default class MessageForm extends CustomElement {
      *  replaced with the new value.
      *  replaced with the new value.
      */
      */
     insertIntoTextArea (value, replace = false, correcting = false, position) {
     insertIntoTextArea (value, replace = false, correcting = false, position) {
-        const textarea = this.querySelector('.chat-textarea');
+        const textarea = /** @type {HTMLTextAreaElement} */(this.querySelector('.chat-textarea'));
         if (correcting) {
         if (correcting) {
             u.addClass('correcting', textarea);
             u.addClass('correcting', textarea);
         } else {
         } else {
@@ -120,6 +122,16 @@ export default class MessageForm extends CustomElement {
         this.model.set({'draft': ev.clipboardData.getData('text/plain')});
         this.model.set({'draft': ev.clipboardData.getData('text/plain')});
     }
     }
 
 
+    onDrop (evt) {
+        if (evt.dataTransfer.files.length == 0) {
+            // There are no files to be dropped, so this isn’t a file
+            // transfer operation.
+            return;
+        }
+        evt.preventDefault();
+        this.model.sendFiles(evt.dataTransfer.files);
+    }
+
     onKeyUp (ev) {
     onKeyUp (ev) {
         this.model.set({'draft': ev.target.value});
         this.model.set({'draft': ev.target.value});
     }
     }
@@ -141,11 +153,11 @@ export default class MessageForm extends CustomElement {
                 // Forward slash is used to run commands. Nothing to do here.
                 // Forward slash is used to run commands. Nothing to do here.
                 return;
                 return;
             } else if (ev.keyCode === converse.keycodes.ESCAPE) {
             } else if (ev.keyCode === converse.keycodes.ESCAPE) {
-                return this.onEscapePressed(ev, this);
+                return this.onEscapePressed(ev);
             } else if (ev.keyCode === converse.keycodes.ENTER) {
             } else if (ev.keyCode === converse.keycodes.ENTER) {
                 return this.onFormSubmitted(ev);
                 return this.onFormSubmitted(ev);
             } else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
             } else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
-                const textarea = this.querySelector('.chat-textarea');
+                const textarea = /** @type {HTMLTextAreaElement} */(this.querySelector('.chat-textarea'));
                 if (!textarea.value || u.hasClass('correcting', textarea)) {
                 if (!textarea.value || u.hasClass('correcting', textarea)) {
                     return this.model.editEarlierMessage();
                     return this.model.editEarlierMessage();
                 }
                 }
@@ -178,7 +190,7 @@ export default class MessageForm extends CustomElement {
     async onFormSubmitted (ev) {
     async onFormSubmitted (ev) {
         ev?.preventDefault?.();
         ev?.preventDefault?.();
 
 
-        const textarea = this.querySelector('.chat-textarea');
+        const textarea = /** @type {HTMLTextAreaElement} */(this.querySelector('.chat-textarea'));
         const message_text = textarea.value.trim();
         const message_text = textarea.value.trim();
         if (
         if (
             (api.settings.get('message_limit') && message_text.length > api.settings.get('message_limit')) ||
             (api.settings.get('message_limit') && message_text.length > api.settings.get('message_limit')) ||
@@ -195,12 +207,12 @@ export default class MessageForm extends CustomElement {
         let spoiler_hint,
         let spoiler_hint,
             hint_el = {};
             hint_el = {};
         if (this.model.get('composing_spoiler')) {
         if (this.model.get('composing_spoiler')) {
-            hint_el = this.querySelector('form.sendXMPPMessage input.spoiler-hint');
+            hint_el = /** @type {HTMLInputElement} */(this.querySelector('form.sendXMPPMessage input.spoiler-hint'));
             spoiler_hint = hint_el.value;
             spoiler_hint = hint_el.value;
         }
         }
         u.addClass('disabled', textarea);
         u.addClass('disabled', textarea);
         textarea.setAttribute('disabled', 'disabled');
         textarea.setAttribute('disabled', 'disabled');
-        this.querySelector('converse-emoji-dropdown')?.hideMenu();
+        /** @type {EmojiDropdown} */(this.querySelector('converse-emoji-dropdown'))?.hideMenu();
 
 
         const is_command = await parseMessageForCommands(this.model, message_text);
         const is_command = await parseMessageForCommands(this.model, message_text);
         const message = is_command ? null : await this.model.sendMessage({'body': message_text, spoiler_hint});
         const message = is_command ? null : await this.model.sendMessage({'body': message_text, spoiler_hint});

+ 1 - 1
src/plugins/chatview/tests/chatbox.js

@@ -248,7 +248,7 @@ describe("Chatboxes", function () {
 
 
         describe("A chat toolbar", function () {
         describe("A chat toolbar", function () {
 
 
-            it("shows the remaining character count if a message_limit is configured",
+            fit("shows the remaining character count if a message_limit is configured",
                     mock.initConverse(['chatBoxesFetched'], {'message_limit': 200}, async function (_converse) {
                     mock.initConverse(['chatBoxesFetched'], {'message_limit': 200}, async function (_converse) {
 
 
                 await mock.waitForRoster(_converse, 'current', 3);
                 await mock.waitForRoster(_converse, 'current', 3);

+ 2 - 2
src/plugins/chatview/tests/receipts.js

@@ -130,7 +130,7 @@ describe("A delivery receipt", function () {
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 1);
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 1);
 
 
         // Also handle receipts with type 'chat'. See #1353
         // Also handle receipts with type 'chat'. See #1353
-        spyOn(_converse, 'handleMessageStanza').and.callThrough();
+        spyOn(_converse.exports, 'handleMessageStanza').and.callThrough();
         textarea.value = 'Another message';
         textarea.value = 'Another message';
         message_form.onKeyDown({
         message_form.onKeyDown({
             target: textarea,
             target: textarea,
@@ -149,6 +149,6 @@ describe("A delivery receipt", function () {
             }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
             }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
         api.connection.get()._dataRecv(mock.createRequest(msg));
         api.connection.get()._dataRecv(mock.createRequest(msg));
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 2);
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 2);
-        expect(_converse.handleMessageStanza.calls.count()).toBe(1);
+        expect(_converse.exports.handleMessageStanza.calls.count()).toBe(1);
     }));
     }));
 });
 });

+ 8 - 3
src/plugins/controlbox/api.js

@@ -1,3 +1,6 @@
+/**
+ * @typedef {import('./controlbox.js').default} ControlBox
+ */
 import { _converse, api, converse } from "@converse/headless";
 import { _converse, api, converse } from "@converse/headless";
 
 
 const { u } = converse.env;
 const { u } = converse.env;
@@ -18,8 +21,10 @@ export default {
          */
          */
         async open () {
         async open () {
             await api.waitUntil('chatBoxesFetched');
             await api.waitUntil('chatBoxesFetched');
-            const model = await api.chatboxes.get('controlbox') ||
-              api.chatboxes.create('controlbox', {}, _converse.Controlbox);
+            let model = await api.chatboxes.get('controlbox');
+            if (!model) {
+              model = await api.chatboxes.create('controlbox', {}, _converse.ControlBox);
+            }
             u.safeSave(model, {'closed': false});
             u.safeSave(model, {'closed': false});
             return model;
             return model;
         },
         },
@@ -27,7 +32,7 @@ export default {
         /**
         /**
          * Returns the controlbox view.
          * Returns the controlbox view.
          * @method _converse.api.controlbox.get
          * @method _converse.api.controlbox.get
-         * @returns { View } View representing the controlbox
+         * @returns {ControlBox} View representing the controlbox
          * @example const view = _converse.api.controlbox.get();
          * @example const view = _converse.api.controlbox.get();
          */
          */
         get () {
         get () {

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

@@ -34,8 +34,8 @@ class ControlBox extends CustomElement {
     }
     }
 
 
     setModel () {
     setModel () {
-        this.model = _converse.chatboxes.get('controlbox');
-        this.listenTo(_converse.connfeedback, 'change:connection_status', () => this.requestUpdate());
+        this.model = _converse.state.chatboxes.get('controlbox');
+        this.listenTo(_converse.state.connfeedback, 'change:connection_status', () => this.requestUpdate());
         this.listenTo(this.model, 'change:active-form', () => this.requestUpdate());
         this.listenTo(this.model, 'change:active-form', () => this.requestUpdate());
         this.listenTo(this.model, 'change:connected', () => this.requestUpdate());
         this.listenTo(this.model, 'change:connected', () => this.requestUpdate());
         this.listenTo(this.model, 'change:closed', () => !this.model.get('closed') && this.afterShown());
         this.listenTo(this.model, 'change:closed', () => !this.model.get('closed') && this.afterShown());

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

@@ -5,13 +5,13 @@ import { CustomElement } from 'shared/components/element.js';
 import { _converse, api, converse } from '@converse/headless';
 import { _converse, api, converse } from '@converse/headless';
 import { updateSettingsWithFormData, validateJID } from './utils.js';
 import { updateSettingsWithFormData, validateJID } from './utils.js';
 
 
-const { Strophe, u } = converse.env;
+const { Strophe } = converse.env;
 
 
 
 
 class LoginForm extends CustomElement {
 class LoginForm extends CustomElement {
 
 
     initialize () {
     initialize () {
-        this.listenTo(_converse.connfeedback, 'change', () => this.requestUpdate());
+        this.listenTo(_converse.state.connfeedback, 'change', () => this.requestUpdate());
         this.handler = () => this.requestUpdate()
         this.handler = () => this.requestUpdate()
     }
     }
 
 

+ 5 - 0
src/plugins/controlbox/navback.js

@@ -5,6 +5,11 @@ import { api } from "@converse/headless";
 
 
 class ControlBoxNavback extends CustomElement {
 class ControlBoxNavback extends CustomElement {
 
 
+    constructor () {
+        super();
+        this.jid = null;
+    }
+
     static get properties () {
     static get properties () {
         return {
         return {
             'jid': { type: String }
             'jid': { type: String }

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

@@ -6,7 +6,7 @@ const { Strophe } = converse.env;
 
 
 
 
 function whenNotConnected (o) {
 function whenNotConnected (o) {
-    const connection_status = _converse.connfeedback.get('connection_status');
+    const connection_status = _converse.state.connfeedback.get('connection_status');
     if ([Strophe.Status.RECONNECTING, Strophe.Status.CONNECTING].includes(connection_status)) {
     if ([Strophe.Status.RECONNECTING, Strophe.Status.CONNECTING].includes(connection_status)) {
         return tplSpinner();
         return tplSpinner();
     }
     }

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

@@ -143,13 +143,14 @@ const form_fields = (el) => {
 };
 };
 
 
 export default (el) => {
 export default (el) => {
-    const connection_status = _converse.connfeedback.get('connection_status');
+    const { connfeedback } = _converse.state;
+    const connection_status = connfeedback.get('connection_status');
     let feedback_class, pretty_status;
     let feedback_class, pretty_status;
     if (REPORTABLE_STATUSES.includes(connection_status)) {
     if (REPORTABLE_STATUSES.includes(connection_status)) {
         pretty_status = PRETTY_CONNECTION_STATUS[connection_status];
         pretty_status = PRETTY_CONNECTION_STATUS[connection_status];
         feedback_class = CONNECTION_STATUS_CSS_CLASS[connection_status];
         feedback_class = CONNECTION_STATUS_CSS_CLASS[connection_status];
     }
     }
-    const conn_feedback_message = _converse.connfeedback.get('message');
+    const conn_feedback_message = connfeedback.get('message');
     return html` <converse-brand-heading></converse-brand-heading>
     return html` <converse-brand-heading></converse-brand-heading>
         <form id="converse-login" class="converse-form" method="post" @submit=${el.onLoginFormSubmitted}>
         <form id="converse-login" class="converse-form" method="post" @submit=${el.onLoginFormSubmitted}>
             <div class="conn-feedback fade-in ${!pretty_status ? 'hidden' : feedback_class}">
             <div class="conn-feedback fade-in ${!pretty_status ? 'hidden' : feedback_class}">

+ 8 - 6
src/plugins/controlbox/utils.js

@@ -52,21 +52,21 @@ export function onChatBoxesFetched () {
 /**
 /**
  * Given the login `<form>` element, parse its data and update the
  * Given the login `<form>` element, parse its data and update the
  * converse settings with the supplied JID, password and connection URL.
  * converse settings with the supplied JID, password and connection URL.
- * @param { HTMLElement } form
- * @param { Object } settings - Extra settings that may be passed in and will
+ * @param {HTMLFormElement} form
+ * @param {Object} settings - Extra settings that may be passed in and will
  *  also be set together with the form settings.
  *  also be set together with the form settings.
  */
  */
 export function updateSettingsWithFormData (form, settings={}) {
 export function updateSettingsWithFormData (form, settings={}) {
     const form_data = new FormData(form);
     const form_data = new FormData(form);
 
 
-    const connection_url  = form_data.get('connection-url');
+    const connection_url  = /** @type {string} */(form_data.get('connection-url'));
     if (connection_url?.startsWith('ws')) {
     if (connection_url?.startsWith('ws')) {
         settings['websocket_url'] = connection_url;
         settings['websocket_url'] = connection_url;
     } else if (connection_url?.startsWith('http')) {
     } else if (connection_url?.startsWith('http')) {
         settings['bosh_service_url'] = connection_url;
         settings['bosh_service_url'] = connection_url;
     }
     }
 
 
-    let jid = form_data.get('jid');
+    let jid = /** @type {string} */(form_data.get('jid'));
     if (api.settings.get('locked_domain')) {
     if (api.settings.get('locked_domain')) {
         const last_part = '@' + api.settings.get('locked_domain');
         const last_part = '@' + api.settings.get('locked_domain');
         if (jid.endsWith(last_part)) {
         if (jid.endsWith(last_part)) {
@@ -85,8 +85,11 @@ export function updateSettingsWithFormData (form, settings={}) {
 }
 }
 
 
 
 
+/**
+ * @param {HTMLFormElement} form
+ */
 export function validateJID (form) {
 export function validateJID (form) {
-    const jid_element = form.querySelector('input[name=jid]');
+    const jid_element = /** @type {HTMLInputElement} */(form.querySelector('input[name=jid]'));
     if (
     if (
         jid_element.value &&
         jid_element.value &&
         !api.settings.get('locked_domain') &&
         !api.settings.get('locked_domain') &&
@@ -99,4 +102,3 @@ export function validateJID (form) {
     jid_element.setCustomValidity('');
     jid_element.setCustomValidity('');
     return true;
     return true;
 }
 }
-

+ 5 - 5
src/plugins/dragresize/index.js

@@ -68,25 +68,25 @@ converse.plugins.add('converse-dragresize', {
         }
         }
 
 
         /**
         /**
-         * This function registers mousedown and mouseup events hadlers to 
+         * This function registers mousedown and mouseup events hadlers to
          * all iframes in the DOM when converse UI resizing events are called
          * all iframes in the DOM when converse UI resizing events are called
          * to prevent mouse drag stutter effect which is bad user experience.
          * to prevent mouse drag stutter effect which is bad user experience.
          * @function dragresizeOverIframeHandler
          * @function dragresizeOverIframeHandler
          * @param {Object} e - dragging node element.
          * @param {Object} e - dragging node element.
          */
          */
         function dragresizeOverIframeHandler (e) {
         function dragresizeOverIframeHandler (e) {
-          const iframes = document.getElementsByTagName('iframe');
-          for (let iframe of iframes) {
+          const iframes = Array.from(document.getElementsByTagName('iframe'));
+          for (const iframe of iframes) {
             e.addEventListener('mousedown', () => {
             e.addEventListener('mousedown', () => {
                 iframe.style.pointerEvents  = 'none';
                 iframe.style.pointerEvents  = 'none';
             }, { once: true });
             }, { once: true });
-            
+
             e.addEventListener('mouseup', () => {
             e.addEventListener('mouseup', () => {
                 iframe.style.pointerEvents  = 'initial';
                 iframe.style.pointerEvents  = 'initial';
             }, { once: true });
             }, { once: true });
           }
           }
         }
         }
-        
+
         api.listen.on('registeredGlobalEventHandlers', registerGlobalEventHandlers);
         api.listen.on('registeredGlobalEventHandlers', registerGlobalEventHandlers);
         api.listen.on('unregisteredGlobalEventHandlers', unregisterGlobalEventHandlers);
         api.listen.on('unregisteredGlobalEventHandlers', unregisterGlobalEventHandlers);
         api.listen.on('beforeShowingChatView', view => view.initDragResize().setDimensions());
         api.listen.on('beforeShowingChatView', view => view.initDragResize().setDimensions());

+ 5 - 0
src/plugins/headlines-view/heading.js

@@ -6,6 +6,11 @@ import { _converse, api } from "@converse/headless";
 
 
 export default class HeadlinesHeading extends CustomElement {
 export default class HeadlinesHeading extends CustomElement {
 
 
+    constructor () {
+        super();
+        this.jid = null;
+    }
+
     static get properties () {
     static get properties () {
         return {
         return {
             'jid': { type: String },
             'jid': { type: String },

+ 0 - 1
src/plugins/headlines-view/view.js

@@ -9,7 +9,6 @@ class HeadlinesFeedView extends BaseChatView {
         _converse.chatboxviews.add(this.jid, this);
         _converse.chatboxviews.add(this.jid, this);
 
 
         this.model = _converse.chatboxes.get(this.jid);
         this.model = _converse.chatboxes.get(this.jid);
-        this.model.disable_mam = true; // Don't do MAM queries for this box
         this.listenTo(this.model, 'change:hidden', () => this.afterShown());
         this.listenTo(this.model, 'change:hidden', () => this.afterShown());
         this.listenTo(this.model, 'destroy', this.remove);
         this.listenTo(this.model, 'destroy', this.remove);
         this.listenTo(this.model.messages, 'add', () => this.requestUpdate());
         this.listenTo(this.model.messages, 'add', () => this.requestUpdate());

+ 5 - 0
src/plugins/mam-views/placeholder.js

@@ -14,6 +14,11 @@ class Placeholder extends CustomElement {
         }
         }
     }
     }
 
 
+    constructor () {
+        super();
+        this.model = null;
+    }
+
     render () {
     render () {
         return tplPlaceholder(this);
         return tplPlaceholder(this);
     }
     }

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä