2
0
Эх сурвалжийг харах

Fix type errors. Some OMEMO fixes.

Move omemo_store into state obj
JC Brand 1 жил өмнө
parent
commit
fdf8c57d62
100 өөрчлөгдсөн 632 нэмэгдсэн , 471 устгасан
  1. 0 1
      src/headless/plugins/chat/model.js
  2. 1 16
      src/headless/plugins/emoji/index.js
  3. 16 0
      src/headless/plugins/emoji/picker.js
  4. 2 0
      src/headless/shared/_converse.js
  5. 1 0
      src/i18n/index.js
  6. 5 3
      src/plugins/bookmark-views/components/bookmark-form.js
  7. 2 2
      src/plugins/bookmark-views/components/bookmarks-list.js
  8. 1 1
      src/plugins/bookmark-views/components/templates/list.js
  9. 10 7
      src/plugins/bookmark-views/index.js
  10. 1 1
      src/plugins/bookmark-views/mixins.js
  11. 1 1
      src/plugins/bookmark-views/utils.js
  12. 1 0
      src/plugins/chatboxviews/container.js
  13. 7 5
      src/plugins/chatboxviews/index.js
  14. 1 1
      src/plugins/chatboxviews/templates/chats.js
  15. 1 1
      src/plugins/chatboxviews/view.js
  16. 4 2
      src/plugins/chatview/bottom-panel.js
  17. 7 5
      src/plugins/chatview/chat.js
  18. 4 2
      src/plugins/chatview/heading.js
  19. 6 1
      src/plugins/chatview/index.js
  20. 9 6
      src/plugins/chatview/message-form.js
  21. 13 13
      src/plugins/chatview/tests/chatbox.js
  22. 2 2
      src/plugins/chatview/tests/messages.js
  23. 2 1
      src/plugins/chatview/utils.js
  24. 4 6
      src/plugins/controlbox/api.js
  25. 9 7
      src/plugins/controlbox/controlbox.js
  26. 3 3
      src/plugins/controlbox/index.js
  27. 2 1
      src/plugins/controlbox/loginform.js
  28. 6 4
      src/plugins/controlbox/model.js
  29. 8 1
      src/plugins/controlbox/templates/controlbox.js
  30. 3 1
      src/plugins/controlbox/toggle.js
  31. 8 8
      src/plugins/controlbox/utils.js
  32. 6 6
      src/plugins/dragresize/index.js
  33. 5 4
      src/plugins/dragresize/mixin.js
  34. 32 22
      src/plugins/dragresize/utils.js
  35. 2 2
      src/plugins/headlines-view/feed-list.js
  36. 1 1
      src/plugins/headlines-view/heading.js
  37. 6 4
      src/plugins/headlines-view/index.js
  38. 5 2
      src/plugins/headlines-view/view.js
  39. 2 1
      src/plugins/mam-views/utils.js
  40. 15 6
      src/plugins/minimize/index.js
  41. 5 5
      src/plugins/minimize/tests/minchats.js
  42. 28 14
      src/plugins/minimize/utils.js
  43. 3 2
      src/plugins/minimize/view.js
  44. 2 1
      src/plugins/muc-views/bottom-panel.js
  45. 1 1
      src/plugins/muc-views/config-form.js
  46. 1 1
      src/plugins/muc-views/destroyed.js
  47. 1 1
      src/plugins/muc-views/disconnected.js
  48. 8 4
      src/plugins/muc-views/heading.js
  49. 6 1
      src/plugins/muc-views/index.js
  50. 7 5
      src/plugins/muc-views/message-form.js
  51. 1 1
      src/plugins/muc-views/modals/add-muc.js
  52. 1 1
      src/plugins/muc-views/modals/muc-invite.js
  53. 1 1
      src/plugins/muc-views/modals/occupant.js
  54. 3 1
      src/plugins/muc-views/modals/templates/occupant.js
  55. 2 1
      src/plugins/muc-views/nickname-form.js
  56. 2 1
      src/plugins/muc-views/password-form.js
  57. 8 1
      src/plugins/muc-views/search.js
  58. 6 4
      src/plugins/muc-views/sidebar.js
  59. 3 2
      src/plugins/muc-views/utils.js
  60. 4 3
      src/plugins/notifications/index.js
  61. 16 13
      src/plugins/notifications/utils.js
  62. 11 9
      src/plugins/omemo/api.js
  63. 2 1
      src/plugins/omemo/device.js
  64. 16 10
      src/plugins/omemo/devicelist.js
  65. 18 15
      src/plugins/omemo/index.js
  66. 0 22
      src/plugins/omemo/mixins/converse.js
  67. 2 1
      src/plugins/omemo/profile.js
  68. 3 2
      src/plugins/omemo/store.js
  69. 12 12
      src/plugins/omemo/tests/corrections.js
  70. 2 2
      src/plugins/omemo/tests/media-sharing.js
  71. 13 13
      src/plugins/omemo/tests/muc.js
  72. 47 45
      src/plugins/omemo/tests/omemo.js
  73. 53 27
      src/plugins/omemo/utils.js
  74. 1 3
      src/plugins/profile/modals/chat-status.js
  75. 8 2
      src/plugins/profile/modals/profile.js
  76. 3 2
      src/plugins/profile/password-reset.js
  77. 1 1
      src/plugins/profile/statusview.js
  78. 1 1
      src/plugins/push/index.js
  79. 12 4
      src/plugins/push/utils.js
  80. 12 11
      src/plugins/register/panel.js
  81. 3 1
      src/plugins/register/utils.js
  82. 6 2
      src/plugins/roomslist/model.js
  83. 1 1
      src/plugins/roomslist/templates/roomslist.js
  84. 12 9
      src/plugins/roomslist/view.js
  85. 1 1
      src/plugins/rosterview/contactview.js
  86. 5 6
      src/plugins/rosterview/index.js
  87. 2 2
      src/plugins/rosterview/modals/add-contact.js
  88. 4 3
      src/plugins/rosterview/modals/templates/add-contact.js
  89. 10 8
      src/plugins/rosterview/rosterview.js
  90. 2 2
      src/plugins/rosterview/templates/group.js
  91. 5 4
      src/plugins/rosterview/templates/roster.js
  92. 13 12
      src/plugins/rosterview/utils.js
  93. 3 4
      src/shared/autocomplete/index.js
  94. 2 2
      src/shared/chat/baseview.js
  95. 4 2
      src/shared/chat/emoji-dropdown.js
  96. 3 2
      src/shared/chat/emoji-picker-content.js
  97. 4 3
      src/shared/chat/emoji-picker.js
  98. 4 4
      src/shared/chat/message-actions.js
  99. 3 2
      src/shared/chat/message.js
  100. 5 4
      src/shared/chat/toolbar.js

+ 0 - 1
src/headless/plugins/chat/model.js

@@ -479,7 +479,6 @@ class ChatBox extends ModelWithContact {
      * Timeouts are set when the  state being set is COMPOSING or PAUSED.
      * After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
      * See XEP-0085 Chat State Notifications.
-     * @private
      * @method ChatBox#setChatState
      * @param { string } state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
      */

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

@@ -6,8 +6,8 @@
 import './utils.js';
 import _converse from '../../shared/_converse.js';
 import api, { converse } from '../../shared/api/index.js';
-import { Model } from '@converse/skeletor';
 import { getOpenPromise } from '@converse/openpromise';
+import EmojiPicker from './picker.js';
 
 
 converse.emojis = {
@@ -58,21 +58,6 @@ converse.plugins.add('converse-emoji', {
             }
         });
 
-        /**
-         * Model for storing data related to the Emoji picker widget
-         * @namespace _converse.EmojiPicker
-         * @memberOf _converse
-         */
-        class EmojiPicker extends Model {
-            defaults () { // eslint-disable-line class-methods-use-this
-                return {
-                    'current_category': 'smileys',
-                    'current_skintone': '',
-                    'scroll_position': 0
-                }
-            }
-        }
-
         const exports = { EmojiPicker };
         Object.assign(_converse, exports); // XXX: DEPRECATED
         Object.assign(_converse.exports, exports);

+ 16 - 0
src/headless/plugins/emoji/picker.js

@@ -0,0 +1,16 @@
+import { Model } from '@converse/skeletor';
+
+/**
+ * Model for storing data related to the Emoji picker widget
+ */
+class EmojiPicker extends Model {
+    defaults () {
+        return {
+            'current_category': 'smileys',
+            'current_skintone': '',
+            'scroll_position': 0
+        }
+    }
+}
+
+export default EmojiPicker;

+ 2 - 0
src/headless/shared/_converse.js

@@ -93,6 +93,8 @@ class ConversePrivateGlobal extends EventEmitter(Object) {
         this.DEFAULT_IMAGE_TYPE = DEFAULT_IMAGE_TYPE;
         this.DEFAULT_IMAGE = DEFAULT_IMAGE;
 
+        this.NUM_PREKEYS = 100; // DEPRECATED. Set here so that tests can override
+
         // Set as module attr so that we can override in tests.
         // TODO: replace with config settings
         this.TIMEOUTS =  {

+ 1 - 0
src/i18n/index.js

@@ -140,4 +140,5 @@ Object.assign(i18n, {
     },
 });
 
+export { i18n };
 export const __ = i18n.__;

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

@@ -17,9 +17,10 @@ class MUCBookmarkForm extends CustomElement {
     }
 
     willUpdate (changed_properties) {
+        const { chatboxes, bookmarks } = _converse.state;
         if (changed_properties.has('jid')) {
-            this.model = _converse.chatboxes.get(this.jid);
-            this.bookmark  = _converse.bookmarks.get(this.jid);
+            this.model = chatboxes.get(this.jid);
+            this.bookmark  = bookmarks.get(this.jid);
         }
     }
 
@@ -29,7 +30,8 @@ class MUCBookmarkForm extends CustomElement {
 
     onBookmarkFormSubmitted (ev) {
         ev.preventDefault();
-        _converse.bookmarks.createBookmark({
+        const { bookmarks } = _converse.state;
+        bookmarks.createBookmark({
             'jid': this.jid,
             'autojoin': ev.target.querySelector('input[name="autojoin"]')?.checked || false,
             'name': ev.target.querySelector('input[name=name]')?.value,

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

@@ -13,7 +13,7 @@ export default class BookmarksView extends CustomElement {
 
     async initialize () {
         await api.waitUntil('bookmarksInitialized');
-        const { bookmarks, chatboxes } = _converse;
+        const { bookmarks, chatboxes } = _converse.state;
 
         this.liveFilter = debounce((ev) => this.model.set({'text': ev.target.value}), 100);
 
@@ -37,7 +37,7 @@ export default class BookmarksView extends CustomElement {
     }
 
     render () {
-        return _converse.bookmarks && this.model ? tplBookmarksList(this) : tplSpinner();
+        return _converse.state.bookmarks && this.model ? tplBookmarksList(this) : tplSpinner();
     }
 
     clearFilter (ev) {

+ 1 - 1
src/plugins/bookmark-views/components/templates/list.js

@@ -8,7 +8,7 @@ const filterBookmark = (b, text) => b.get('name')?.includes(text) || b.get('jid'
 export default (el) => {
     const i18n_placeholder = __('Filter');
     const filter_text = el.model.get('text');
-    const { bookmarks } = _converse;
+    const { bookmarks } = _converse.state;
     const shown_bookmarks = filter_text ? bookmarks.filter(b => filterBookmark(b, filter_text)) : bookmarks;
 
     return html`

+ 10 - 7
src/plugins/bookmark-views/index.js

@@ -35,13 +35,16 @@ converse.plugins.add('converse-bookmark-views', {
             hide_open_bookmarks: true
         });
 
-        _converse.removeBookmarkViaEvent = removeBookmarkViaEvent;
-        _converse.addBookmarkViaEvent = addBookmarkViaEvent;
-
-        Object.assign(_converse.ChatRoomView.prototype, bookmarkableChatRoomView);
-
-        _converse.MUCBookmarkForm = BookmarkForm;
-        _converse.BookmarksView = BookmarksView;
+        const exports =  {
+            removeBookmarkViaEvent,
+            addBookmarkViaEvent,
+            MUCBookmarkForm: BookmarkForm,
+            BookmarksView,
+        }
+
+        Object.assign(_converse, exports); // DEPRECATED
+        Object.assign(_converse.exports, exports);
+        Object.assign(_converse.exports.ChatRoomView.prototype, bookmarkableChatRoomView);
 
         api.listen.on('getHeadingButtons', getHeadingButtons);
         api.listen.on('chatRoomViewInitialized', view => view.setBookmarkState());

+ 1 - 1
src/plugins/bookmark-views/mixins.js

@@ -21,7 +21,7 @@ export const bookmarkableChatRoomView = {
 
     renderBookmarkForm () {
         if (!this.bookmark_form) {
-            this.bookmark_form = new _converse.MUCBookmarkForm({
+            this.bookmark_form = new _converse.state.MUCBookmarkForm({
                 'model': this.model,
                 'chatroomview': this
             });

+ 1 - 1
src/plugins/bookmark-views/utils.js

@@ -28,7 +28,7 @@ export async function removeBookmarkViaEvent (ev) {
     const jid = ev.currentTarget.getAttribute('data-room-jid');
     const result = await api.confirm(__('Are you sure you want to remove the bookmark "%1$s"?', name));
     if (result) {
-        _converse.bookmarks.where({ jid }).forEach(b => b.destroy());
+        _converse.state.bookmarks.where({ jid }).forEach(b => b.destroy());
     }
 }
 

+ 1 - 0
src/plugins/chatboxviews/container.js

@@ -3,6 +3,7 @@ class ChatBoxViews {
 
     constructor () {
         this.views = {};
+        this.el = null;
     }
 
     add (key, val) {

+ 7 - 5
src/plugins/chatboxviews/index.js

@@ -30,11 +30,12 @@ converse.plugins.add('converse-chatboxviews', {
 
         /************************ BEGIN Event Handlers ************************/
         api.listen.on('chatBoxesInitialized', () => {
-            _converse.chatboxes.on('destroy', m => _converse.chatboxviews.remove(m.get('jid')));
+            _converse.state.chatboxes.on('destroy', (m) => chatboxviews.remove(m.get('jid')));
         });
 
-        api.listen.on('cleanup', () => delete _converse.chatboxviews);
-        api.listen.on('clearSession', () => _converse.chatboxviews.closeAllChatBoxes());
+        api.listen.on('cleanup', () => Object.assign(_converse, { chatboxviews: null })); // DEPRECATED
+        api.listen.on('cleanup', () => delete _converse.state.chatboxviews);
+        api.listen.on('clearSession', () => chatboxviews.closeAllChatBoxes());
         api.listen.on('chatBoxViewsInitialized', calculateViewportHeightUnit);
 
         window.addEventListener('resize', calculateViewportHeightUnit);
@@ -50,13 +51,14 @@ converse.plugins.add('converse-chatboxviews', {
              * @async
              * @memberOf converse
              * @method insertInto
+             * @param {HTMLElement} container
              * @example
              * converse.insertInto(document.querySelector('#converse-container'));
              */
             insertInto (container) {
-                const el = _converse.chatboxviews?.el;
+                const el = chatboxviews.el;
                 if (el && !container.contains(el)) {
-                    container.insertAdjacentElement('afterBegin', el);
+                    container.insertAdjacentElement('afterbegin', el);
                 } else if (!el) {
                     throw new Error('Cannot insert non-existing #conversejs element into the DOM');
                 }

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

@@ -11,7 +11,7 @@ function shouldShowChat (c) {
 
 
 export default () => {
-    const { chatboxes } = _converse;
+    const { chatboxes } = _converse.state;
     const view_mode = api.settings.get('view_mode');
     const connection = api.connection.get();
     const logged_out = !connection?.connected || !connection?.authenticated || connection?.disconnecting;

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

@@ -9,7 +9,7 @@ import { render } from 'lit';
 class ConverseChats extends CustomElement {
 
     initialize () {
-        this.model = _converse.chatboxes;
+        this.model = _converse.state.chatboxes;
         this.listenTo(this.model, 'add', () => this.requestUpdate());
         this.listenTo(this.model, 'change:closed', () => this.requestUpdate());
         this.listenTo(this.model, 'change:hidden', () => this.requestUpdate());

+ 4 - 2
src/plugins/chatview/bottom-panel.js

@@ -54,11 +54,13 @@ export default class ChatBottomPanel extends CustomElement {
     }
 
     emitFocused (ev) {
-        _converse.chatboxviews.get(this.getAttribute('jid'))?.emitFocused(ev);
+        const { chatboxviews } = _converse.state;
+        chatboxviews.get(this.getAttribute('jid'))?.emitFocused(ev);
     }
 
     emitBlurred (ev) {
-        _converse.chatboxviews.get(this.getAttribute('jid'))?.emitBlurred(ev);
+        const { chatboxviews } = _converse.state;
+        chatboxviews.get(this.getAttribute('jid'))?.emitBlurred(ev);
     }
 
     onDragOver (ev) { // eslint-disable-line class-methods-use-this

+ 7 - 5
src/plugins/chatview/chat.js

@@ -4,6 +4,7 @@ import BaseChatView from 'shared/chat/baseview.js';
 import tplChat from './templates/chat.js';
 import { __ } from 'i18n';
 import { _converse, api } from '@converse/headless';
+import { ACTIVE } from 'headless/shared/constants.js';
 
 /**
  * The view of an open/ongoing chat conversation.
@@ -15,8 +16,9 @@ export default class ChatView extends BaseChatView {
     length = 200
 
     async initialize () {
-        _converse.chatboxviews.add(this.jid, this);
-        this.model = _converse.chatboxes.get(this.jid);
+        const { chatboxviews, chatboxes } = _converse.state;
+        chatboxviews.add(this.jid, this);
+        this.model = chatboxes.get(this.jid);
         this.listenTo(this.model, 'change:hidden', () => !this.model.get('hidden') && this.afterShown());
         this.listenTo(this.model, 'change:show_help_messages', () => this.requestUpdate());
 
@@ -25,9 +27,9 @@ export default class ChatView extends BaseChatView {
         await this.model.messages.fetched;
         !this.model.get('hidden') && this.afterShown()
         /**
-         * Triggered once the {@link _converse.ChatBoxView} has been initialized
+         * Triggered once the {@link ChatView} has been initialized
          * @event _converse#chatBoxViewInitialized
-         * @type { _converse.ChatBoxView }
+         * @type {ChatView}
          * @example _converse.api.listen.on('chatBoxViewInitialized', view => { ... });
          */
         api.trigger('chatBoxViewInitialized', this);
@@ -51,7 +53,7 @@ export default class ChatView extends BaseChatView {
     }
 
     afterShown () {
-        this.model.setChatState(_converse.ACTIVE);
+        this.model.setChatState(ACTIVE);
         this.model.clearUnreadMsgCounter();
         this.maybeFocus();
     }

+ 4 - 2
src/plugins/chatview/heading.js

@@ -35,7 +35,8 @@ export default class ChatHeading extends CustomElement {
     }
 
     initialize () {
-        this.model = _converse.chatboxes.get(this.jid);
+        const { chatboxes } = _converse.state;
+        this.model = chatboxes.get(this.jid);
         this.listenTo(this.model, 'change:status', () => this.requestUpdate());
         this.listenTo(this.model, 'vcard:add', () => this.requestUpdate());
         this.listenTo(this.model, 'vcard:change', () => this.requestUpdate());
@@ -95,7 +96,8 @@ export default class ChatHeading extends CustomElement {
                 'standalone': api.settings.get('view_mode') === 'overlayed'
             });
         }
-        const el = _converse.chatboxviews.get(this.getAttribute('jid'));
+        const { chatboxviews } = _converse.state;
+        const el = chatboxviews.get(this.getAttribute('jid'));
         if (el) {
             /**
              * *Hook* which allows plugins to add more buttons to a chat's heading.

+ 6 - 1
src/plugins/chatview/index.js

@@ -57,7 +57,12 @@ converse.plugins.add('converse-chatview', {
             }
         });
 
-        _converse.ChatBoxView = ChatView;
+        const exports = {
+            ChatBoxView: ChatView,
+            ChatView,
+        }
+        Object.assign(_converse, exports); // DEPRECATED
+        Object.assign(_converse.exports, exports);
 
         api.listen.on('connected', () => api.disco.own.features.add(Strophe.NS.SPOILER));
         api.listen.on('chatBoxClosed', (model) => clearHistory(model.get('jid')));

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

@@ -2,6 +2,7 @@
  * @typedef {import('shared/chat/emoji-dropdown.js').default} EmojiDropdown
  */
 import tplMessageForm from './templates/message-form.js';
+import { ACTIVE, COMPOSING } from 'headless/shared/constants.js';
 import { CustomElement } from 'shared/components/element.js';
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless";
@@ -14,7 +15,8 @@ const { u } = converse.env;
 export default class MessageForm extends CustomElement {
 
     async initialize () {
-        this.model = _converse.chatboxes.get(this.getAttribute('jid'));
+        const { chatboxes } = _converse.state;
+        this.model = chatboxes.get(this.getAttribute('jid'));
         await this.model.initialized;
         this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
         this.listenTo(this.model, 'change:composing_spoiler', () => this.requestUpdate());
@@ -180,15 +182,16 @@ export default class MessageForm extends CustomElement {
         ) {
             return;
         }
-        if (this.model.get('chat_state') !== _converse.COMPOSING) {
+        if (this.model.get('chat_state') !== COMPOSING) {
             // Set chat state to composing if keyCode is not a forward-slash
             // (which would imply an internal command and not a message).
-            this.model.setChatState(_converse.COMPOSING);
+            this.model.setChatState(COMPOSING);
         }
     }
 
     async onFormSubmitted (ev) {
         ev?.preventDefault?.();
+        const { chatboxviews } = _converse.state;
 
         const textarea = /** @type {HTMLTextAreaElement} */(this.querySelector('.chat-textarea'));
         const message_text = textarea.value.trim();
@@ -226,7 +229,7 @@ export default class MessageForm extends CustomElement {
         if (api.settings.get('view_mode') === 'overlayed') {
             // XXX: Chrome flexbug workaround. The .chat-content area
             // doesn't resize when the textarea is resized to its original size.
-            const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
+            const chatview = chatboxviews.get(this.getAttribute('jid'));
             const msgs_container = chatview.querySelector('.chat-content__messages');
             msgs_container.parentElement.style.display = 'none';
         }
@@ -235,13 +238,13 @@ export default class MessageForm extends CustomElement {
 
         if (api.settings.get('view_mode') === 'overlayed') {
             // XXX: Chrome flexbug workaround.
-            const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
+            const chatview = chatboxviews.get(this.getAttribute('jid'));
             const msgs_container = chatview.querySelector('.chat-content__messages');
             msgs_container.parentElement.style.display = '';
         }
         // Suppress events, otherwise superfluous CSN gets set
         // immediately after the message, causing rate-limiting issues.
-        this.model.setChatState(_converse.ACTIVE, { 'silent': true });
+        this.model.setChatState(ACTIVE, { 'silent': true });
         textarea.focus();
     }
 }

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

@@ -80,7 +80,7 @@ describe("Chatboxes", function () {
             // openControlBox was called earlier, so the controlbox is
             // visible, but no other chat boxes have been created.
             expect(_converse.chatboxes.length).toEqual(1);
-            spyOn(_converse.minimize, 'trimChats');
+            spyOn(_converse.exports.minimize, 'trimChats');
             expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
 
             const rosterview = document.querySelector('converse-roster');
@@ -90,11 +90,11 @@ describe("Chatboxes", function () {
             let el = online_contacts[0];
             el.click();
             await u.waitUntil(() => document.querySelectorAll("#conversejs .chatbox").length == 2);
-            expect(_converse.minimize.trimChats).toHaveBeenCalled();
+            expect(_converse.exports.minimize.trimChats).toHaveBeenCalled();
             online_contacts[1].click();
             await u.waitUntil(() => _converse.chatboxes.length == 3);
             el = online_contacts[1];
-            expect(_converse.minimize.trimChats).toHaveBeenCalled();
+            expect(_converse.exports.minimize.trimChats).toHaveBeenCalled();
             // Check that new chat boxes are created to the left of the
             // controlbox (but to the right of all existing chat boxes)
             expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(3);
@@ -163,7 +163,7 @@ describe("Chatboxes", function () {
         it("can be saved to, and retrieved from, browserStorage",
                 mock.initConverse([], {}, async function (_converse) {
 
-            spyOn(_converse.minimize, 'trimChats');
+            spyOn(_converse.exports.minimize, 'trimChats');
             await mock.waitForRoster(_converse, 'current');
             await mock.openControlBox(_converse);
 
@@ -171,7 +171,7 @@ describe("Chatboxes", function () {
 
             mock.openChatBoxes(_converse, 6);
             await u.waitUntil(() => _converse.chatboxes.length == 7);
-            expect(_converse.minimize.trimChats).toHaveBeenCalled();
+            expect(_converse.exports.minimize.trimChats).toHaveBeenCalled();
             // We instantiate a new ChatBoxes collection, which by default
             // will be empty.
             const newchatboxes = new _converse.ChatBoxes();
@@ -215,7 +215,7 @@ describe("Chatboxes", function () {
 
             await mock.waitForRoster(_converse, 'current');
             await mock.openControlBox(_converse);
-            spyOn(_converse.minimize, 'trimChats');
+            spyOn(_converse.exports.minimize, 'trimChats');
             const rosterview = document.querySelector('converse-roster');
             await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
             spyOn(_converse.api, "trigger").and.callThrough();
@@ -226,7 +226,7 @@ describe("Chatboxes", function () {
             expect(_converse.chatboxes.pluck('id')).toEqual(['controlbox']);
             mock.openChatBoxes(_converse, 6);
             await u.waitUntil(() => _converse.chatboxes.length == 7)
-            expect(_converse.minimize.trimChats).toHaveBeenCalled();
+            expect(_converse.exports.minimize.trimChats).toHaveBeenCalled();
             expect(_converse.chatboxes.length).toEqual(7);
             expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxViewInitialized', jasmine.any(Object));
             await mock.closeAllChatBoxes(_converse);
@@ -394,11 +394,11 @@ describe("Chatboxes", function () {
                     await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length);
                     await mock.openChatBoxFor(_converse, contact_jid);
                     const model = _converse.chatboxes.get(contact_jid);
-                    _converse.minimize.minimize(model);
+                    _converse.exports.minimize.minimize(model);
                     const sent_stanzas = api.connection.get().sent_stanzas;
                     sent_stanzas.splice(0, sent_stanzas.length);
                     expect(model.get('chat_state')).toBe('inactive');
-                    _converse.minimize.maximize(model);
+                    _converse.exports.minimize.maximize(model);
                     await u.waitUntil(() => model.get('chat_state') === 'active', 1000);
                     const stanza = await u.waitUntil(() => sent_stanzas.filter(s => sizzle(`active`, s).length).pop());
                     expect(Strophe.serialize(stanza)).toBe(
@@ -761,7 +761,7 @@ describe("Chatboxes", function () {
                     await mock.openChatBoxFor(_converse, contact_jid);
                     const view = _converse.chatboxviews.get(contact_jid);
                     spyOn(api.connection.get(), 'send');
-                    _converse.minimize.minimize(view.model);
+                    _converse.exports.minimize.minimize(view.model);
                     expect(view.model.get('chat_state')).toBe('inactive');
                     expect(api.connection.get().send).toHaveBeenCalled();
                     var stanza = api.connection.get().send.calls.argsFor(0)[0];
@@ -987,7 +987,7 @@ describe("Chatboxes", function () {
             await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
-            _converse.minimize.minimize(chatbox);
+            _converse.exports.minimize.minimize(chatbox);
 
             msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
             await _converse.handleMessageStanza(msg);
@@ -1016,14 +1016,14 @@ describe("Chatboxes", function () {
             const view = _converse.chatboxviews.get(sender_jid);
             const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
             const select_msgs_indicator = () => sizzle(selector, rosterview).pop();
-            _converse.minimize.minimize(view.model);
+            _converse.exports.minimize.minimize(view.model);
             _converse.handleMessageStanza(msgFactory());
             await u.waitUntil(() => chatbox.messages.length);
             expect(select_msgs_indicator().textContent).toBe('1');
             _converse.handleMessageStanza(msgFactory());
             await u.waitUntil(() => chatbox.messages.length > 1);
             expect(select_msgs_indicator().textContent).toBe('2');
-            _converse.minimize.maximize(view.model);
+            _converse.exports.minimize.maximize(view.model);
             u.waitUntil(() => typeof select_msgs_indicator() === 'undefined');
         }));
 

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

@@ -107,7 +107,7 @@ describe("A Chat Message", function () {
                 'from': sender_jid,
                 'type': 'chat'})
             .c('body').t("Older message").up()
-            .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'})
+            .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T11:08:25Z'})
             .tree();
         _converse.handleMessageStanza(msg);
         await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2);
@@ -155,7 +155,7 @@ describe("A Chat Message", function () {
                 'from': sender_jid,
                 'type': 'chat'})
             .c('body').t("newer message from the next day").up()
-            .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'})
+            .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T20:28:23Z'})
             .tree();
         _converse.handleMessageStanza(msg);
         await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 6);

+ 2 - 1
src/plugins/chatview/utils.js

@@ -41,7 +41,8 @@ export async function parseMessageForCommands (chat, text) {
             clearMessages(chat);
             return true;
         } else if (match[1] === 'close') {
-            _converse.chatboxviews.get(chat.get('jid'))?.close();
+            const { chatboxviews } = _converse.state;
+            chatboxviews.get(chat.get('jid'))?.close();
             return true;
         } else if (match[1] === 'help') {
             chat.set({ 'show_help_messages': false }, { 'silent': true });

+ 4 - 6
src/plugins/controlbox/api.js

@@ -1,7 +1,5 @@
-/**
- * @typedef {import('./controlbox.js').default} ControlBox
- */
 import { _converse, api, converse } from "@converse/headless";
+import ControlBox from "./model.js";
 
 const { u } = converse.env;
 
@@ -17,13 +15,13 @@ export default {
         /**
          * Opens the controlbox
          * @method _converse.api.controlbox.open
-         * @returns { Promise<_converse.ControlBox> }
+         * @returns { Promise<ControlBox> }
          */
         async open () {
             await api.waitUntil('chatBoxesFetched');
             let model = await api.chatboxes.get('controlbox');
             if (!model) {
-              model = await api.chatboxes.create('controlbox', {}, _converse.ControlBox);
+              model = await api.chatboxes.create('controlbox', {}, ControlBox);
             }
             u.safeSave(model, {'closed': false});
             return model;
@@ -36,7 +34,7 @@ export default {
          * @example const view = _converse.api.controlbox.get();
          */
         get () {
-            return _converse.chatboxviews.get('controlbox');
+            return _converse.state.chatboxviews.get('controlbox');
         }
     }
 }

+ 9 - 7
src/plugins/controlbox/controlbox.js

@@ -12,11 +12,12 @@ const u = converse.env.utils;
  * In `overlayed` `view_mode` it's a box like the chat boxes, in `fullscreen`
  * `view_mode` it's a left-aligned sidebar.
  */
-class ControlBox extends CustomElement {
+class ControlBoxView extends CustomElement {
 
     initialize () {
         this.setModel();
-        _converse.chatboxviews.add('controlbox', this);
+        const { chatboxviews } = _converse.state;
+        chatboxviews.add('controlbox', this);
         if (this.model.get('connected') && this.model.get('closed') === undefined) {
             this.model.set('closed', !api.settings.get('show_controlbox_by_default'));
         }
@@ -27,7 +28,7 @@ class ControlBox extends CustomElement {
          * exists. The controlbox contains the login and register forms when the user is
          * logged out and a list of the user's contacts and group chats when logged in.
          * @event _converse#controlBoxInitialized
-         * @type { _converse.ControlBoxView }
+         * @type {ControlBoxView}
          * @example _converse.api.listen.on('controlBoxInitialized', view => { ... });
          */
         api.trigger('controlBoxInitialized', this);
@@ -48,9 +49,10 @@ class ControlBox extends CustomElement {
 
     close (ev) {
         ev?.preventDefault?.();
+        const connection = api.connection.get();
         if (
             ev?.name === 'closeAllChatBoxes' &&
-            (_converse.disconnection_cause !== LOGOUT ||
+            (connection.disconnection_cause !== LOGOUT ||
                 api.settings.get('show_controlbox_by_default'))
         ) {
             return;
@@ -67,13 +69,13 @@ class ControlBox extends CustomElement {
         /**
          * Triggered once the controlbox has been opened
          * @event _converse#controlBoxOpened
-         * @type {_converse.ControlBox}
+         * @type {ControlBoxView}
          */
         api.trigger('controlBoxOpened', this);
         return this;
     }
 }
 
-api.elements.define('converse-controlbox', ControlBox);
+api.elements.define('converse-controlbox', ControlBoxView);
 
-export default ControlBox;
+export default ControlBoxView;

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

@@ -47,9 +47,9 @@ converse.plugins.add('converse-controlbox', {
         api.promises.add('controlBoxInitialized', false);
         Object.assign(api, controlbox_api);
 
-        _converse.ControlBoxView = ControlBoxView;
-        _converse.ControlBox = ControlBox;
-        _converse.ControlBoxToggle = ControlBoxToggle;
+        const exports = { ControlBox, ControlBoxView, ControlBoxToggle };
+        Object.assign(_converse, exports); // DEPRECATED
+        Object.assign(_converse.exports, exports);
 
         api.chatboxes.registry.add(CONTROLBOX_TYPE, ControlBox);
 

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

@@ -37,7 +37,8 @@ class LoginForm extends CustomElement {
         ev?.preventDefault();
 
         if (api.settings.get('authentication') === ANONYMOUS) {
-            return this.connect(_converse.jid);
+            const jid = _converse.session.get('jid');
+            return this.connect(jid);
         }
 
         if (!validateJID(ev.target)) {

+ 6 - 4
src/plugins/controlbox/model.js

@@ -10,11 +10,10 @@ const { dayjs } = converse.env;
  *
  * In `overlayed` `view_mode` it's a box like the chat boxes, in `fullscreen`
  * `view_mode` it's a left-aligned sidebar.
- * @mixin
  */
 class ControlBox extends Model {
 
-    defaults () {  // eslint-disable-line class-methods-use-this
+    defaults () {
         return {
             'bookmarked': false,
             'box_id': 'controlbox',
@@ -34,15 +33,18 @@ class ControlBox extends Model {
             }
             return;
         }
-        return _converse.ChatBox.prototype.validate.call(this, attrs);
+        return _converse.state.ChatBox.prototype.validate.call(this, attrs);
     }
 
+    /**
+     * @param {boolean} [force]
+     */
     maybeShow (force) {
         if (!force && this.get('id') === 'controlbox') {
             // Must return the chatbox
             return this;
         }
-        return _converse.ChatBox.prototype.maybeShow.call(this, force);
+        return _converse.state.ChatBox.prototype.maybeShow.call(this, force);
     }
 
     onReconnection () {

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

@@ -1,6 +1,10 @@
+/**
+ * @typedef {import('../controlbox').default} ControlBoxView
+ */
 import tplSpinner from "templates/spinner.js";
 import { _converse, api, converse } from "@converse/headless";
 import { html } from 'lit';
+import { ANONYMOUS } from "@converse/headless/shared/constants";
 
 const { Strophe } = converse.env;
 
@@ -17,6 +21,9 @@ function whenNotConnected (o) {
 }
 
 
+/**
+ * @param {ControlBoxView} el
+ */
 export default (el) => {
     const o = el.model.toJSON();
     const sticky_controlbox = api.settings.get('sticky_controlbox');
@@ -40,7 +47,7 @@ export default (el) => {
                             <converse-user-profile></converse-user-profile>
                             <converse-headlines-feeds-list class="controlbox-section"></converse-headlines-feeds-list>
                             <div id="chatrooms" class="controlbox-section"><converse-rooms-list></converse-rooms-list></div>
-                            ${ api.settings.get("authentication") === _converse.ANONYMOUS ? '' :
+                            ${ api.settings.get("authentication") === ANONYMOUS ? '' :
                                 html`<div id="converse-roster" class="controlbox-section"><converse-roster></converse-roster></div>`
                             }`
                         : whenNotConnected(o)

+ 3 - 1
src/plugins/controlbox/toggle.js

@@ -9,7 +9,9 @@ class ControlBoxToggle extends CustomElement {
     async connectedCallback () {
         super.connectedCallback();
         await api.waitUntil('initialized')
-        this.model = _converse.chatboxes.get('controlbox');
+
+        const { chatboxes } = _converse.state;
+        this.model = chatboxes.get('controlbox');
         this.listenTo(this.model, 'change:closed', () => this.requestUpdate());
         this.requestUpdate();
     }

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

@@ -4,20 +4,20 @@ import { _converse, api, converse } from "@converse/headless";
 const { Strophe, u } = converse.env;
 
 export function addControlBox () {
-    const m = _converse.chatboxes.add(new _converse.ControlBox({'id': 'controlbox'}));
-     _converse.chatboxviews.get('controlbox')?.setModel();
+    const m = _converse.state.chatboxes.add(new _converse.exports.ControlBox({'id': 'controlbox'}));
+     _converse.state.chatboxviews.get('controlbox')?.setModel();
     return m;
 }
 
 export function showControlBox (ev) {
     ev?.preventDefault?.();
-    const controlbox = _converse.chatboxes.get('controlbox') || addControlBox();
+    const controlbox = _converse.state.chatboxes.get('controlbox') || addControlBox();
     u.safeSave(controlbox, {'closed': false});
 }
 
 export function navigateToControlBox (jid) {
     showControlBox();
-    const model = _converse.chatboxes.get(jid);
+    const model = _converse.state.chatboxes.get(jid);
     u.safeSave(model, {'hidden': true});
 }
 
@@ -26,13 +26,13 @@ export function disconnect () {
      * we reconnect, "onConnected" will be called,
      * to fetch the roster again and to send out a presence stanza.
      */
-    const view = _converse.chatboxviews.get('controlbox');
+    const view = _converse.state.chatboxviews.get('controlbox');
     view.model.set({ 'connected': false });
     return view;
 }
 
 export function clearSession () {
-    const chatboxviews = _converse?.chatboxviews;
+    const chatboxviews = _converse.state.chatboxviews;
     const view = chatboxviews && chatboxviews.get('controlbox');
     if (view) {
         u.safeSave(view.model, { 'connected': false });
@@ -44,7 +44,7 @@ export function clearSession () {
 }
 
 export function onChatBoxesFetched () {
-    const controlbox = _converse.chatboxes.get('controlbox') || addControlBox();
+    const controlbox = _converse.state.chatboxes.get('controlbox') || addControlBox();
     controlbox.save({ 'connected': true });
 }
 
@@ -81,7 +81,7 @@ export function updateSettingsWithFormData (form, settings={}) {
 
     api.settings.set(settings);
 
-    _converse.config.save({ 'trusted': (form_data.get('trusted') && true) || false });
+    _converse.state.config.save({ 'trusted': (form_data.get('trusted') && true) || false });
 }
 
 

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

@@ -19,8 +19,8 @@ converse.plugins.add('converse-dragresize', {
      */
     dependencies: ['converse-chatview', 'converse-headlines-view', 'converse-muc-views'],
 
-    enabled (_converse) {
-        return _converse.api.settings.get('view_mode') == 'overlayed';
+    enabled () {
+        return api.settings.get('view_mode') == 'overlayed';
     },
 
     // Overrides mentioned here will be picked up by converse.js's
@@ -50,10 +50,10 @@ converse.plugins.add('converse-dragresize', {
             'allow_dragresize': true
         });
 
-        Object.assign(_converse.ChatBoxView.prototype, DragResizableMixin);
-        Object.assign(_converse.ChatRoomView.prototype, DragResizableMixin);
-        if (_converse.ControlBoxView) {
-            Object.assign(_converse.ControlBoxView.prototype, DragResizableMixin);
+        Object.assign(_converse.exports.ChatView.prototype, DragResizableMixin);
+        Object.assign(_converse.exports.MUCView.prototype, DragResizableMixin);
+        if (_converse.exports.ControlBoxView) {
+            Object.assign(_converse.exports.ControlBoxView.prototype, DragResizableMixin);
         }
 
         /************************ BEGIN Event Handlers ************************/

+ 5 - 4
src/plugins/dragresize/mixin.js

@@ -1,6 +1,6 @@
 import debounce from 'lodash-es/debounce';
-import { _converse, api } from '@converse/headless';
-import { applyDragResistance } from './utils.js';
+import { api } from '@converse/headless';
+import { applyDragResistance, getResizingDirection } from './utils.js';
 
 const DragResizableMixin = {
     initDragResize () {
@@ -37,7 +37,8 @@ const DragResizableMixin = {
 
     resizeChatBox (ev) {
         let diff;
-        if (_converse.resizing.direction.indexOf('top') === 0) {
+        const direction = getResizingDirection();
+        if (direction.indexOf('top') === 0) {
             diff = ev.pageY - this.prev_pageY;
             if (diff) {
                 this.height =
@@ -48,7 +49,7 @@ const DragResizableMixin = {
                 this.setChatBoxHeight(this.height);
             }
         }
-        if (_converse.resizing.direction.includes('left')) {
+        if (direction.includes('left')) {
             diff = this.prev_pageX - ev.pageX;
             if (diff) {
                 this.width =

+ 32 - 22
src/plugins/dragresize/utils.js

@@ -1,7 +1,20 @@
-import { _converse, api, converse } from '@converse/headless';
+import { api, converse } from '@converse/headless';
 
 const { u } = converse.env;
 
+/**
+ * @typedef {Object} ResizingData
+ * @property {HTMLElement} chatbox
+ * @property {string} direction
+ */
+const resizing = {};
+
+/**
+ * @returns {string}
+ */
+export function getResizingDirection () {
+    return resizing.direction;
+}
 
 export function onStartVerticalResize (ev, trigger = true) {
     if (!api.settings.get('allow_dragresize')) {
@@ -13,10 +26,8 @@ export function onStartVerticalResize (ev, trigger = true) {
     const style = window.getComputedStyle(flyout);
     const chatbox_el = flyout.parentElement;
     chatbox_el.height = parseInt(style.height.replace(/px$/, ''), 10);
-    _converse.resizing = {
-        'chatbox': chatbox_el,
-        'direction': 'top'
-    };
+    resizing.chatbox = chatbox_el;
+    resizing.direction = 'top';
     chatbox_el.prev_pageY = ev.pageY;
     if (trigger) {
         /**
@@ -37,10 +48,8 @@ export function onStartHorizontalResize (ev, trigger = true) {
     const style = window.getComputedStyle(flyout);
     const chatbox_el = flyout.parentElement;
     chatbox_el.width = parseInt(style.width.replace(/px$/, ''), 10);
-    _converse.resizing = {
-        'chatbox': chatbox_el,
-        'direction': 'left'
-    };
+    resizing.chatbox = chatbox_el;
+    resizing.direction = 'left';
     chatbox_el.prev_pageX = ev.pageX;
     if (trigger) {
         /**
@@ -55,7 +64,7 @@ export function onStartHorizontalResize (ev, trigger = true) {
 export function onStartDiagonalResize (ev) {
     onStartHorizontalResize(ev, false);
     onStartVerticalResize(ev, false);
-    _converse.resizing.direction = 'topleft';
+    resizing.direction = 'topleft';
     /**
      * Triggered once the user starts to diagonally resize a {@link _converse.ChatBoxView}
      * @event _converse#startDiagonalResize
@@ -86,32 +95,33 @@ export function applyDragResistance (value, default_value) {
 }
 
 export function onMouseMove (ev) {
-    if (!_converse.resizing || !api.settings.get('allow_dragresize')) {
+    if (!resizing.chatbox || !api.settings.get('allow_dragresize')) {
         return true;
     }
     ev.preventDefault();
-    _converse.resizing.chatbox.resizeChatBox(ev);
+    resizing.chatbox.resizeChatBox(ev);
 }
 
 export function onMouseUp (ev) {
-    if (!_converse.resizing || !api.settings.get('allow_dragresize')) {
+    if (!resizing.chatbox || !api.settings.get('allow_dragresize')) {
         return true;
     }
     ev.preventDefault();
     const height = applyDragResistance(
-        _converse.resizing.chatbox.height,
-        _converse.resizing.chatbox.model.get('default_height')
+        resizing.chatbox.height,
+        resizing.chatbox.model.get('default_height')
     );
     const width = applyDragResistance(
-        _converse.resizing.chatbox.width,
-        _converse.resizing.chatbox.model.get('default_width')
+        resizing.chatbox.width,
+        resizing.chatbox.model.get('default_width')
     );
     if (api.connection.connected()) {
-        _converse.resizing.chatbox.model.save({ 'height': height });
-        _converse.resizing.chatbox.model.save({ 'width': width });
+        resizing.chatbox.model.save({ 'height': height });
+        resizing.chatbox.model.save({ 'width': width });
     } else {
-        _converse.resizing.chatbox.model.set({ 'height': height });
-        _converse.resizing.chatbox.model.set({ 'width': width });
+        resizing.chatbox.model.set({ 'height': height });
+        resizing.chatbox.model.set({ 'width': width });
     }
-    _converse.resizing = null;
+    delete resizing.chatbox;
+    delete resizing.direction;
 }

+ 2 - 2
src/plugins/headlines-view/feed-list.js

@@ -12,7 +12,7 @@ import { HEADLINES_TYPE } from '@converse/headless/shared/constants.js';
 export class HeadlinesFeedsList extends CustomElement {
 
     initialize () {
-        this.model = _converse.chatboxes;
+        this.model = _converse.state.chatboxes;
         this.listenTo(this.model, 'add', (m) => this.renderIfHeadline(m));
         this.listenTo(this.model, 'remove', (m) => this.renderIfHeadline(m));
         this.listenTo(this.model, 'destroy', (m) => this.renderIfHeadline(m));
@@ -27,7 +27,7 @@ export class HeadlinesFeedsList extends CustomElement {
         return model?.get('type') === HEADLINES_TYPE && this.requestUpdate();
     }
 
-    async openHeadline (ev) { // eslint-disable-line class-methods-use-this
+    async openHeadline (ev) {
         ev.preventDefault();
         const jid = ev.target.getAttribute('data-headline-jid');
         const feed = await api.headlines.get(jid);

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

@@ -18,7 +18,7 @@ export default class HeadlinesHeading extends CustomElement {
     }
 
     async initialize () {
-        this.model = _converse.chatboxes.get(this.jid);
+        this.model = _converse.state.chatboxes.get(this.jid);
         await this.model.initialized;
         this.requestUpdate();
     }

+ 6 - 4
src/plugins/headlines-view/index.js

@@ -26,9 +26,11 @@ converse.plugins.add('converse-headlines-view', {
     dependencies: ['converse-headlines', 'converse-chatview'],
 
     initialize () {
-        _converse.HeadlinesFeedsList = HeadlinesFeedsList;
-
-        // Deprecated
-        _converse.HeadlinesPanel = HeadlinesFeedsList;
+        const exports =  {
+            HeadlinesFeedsList: HeadlinesFeedsList,
+            HeadlinesPanel: HeadlinesFeedsList, // DEPRECATED
+        }
+        Object.assign(_converse, exports); // DEPRECATED
+        Object.assign(_converse.exports, exports);
     }
 });

+ 5 - 2
src/plugins/headlines-view/view.js

@@ -6,9 +6,10 @@ import { _converse, api } from '@converse/headless';
 class HeadlinesFeedView extends BaseChatView {
 
     async initialize() {
-        _converse.chatboxviews.add(this.jid, this);
+        const { chatboxviews, chatboxes } = _converse.state;
+        chatboxviews.add(this.jid, this);
 
-        this.model = _converse.chatboxes.get(this.jid);
+        this.model = chatboxes.get(this.jid);
         this.listenTo(this.model, 'change:hidden', () => this.afterShown());
         this.listenTo(this.model, 'destroy', this.remove);
         this.listenTo(this.model.messages, 'add', () => this.requestUpdate());
@@ -56,3 +57,5 @@ class HeadlinesFeedView extends BaseChatView {
 }
 
 api.elements.define('converse-headlines', HeadlinesFeedView);
+
+export default HeadlinesFeedView;

+ 2 - 1
src/plugins/mam-views/utils.js

@@ -21,7 +21,8 @@ export async function fetchMessagesOnScrollUp (view) {
         const is_groupchat = view.model.get('type') === CHATROOMS_TYPE;
         const oldest_message = view.model.getOldestMessage();
         if (oldest_message) {
-            const by_jid = is_groupchat ? view.model.get('jid') : _converse.bare_jid;
+            const bare_jid = _converse.session.get('bare_jid');
+            const by_jid = is_groupchat ? view.model.get('jid') : bare_jid;
             const stanza_id = oldest_message && oldest_message.get(`stanza_id ${by_jid}`);
             view.model.ui.set('chat-content-spinner-top', true);
             try {

+ 15 - 6
src/plugins/minimize/index.js

@@ -2,6 +2,9 @@
  * @module converse-minimize
  * @copyright 2022, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
+ *
+ * @typedef {import('@converse/headless/plugins/muc/muc').default} MUC
+ * @typedef {import('@converse/headless/plugins/chat/model').default} ChatBox
  */
 import './view.js';
 import './components/minimized-chat.js';
@@ -90,17 +93,23 @@ converse.plugins.add('converse-minimize', {
 
         api.promises.add('minimizedChatsInitialized');
 
-        _converse.MinimizedChatsToggle = MinimizedChatsToggle;
-        _converse.minimize = { trimChats, minimize, maximize };
+        const exports = { MinimizedChatsToggle };
+        Object.assign(_converse, exports); // DEPRECATED
+        Object.assign(_converse.exports, exports);
+        Object.assign(_converse, { minimize: { trimChats, minimize, maximize }}); // DEPRECATED
+        Object.assign(_converse.exports, { minimize: { trimChats, minimize, maximize }});
 
+        /**
+         * @param { ChatBox|MUC } model
+         */
         function onChatInitialized (model) {
             initializeChat(model);
             model.on( 'change:minimized', () => onMinimizedChanged(model));
         }
 
-        api.listen.on('chatBoxViewInitialized', view => _converse.minimize.trimChats(view));
-        api.listen.on('chatRoomViewInitialized', view => _converse.minimize.trimChats(view));
-        api.listen.on('controlBoxOpened', view => _converse.minimize.trimChats(view));
+        api.listen.on('chatBoxViewInitialized', view => _converse.exports.minimize.trimChats(view));
+        api.listen.on('chatRoomViewInitialized', view => _converse.exports.minimize.trimChats(view));
+        api.listen.on('controlBoxOpened', view => _converse.exports.minimize.trimChats(view));
         api.listen.on('chatBoxInitialized', onChatInitialized);
         api.listen.on('chatRoomInitialized', onChatInitialized);
 
@@ -112,7 +121,7 @@ converse.plugins.add('converse-minimize', {
             }
         });
 
-        const debouncedTrimChats = debounce(() => _converse.minimize.trimChats(), 250);
+        const debouncedTrimChats = debounce(() => _converse.exports.minimize.trimChats(), 250);
         api.listen.on('registeredGlobalEventHandlers', () => window.addEventListener("resize", debouncedTrimChats));
         api.listen.on('unregisteredGlobalEventHandlers', () => window.removeEventListener("resize", debouncedTrimChats));
     }

+ 5 - 5
src/plugins/minimize/tests/minchats.js

@@ -131,7 +131,7 @@ describe("A Chatbox", function () {
 
 
     it("can be trimmed to conserve space", mock.initConverse([], {}, async function (_converse) {
-        spyOn(_converse.minimize, 'trimChats');
+        spyOn(_converse.exports.minimize, 'trimChats');
         await mock.waitForRoster(_converse, 'current');
         await mock.openControlBox(_converse);
 
@@ -151,7 +151,7 @@ describe("A Chatbox", function () {
             el.click();
         }
         await u.waitUntil(() => _converse.chatboxes.length == 16);
-        expect(_converse.minimize.trimChats.calls.count()).toBe(16);
+        expect(_converse.exports.minimize.trimChats.calls.count()).toBe(16);
 
         for (i=0; i<online_contacts.length; i++) {
             const el = online_contacts[i];
@@ -162,7 +162,7 @@ describe("A Chatbox", function () {
         await u.waitUntil(() => _converse.chatboxviews.keys().length === 1);
         const minimized_chats = await u.waitUntil(() => document.querySelector("converse-minimized-chats"));
         minimized_chats.querySelector("a.restore-chat").click();
-        expect(_converse.minimize.trimChats.calls.count()).toBe(16);
+        expect(_converse.exports.minimize.trimChats.calls.count()).toBe(16);
     }));
 });
 
@@ -181,7 +181,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
         _converse.handleMessageStanza(msgFactory());
         await u.waitUntil(() => chatbox.messages.length);
         await u.waitUntil(() => chatbox.get('num_unread') === 1);
-        _converse.minimize.minimize(chatbox);
+        _converse.exports.minimize.minimize(chatbox);
 
         const minimized_chats = await u.waitUntil(() => document.querySelector("converse-minimized-chats"));
         const unread_count = minimized_chats.querySelector('#toggle-minimized-chats .unread-message-count');
@@ -196,7 +196,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
         const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         const view = await mock.openChatBoxFor(_converse, sender_jid)
         const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
-        _converse.minimize.minimize(view.model);
+        _converse.exports.minimize.minimize(view.model);
         _converse.handleMessageStanza(msgFactory());
         await u.waitUntil(() => view.model.messages.length);
         const minimized_chats = await u.waitUntil(() => document.querySelector("converse-minimized-chats"));

+ 28 - 14
src/plugins/minimize/utils.js

@@ -1,9 +1,21 @@
+/**
+ * @typedef {import('@converse/headless/plugins/muc/muc').default} MUC
+ * @typedef {import('@converse/headless/plugins/chat/model').default} ChatBox
+ * @typedef {import('plugins/chatview/chat').default} ChatView
+ * @typedef {import('plugins/muc-views/muc').default} MUCView
+ * @typedef {import('plugins/controlbox/controlbox').default} ControlBoxView
+ * @typedef {import('plugins/headlines-view/view').default} HeadlinesFeedView
+ */
 import { _converse, api, converse } from '@converse/headless';
 import { __ } from 'i18n';
 import { isTestEnv } from '@converse/headless/utils/session.js';
+import {ACTIVE, INACTIVE} from 'headless/shared/constants';
 
 const { dayjs, u } = converse.env;
 
+/**
+ * @param { ChatBox|MUC } chat
+ */
 export function initializeChat (chat) {
     chat.on('change:hidden', m => !m.get('hidden') && maximize(chat), chat);
 
@@ -33,7 +45,7 @@ function getChatBoxWidth (view) {
 }
 
 function getShownChats () {
-    return _converse.chatboxviews.filter(el =>
+    return _converse.state.chatboxviews.filter(el =>
         // The controlbox can take a while to close,
         // so we need to check its state. That's why we checked the 'closed' state.
         !el.model.get('minimized') && !el.model.get('closed') && u.isVisible(el)
@@ -42,13 +54,13 @@ function getShownChats () {
 
 function getMinimizedWidth () {
     const minimized_el = document.querySelector('converse-minimized-chats');
-    return _converse.chatboxes.pluck('minimized').includes(true) ? u.getOuterWidth(minimized_el, true) : 0;
+    return _converse.state.chatboxes.pluck('minimized').includes(true) ? u.getOuterWidth(minimized_el, true) : 0;
 }
 
 function getBoxesWidth (newchat) {
     const new_id = newchat ? newchat.model.get('id') : null;
     const newchat_width = newchat ? u.getOuterWidth(newchat, true) : 0;
-    return Object.values(_converse.chatboxviews.xget(new_id))
+    return Object.values(_converse.state.chatboxviews.xget(new_id))
         .reduce((memo, view) => memo + getChatBoxWidth(view), newchat_width);
 }
 
@@ -57,9 +69,8 @@ function getBoxesWidth (newchat) {
  * It checks whether there is enough space on the page to show
  * another chat box. Otherwise it minimizes the oldest chat box
  * to create space.
- * @private
  * @method _converse.ChatBoxViews#trimChats
- * @param { _converse.ChatBoxView|_converse.ChatRoomView|_converse.ControlBoxView|_converse.HeadlinesFeedView } [newchat]
+ * @param { ChatView|MUCView|ControlBoxView|HeadlinesFeedView } [newchat]
  */
 export function trimChats (newchat) {
     if (isTestEnv() || api.settings.get('no_trimming') || api.settings.get("view_mode") !== 'overlayed') {
@@ -82,7 +93,7 @@ export function trimChats (newchat) {
             const new_id = newchat ? newchat.model.get('id') : null;
             const oldest_chat = getOldestMaximizedChat([new_id]);
             if (oldest_chat) {
-                const model = _converse.chatboxes.get(oldest_chat.get('id'));
+                const model = _converse.state.chatboxes.get(oldest_chat.get('id'));
                 model?.save('hidden', true);
                 minimize(oldest_chat);
             } else {
@@ -96,10 +107,10 @@ function getOldestMaximizedChat (exclude_ids) {
     // Get oldest view (if its id is not excluded)
     exclude_ids.push('controlbox');
     let i = 0;
-    let model = _converse.chatboxes.sort().at(i);
+    let model = _converse.state.chatboxes.sort().at(i);
     while (exclude_ids.includes(model.get('id')) || model.get('minimized') === true) {
         i++;
-        model = _converse.chatboxes.at(i);
+        model = _converse.state.chatboxes.at(i);
         if (!model) {
             return null;
         }
@@ -157,7 +168,7 @@ export function minimize (ev, model) {
     } else {
         model = ev;
     }
-    model.setChatState(_converse.INACTIVE);
+    model.setChatState(INACTIVE);
     u.safeSave(model, {
         'hidden': true,
         'minimized': true,
@@ -170,17 +181,17 @@ export function minimize (ev, model) {
  * `minimized` property set to false.
  *
  * Will trigger {@link _converse#chatBoxMaximized}
- * @returns {_converse.ChatBoxView|_converse.ChatRoomView}
+ * @param { ChatBox|MUC } model
  */
 function onMaximized (model) {
     if (!model.isScrolledUp()) {
         model.clearUnreadMsgCounter();
     }
-    model.setChatState(_converse.ACTIVE);
+    model.setChatState(ACTIVE);
     /**
      * Triggered when a previously minimized chat gets maximized
      * @event _converse#chatBoxMaximized
-     * @type { _converse.ChatBoxView }
+     * @type { ChatBox | MUC }
      * @example _converse.api.listen.on('chatBoxMaximized', view => { ... });
      */
     api.trigger('chatBoxMaximized', model);
@@ -189,20 +200,23 @@ function onMaximized (model) {
 /**
  * Handler which gets called when a {@link _converse#ChatBox} has it's
  * `minimized` property set to true.
+ * @param { ChatBox|MUC } model
  *
  * Will trigger {@link _converse#chatBoxMinimized}
- * @returns {_converse.ChatBoxView|_converse.ChatRoomView}
  */
 function onMinimized (model) {
     /**
      * Triggered when a previously maximized chat gets Minimized
      * @event _converse#chatBoxMinimized
-     * @type { _converse.ChatBoxView }
+     * @type { ChatBox|MUC }
      * @example _converse.api.listen.on('chatBoxMinimized', view => { ... });
      */
     api.trigger('chatBoxMinimized', model);
 }
 
+/**
+ * @param { ChatBox|MUC } model
+ */
 export function onMinimizedChanged (model) {
     if (model.get('minimized')) {
         onMinimized(model);

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

@@ -8,7 +8,7 @@ import { initStorage } from '@converse/headless/utils/storage.js';
 export default class MinimizedChats extends CustomElement {
 
     async initialize () {
-        this.model = _converse.chatboxes;
+        this.model = _converse.state.chatboxes;
         await this.initToggle();
         this.listenTo(this.minchats, 'change:collapsed', () => this.requestUpdate())
         this.listenTo(this.model, 'add', () => this.requestUpdate())
@@ -35,7 +35,8 @@ export default class MinimizedChats extends CustomElement {
     }
 
     async initToggle () {
-        const id = `converse.minchatstoggle-${_converse.bare_jid}`;
+        const bare_jid = _converse.session.get('bare_jid');
+        const id = `converse.minchatstoggle-${bare_jid}`;
         this.minchats = new MinimizedChatsToggle({id});
         initStorage(this.minchats, id, 'session');
         await new Promise(resolve => this.minchats.fetch({'success': resolve, 'error': resolve}));

+ 2 - 1
src/plugins/muc-views/bottom-panel.js

@@ -31,7 +31,8 @@ export default class MUCBottomPanel extends BottomPanel {
     }
 
     renderIfOwnOccupant (o) {
-        (o.get('jid') === _converse.bare_jid) && this.requestUpdate();
+        const bare_jid = _converse.session.get('bare_jid');
+        (o.get('jid') === bare_jid) && this.requestUpdate();
     }
 
     sendButtonClicked (ev) {

+ 1 - 1
src/plugins/muc-views/config-form.js

@@ -22,7 +22,7 @@ class MUCConfigForm extends CustomElement {
 
     connectedCallback () {
         super.connectedCallback();
-        this.model = _converse.chatboxes.get(this.jid);
+        this.model = _converse.state.chatboxes.get(this.jid);
         this.listenTo(this.model.features, 'change:passwordprotected', () => this.requestUpdate());
         this.listenTo(this.model.session, 'change:config_stanza', () => this.requestUpdate());
         this.getConfig();

+ 1 - 1
src/plugins/muc-views/destroyed.js

@@ -18,7 +18,7 @@ class MUCDestroyed extends CustomElement {
 
     connectedCallback () {
         super.connectedCallback();
-        this.model = _converse.chatboxes.get(this.jid);
+        this.model = _converse.state.chatboxes.get(this.jid);
     }
 
     render () {

+ 1 - 1
src/plugins/muc-views/disconnected.js

@@ -19,7 +19,7 @@ class MUCDisconnected extends CustomElement {
 
     connectedCallback () {
         super.connectedCallback();
-        this.model = _converse.chatboxes.get(this.jid);
+        this.model = _converse.state.chatboxes.get(this.jid);
     }
 
     render () {

+ 8 - 4
src/plugins/muc-views/heading.js

@@ -14,7 +14,8 @@ import './styles/muc-head.scss';
 export default class MUCHeading extends CustomElement {
 
     async initialize () {
-        this.model = _converse.chatboxes.get(this.getAttribute('jid'));
+        const { chatboxes } = _converse.state;
+        this.model = chatboxes.get(this.getAttribute('jid'));
         this.listenTo(this.model, 'change', () => this.requestUpdate());
         this.listenTo(this.model, 'vcard:add', () => this.requestUpdate());
         this.listenTo(this.model, 'vcard:change', () => this.requestUpdate());
@@ -35,13 +36,15 @@ export default class MUCHeading extends CustomElement {
     }
 
     onOccupantAdded (occupant) {
-        if (occupant.get('jid') === _converse.bare_jid) {
+        const bare_jid = _converse.session.get('bare_jid');
+        if (occupant.get('jid') === bare_jid) {
             this.requestUpdate();
         }
     }
 
     onOccupantAffiliationChanged (occupant) {
-        if (occupant.get('jid') === _converse.bare_jid) {
+        const bare_jid = _converse.session.get('bare_jid');
+        if (occupant.get('jid') === bare_jid) {
             this.requestUpdate();
         }
     }
@@ -177,7 +180,8 @@ export default class MUCHeading extends CustomElement {
             });
         }
 
-        const el = _converse.chatboxviews.get(this.getAttribute('jid'));
+        const { chatboxviews } = _converse.state;
+        const el = chatboxviews.get(this.getAttribute('jid'));
         if (el) {
             // This hook is described in src/plugins/chatview/heading.js
             return _converse.api.hook('getHeadingButtons', el, buttons);

+ 6 - 1
src/plugins/muc-views/index.js

@@ -59,7 +59,12 @@ converse.plugins.add('converse-muc-views', {
             }
         });
 
-        _converse.ChatRoomView = MUCView;
+        const exports = {
+            ChatRoomView: MUCView,
+            MUCView
+        };
+        Object.assign(_converse, exports); // DEPRECATED
+        Object.assign(_converse.exports, exports);
 
         if (!api.settings.get('muc_domain')) {
             // Use service discovery to get the default MUC domain

+ 7 - 5
src/plugins/muc-views/message-form.js

@@ -1,6 +1,8 @@
+import AutoComplete from 'shared/autocomplete/autocomplete.js';
 import MessageForm from 'plugins/chatview/message-form.js';
 import tplMUCMessageForm from './templates/message-form.js';
-import { _converse, api, converse } from "@converse/headless";
+import { FILTER_CONTAINS, FILTER_STARTSWITH } from 'shared/autocomplete/utils.js';
+import { api, converse } from "@converse/headless";
 import { getAutoCompleteListItem } from './utils.js';
 
 
@@ -32,7 +34,7 @@ export default class MUCMessageForm extends MessageForm {
     }
 
     initMentionAutoComplete () {
-        this.mention_auto_complete = new _converse.AutoComplete(this, {
+        this.mention_auto_complete = new AutoComplete(this, {
             'auto_first': true,
             'auto_evaluate': false,
             'min_chars': api.settings.get('muc_mention_autocomplete_min_chars'),
@@ -40,8 +42,8 @@ export default class MUCMessageForm extends MessageForm {
             'list': () => this.getAutoCompleteList(),
             'filter':
                 api.settings.get('muc_mention_autocomplete_filter') == 'contains'
-                    ? _converse.FILTER_CONTAINS
-                    : _converse.FILTER_STARTSWITH,
+                    ? FILTER_CONTAINS
+                    : FILTER_STARTSWITH,
             'ac_triggers': ['Tab', '@'],
             'include_triggers': [],
             'item': getAutoCompleteListItem
@@ -64,7 +66,7 @@ export default class MUCMessageForm extends MessageForm {
     }
 
     /**
-     * @param {Event} ev
+     * @param {KeyboardEvent} ev
      */
     onKeyUp (ev) {
         if (this.shouldAutoComplete()) this.mention_auto_complete.evaluate(ev);

+ 1 - 1
src/plugins/muc-views/modals/add-muc.js

@@ -38,7 +38,7 @@ export default class AddMUCModal extends BaseModal {
         const jid = /** @type {string} */ (data.get('chatroom'))?.trim();
         let nick;
         if (api.settings.get('locked_muc_nickname')) {
-            nick = _converse.getDefaultMUCNickname();
+            nick = _converse.exports.getDefaultMUCNickname();
             if (!nick) {
                 throw new Error('Using locked_muc_nickname but no nickname found!');
             }

+ 1 - 1
src/plugins/muc-views/modals/muc-invite.js

@@ -27,7 +27,7 @@ export default class MUCInviteModal extends BaseModal {
     }
 
     getAutoCompleteList () {
-        return _converse.roster.map((i) => ({ 'label': i.getDisplayName(), 'value': i.get('jid') }));
+        return _converse.state.roster.map((i) => ({ 'label': i.getDisplayName(), 'value': i.get('jid') }));
     }
 
     submitInviteForm (ev) {

+ 1 - 1
src/plugins/muc-views/modals/occupant.js

@@ -34,7 +34,7 @@ export default class OccupantModal extends BaseModal {
             return model.vcard;
         }
         const jid = model?.get('jid') || model?.get('from');
-        return jid ? _converse.vcards.get(jid) : null;
+        return jid ? _converse.state.vcards.get(jid) : null;
     }
 
     renderModal () {

+ 3 - 1
src/plugins/muc-views/modals/templates/occupant.js

@@ -22,7 +22,9 @@ export default (el) => {
     const i18n_add_to_contacts = __('Add to Contacts');
 
     const can_see_real_jids = muc.features.get('nonanonymous') || muc.getOwnRole() === 'moderator';
-    const not_me =  jid != _converse.bare_jid;
+
+    const bare_jid = _converse.session.get('bare_jid');
+    const not_me =  jid != bare_jid;
 
     const add_to_contacts = api.contacts.get(jid)
         .then(contact => !contact && not_me && can_see_real_jids)

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

@@ -20,7 +20,8 @@ class MUCNicknameForm extends CustomElement {
 
     connectedCallback () {
         super.connectedCallback();
-        this.model = _converse.chatboxes.get(this.jid);
+        const { chatboxes } = _converse.state;
+        this.model = chatboxes.get(this.jid);
     }
 
     render () {

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

@@ -16,7 +16,8 @@ class MUCPasswordForm extends CustomElement {
 
     connectedCallback () {
         super.connectedCallback();
-        this.model = _converse.chatboxes.get(this.jid);
+        const { chatboxes } = _converse.state;
+        this.model = chatboxes.get(this.jid);
         this.listenTo(this.model, 'change:password_validation_message', this.render);
         this.render();
     }

+ 8 - 1
src/plugins/muc-views/search.js

@@ -6,10 +6,14 @@ Strophe.addNamespace('MUCSEARCH', 'https://xmlns.zombofant.net/muclumbus/search/
 
 const rooms_cache = {};
 
+/**
+ * @param {string} query
+ */
 async function searchRooms (query) {
+    const bare_jid = _converse.session.get('bare_jid');
     const iq = $iq({
         'type': 'get',
-        'from': _converse.bare_jid,
+        'from': bare_jid,
         'to': 'api@search.jabber.network'
     }).c('search', { 'xmlns': Strophe.NS.MUCSEARCH })
         .c('set', { 'xmlns': Strophe.NS.RSM })
@@ -49,6 +53,9 @@ async function searchRooms (query) {
     });
 }
 
+/**
+ * @param {string} query
+ */
 export function getAutoCompleteList (query) {
     if (!rooms_cache[query]) {
         rooms_cache[query] = searchRooms(query);

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

@@ -2,8 +2,8 @@ import 'shared/autocomplete/index.js';
 import tplMUCSidebar from "./templates/muc-sidebar.js";
 import { CustomElement } from 'shared/components/element.js';
 import { _converse, api, converse } from "@converse/headless";
-import { RosterFilter } from 'headless/plugins/roster/filter.js';
-import { initStorage } from "headless/utils/storage";
+import { RosterFilter } from '@converse/headless/plugins/roster/filter.js';
+import { initStorage } from "@converse/headless/utils/storage";
 import debounce from 'lodash-es/debounce.js';
 
 import 'shared/styles/status.scss';
@@ -31,7 +31,8 @@ export default class MUCSidebar extends CustomElement {
         initStorage(this.filter, filter_id);
         this.filter.fetch();
 
-        this.model = _converse.chatboxes.get(this.jid);
+        const { chatboxes } = _converse.state;
+        this.model = chatboxes.get(this.jid);
 
         // To avoid rendering continuously the participant list in case of massive joins/leaves:
         const debouncedRequestUpdate = debounce(() => this.requestUpdate(), 200, {
@@ -66,7 +67,8 @@ export default class MUCSidebar extends CustomElement {
 
     onOccupantClicked (ev) {
         ev?.preventDefault?.();
-        const view = _converse.chatboxviews.get(this.getAttribute('jid'));
+        const { chatboxviews } = _converse.state;
+        const view = chatboxviews.get(this.getAttribute('jid'));
         view?.getMessageForm().insertIntoTextArea(`@${ev.target.textContent}`);
     }
 }

+ 3 - 2
src/plugins/muc-views/utils.js

@@ -120,8 +120,9 @@ export function getAutoCompleteListItem (text, input) {
         const img = document.createElement('img');
         let dataUri = 'data:' + _converse.DEFAULT_IMAGE_TYPE + ';base64,' + _converse.DEFAULT_IMAGE;
 
-        if (_converse.vcards) {
-            const vcard = _converse.vcards.findWhere({ 'nickname': text });
+        const { vcards } = _converse.state;
+        if (vcards) {
+            const vcard = vcards.findWhere({ 'nickname': text });
             if (vcard) dataUri = 'data:' + vcard.get('image_type') + ';base64,' + vcard.get('image');
         }
 

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

@@ -35,9 +35,10 @@ converse.plugins.add('converse-notification', {
         /************************ Event Handlers ************************/
         api.listen.on('clearSession', clearFavicon); // Needed for tests
 
-        api.waitUntil('chatBoxesInitialized').then(() =>
-            _converse.chatboxes.on('change:num_unread', updateUnreadFavicon)
-        );
+        api.waitUntil('chatBoxesInitialized').then(() => {
+            const { chatboxes } = _converse.state;
+            chatboxes.on('change:num_unread', updateUnreadFavicon)
+        });
 
         api.listen.on('pluginsInitialized', function () {
             // We only register event handlers after all plugins are

+ 16 - 13
src/plugins/notifications/utils.js

@@ -4,7 +4,7 @@
  * @typedef {module:headless-plugins-chat-utils.MessageData} MessageData
  */
 import Favico from 'favico.js-slevomat';
-import { __ } from 'i18n';
+import { __, i18n } from 'i18n';
 import { _converse, api, converse, log } from '@converse/headless';
 import { isEmptyMessage } from '@converse/headless/utils/index.js';
 import { isTestEnv } from '@converse/headless/utils/session.js';
@@ -18,7 +18,7 @@ let favicon;
 
 
 export function isMessageToHiddenChat (attrs) {
-    return isTestEnv() || (_converse.chatboxes.get(attrs.from)?.isHidden() ?? false);
+    return isTestEnv() || (_converse.state.chatboxes.get(attrs.from)?.isHidden() ?? false);
 }
 
 export function areDesktopNotificationsEnabled () {
@@ -42,7 +42,7 @@ export function clearFavicon () {
 export function updateUnreadFavicon () {
     if (api.settings.get('show_tab_notifications')) {
         favicon = favicon ?? new converse.env.Favico({ type: 'circle', animation: 'pop' });
-        const chats = _converse.chatboxes.models;
+        const chats = _converse.state.chatboxes.models;
         const num_unread = chats.reduce((acc, chat) => acc + (chat.get('num_unread') || 0), 0);
         favicon.badge(num_unread);
         /** @type navigator */(navigator).setAppBadge?.(num_unread)
@@ -52,7 +52,8 @@ export function updateUnreadFavicon () {
 
 
 function isReferenced (references, muc_jid, nick) {
-    const check = r => [_converse.bare_jid, `${muc_jid}/${nick}`].includes(r.uri.replace(/^xmpp:/, ''));
+    const bare_jid = _converse.session.get('bare_jid');
+    const check = r => [bare_jid, `${muc_jid}/${nick}`].includes(r.uri.replace(/^xmpp:/, ''));
     return references.reduce((acc, r) => acc || check(r), false);
 }
 
@@ -69,7 +70,7 @@ export async function shouldNotifyOfGroupMessage (attrs) {
     const jid = attrs.from;
     const muc_jid = attrs.from_muc;
     const notify_all = api.settings.get('notify_all_room_messages');
-    const room = _converse.chatboxes.get(muc_jid);
+    const room = _converse.state.chatboxes.get(muc_jid);
     const resource = Strophe.getResourceFromJid(jid);
     const sender = (resource && Strophe.unescapeNode(resource)) || '';
     let is_mentioned = false;
@@ -139,7 +140,9 @@ function shouldNotifyOfMessage (data) {
         // We want to show notifications for headline messages.
         return isMessageToHiddenChat(attrs);
     }
-    const is_me = Strophe.getBareJidFromJid(attrs.from) === _converse.bare_jid;
+
+    const bare_jid = _converse.session.get('bare_jid');
+    const is_me = Strophe.getBareJidFromJid(attrs.from) === bare_jid;
     return (
         !isEmptyMessage(attrs) &&
         !is_me &&
@@ -151,7 +154,7 @@ export function showFeedbackNotification (data) {
     if (data.klass === 'error' || data.klass === 'warn') {
         const n = new Notification(data.subject, {
             body: data.message,
-            lang: _converse.locale,
+            lang: i18n.getLocale(),
             icon: api.settings.get('notification_icon')
         });
         setTimeout(n.close.bind(n), 5000);
@@ -183,7 +186,7 @@ function showChatStateNotification (contact) {
     }
     const n = new Notification(contact.getDisplayName(), {
         body: message,
-        lang: _converse.locale,
+        lang: i18n.getLocale(),
         icon: api.settings.get('notification_icon')
     });
     setTimeout(() => n.close(), 5000);
@@ -221,11 +224,11 @@ function showMessageNotification (data) {
     } else if (attrs.type === 'groupchat') {
         title = __('%1$s says', Strophe.getResourceFromJid(full_from_jid));
     } else {
-        if (_converse.roster === undefined) {
+        if (_converse.state.roster === undefined) {
             log.error('Could not send notification, because roster is undefined');
             return;
         }
-        roster_item = _converse.roster.get(from_jid);
+        roster_item = _converse.state.roster.get(from_jid);
         if (roster_item !== undefined) {
             title = __('%1$s says', roster_item.getDisplayName());
         } else {
@@ -249,7 +252,7 @@ function showMessageNotification (data) {
 
     const n = new Notification(title, {
         'body': body,
-        'lang': _converse.locale,
+        'lang': i18n.getLocale(),
         'icon': api.settings.get('notification_icon'),
         'requireInteraction': !api.settings.get('notification_delay')
     });
@@ -259,7 +262,7 @@ function showMessageNotification (data) {
     n.onclick = function (event) {
         event.preventDefault();
         window.focus();
-        const chat = _converse.chatboxes.get(from_jid);
+        const chat = _converse.state.chatboxes.get(from_jid);
         chat.maybeShow(true);
     }
 }
@@ -322,7 +325,7 @@ export function handleChatStateNotification (contact) {
 function showContactRequestNotification (contact) {
     const n = new Notification(contact.getDisplayName(), {
         body: __('wants to be your contact'),
-        lang: _converse.locale,
+        lang: i18n.getLocale(),
         icon: api.settings.get('notification_icon')
     });
     setTimeout(() => n.close(), 5000);

+ 11 - 9
src/plugins/omemo/api.js

@@ -15,7 +15,7 @@ export default {
          */
         async getDeviceID () {
             await api.waitUntil('OMEMOInitialized');
-            return _converse.omemo_store.get('device_id');
+            return _converse.state.omemo_store.get('device_id');
         },
 
         /**
@@ -34,8 +34,8 @@ export default {
              *      should be created if it cannot be found.
              */
             async get (jid, create=false) {
-                const list = _converse.devicelists.get(jid) ||
-                    (create ? _converse.devicelists.create({ jid }) : null);
+                const list = _converse.state.devicelists.get(jid) ||
+                    (create ? _converse.state.devicelists.create({ jid }) : null);
                 await list?.initialized;
                 return list;
             }
@@ -58,12 +58,14 @@ export default {
             'generate': async () => {
                 await api.waitUntil('OMEMOInitialized');
                 // Remove current device
-                const devicelist = await api.omemo.devicelists.get(_converse.bare_jid);
+                const bare_jid = _converse.session.get('bare_jid');
+                const devicelist = await api.omemo.devicelists.get(bare_jid);
 
-                const device_id = _converse.omemo_store.get('device_id');
+                const { omemo_store } = _converse.state;
+                const device_id = omemo_store.get('device_id');
                 if (device_id) {
                     const device = devicelist.devices.get(device_id);
-                    _converse.omemo_store.unset(device_id);
+                    omemo_store.unset(device_id);
                     if (device) {
                         await new Promise(done => device.destroy({ 'success': done, 'error': done }));
                     }
@@ -71,11 +73,11 @@ export default {
                 }
                 // Generate new device bundle and publish
                 // https://xmpp.org/extensions/attic/xep-0384-0.3.0.html#usecases-announcing
-                await _converse.omemo_store.generateBundle();
+                await omemo_store.generateBundle();
                 await devicelist.publishDevices();
-                const device = devicelist.devices.get(_converse.omemo_store.get('device_id'));
+                const device = devicelist.devices.get(omemo_store.get('device_id'));
                 const fp = generateFingerprint(device);
-                await _converse.omemo_store.publishBundle();
+                await omemo_store.publishBundle();
                 return fp;
             }
         }

+ 2 - 1
src/plugins/omemo/device.js

@@ -26,9 +26,10 @@ class Device extends Model {
     }
 
     async fetchBundleFromServer () {
+        const bare_jid = _converse.session.get('bare_jid');
         const stanza = $iq({
             'type': 'get',
-            'from': _converse.bare_jid,
+            'from': bare_jid,
             'to': this.get('jid')
         }).c('pubsub', { 'xmlns': Strophe.NS.PUBSUB })
           .c('items', { 'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}` });

+ 16 - 10
src/plugins/omemo/devicelist.js

@@ -23,8 +23,9 @@ class DeviceList extends Model {
     }
 
     initDevices () {
-        this.devices = new _converse.Devices();
-        const id = `converse.devicelist-${_converse.bare_jid}-${this.get('jid')}`;
+        this.devices = new _converse.exports.Devices();
+        const bare_jid = _converse.session.get('bare_jid');
+        const id = `converse.devicelist-${bare_jid}-${this.get('jid')}`;
         initStorage(this.devices, id);
         return this.fetchDevices();
     }
@@ -43,7 +44,8 @@ class DeviceList extends Model {
                 }
                 this.destroy();
             }
-            if (this.get('jid') === _converse.bare_jid) {
+            const bare_jid = _converse.session.get('bare_jid');
+            if (this.get('jid') === bare_jid) {
                 this.publishCurrentDevice(ids);
             }
         }
@@ -65,22 +67,24 @@ class DeviceList extends Model {
     }
 
     async getOwnDeviceId () {
-        let device_id = _converse.omemo_store.get('device_id');
+        const { omemo_store } = _converse.state;
+        let device_id = omemo_store.get('device_id');
         if (!this.devices.get(device_id)) {
             // Generate a new bundle if we cannot find our device
-            await _converse.omemo_store.generateBundle();
-            device_id = _converse.omemo_store.get('device_id');
+            await omemo_store.generateBundle();
+            device_id = omemo_store.get('device_id');
         }
         return device_id;
     }
 
     async publishCurrentDevice (device_ids) {
-        if (this.get('jid') !== _converse.bare_jid) {
+        const bare_jid = _converse.session.get('bare_jid');
+        if (this.get('jid') !== bare_jid) {
             return; // We only publish for ourselves.
         }
         await restoreOMEMOSession();
 
-        if (!_converse.omemo_store) {
+        if (!_converse.state.omemo_store) {
             // Happens during tests. The connection gets torn down
             // before publishCurrentDevice has time to finish.
             log.warn('publishCurrentDevice: omemo_store is not defined, likely a timing issue');
@@ -92,9 +96,10 @@ class DeviceList extends Model {
     }
 
     async fetchDevicesFromServer () {
+        const bare_jid = _converse.session.get('bare_jid');
         const stanza = $iq({
             'type': 'get',
-            'from': _converse.bare_jid,
+            'from': bare_jid,
             'to': this.get('jid')
         }).c('pubsub', { 'xmlns': Strophe.NS.PUBSUB })
           .c('items', { 'node': Strophe.NS.OMEMO_DEVICELIST });
@@ -119,7 +124,8 @@ class DeviceList extends Model {
     }
 
     async removeOwnDevices (device_ids) {
-        if (this.get('jid') !== _converse.bare_jid) {
+        const bare_jid = _converse.session.get('bare_jid');
+        if (this.get('jid') !== bare_jid) {
             throw new Error("Cannot remove devices from someone else's device list");
         }
         await Promise.all(device_ids.map(id => this.devices.get(id)).map(d =>

+ 18 - 15
src/plugins/omemo/index.js

@@ -8,7 +8,6 @@
 import './fingerprints.js';
 import './profile.js';
 import 'shared/modals/user-details.js';
-import ConverseMixins from './mixins/converse.js';
 import Device from './device.js';
 import DeviceList from './devicelist.js';
 import DeviceLists from './devicelists.js';
@@ -20,6 +19,7 @@ import { shouldClearCache } from '@converse/headless/utils/session.js';
 import {
     createOMEMOMessageStanza,
     encryptFile,
+    generateFingerprints,
     getOMEMOToolbarButton,
     getOutgoingMessageAttributes,
     handleEncryptedFiles,
@@ -59,16 +59,18 @@ converse.plugins.add('converse-omemo', {
         api.settings.extend({ 'omemo_default': false });
         api.promises.add(['OMEMOInitialized']);
 
-        _converse.NUM_PREKEYS = 100; // Set here so that tests can override
-
-        Object.assign(_converse, ConverseMixins);
         Object.assign(_converse.api, omemo_api);
 
-        _converse.OMEMOStore = OMEMOStore;
-        _converse.Device = Device;
-        _converse.Devices = Devices;
-        _converse.DeviceList = DeviceList;
-        _converse.DeviceLists = DeviceLists;
+        const exports = {
+            OMEMOStore,
+            Device,
+            Devices,
+            DeviceList,
+            DeviceLists,
+        };
+
+        Object.assign(_converse, exports); // DEPRECATED
+        Object.assign(_converse.exports, exports);
 
         /******************** Event Handlers ********************/
         api.waitUntil('chatBoxesInitialized').then(onChatBoxesInitialized);
@@ -103,18 +105,19 @@ converse.plugins.add('converse-omemo', {
 
         api.listen.on('userDetailsModalInitialized', contact => {
             const jid = contact.get('jid');
-            _converse.generateFingerprints(jid).catch(e => log.error(e));
+            generateFingerprints(jid).catch(e => log.error(e));
         });
 
         api.listen.on('profileModalInitialized', () => {
-            _converse.generateFingerprints(_converse.bare_jid).catch(e => log.error(e));
+            const bare_jid = _converse.session.get('bare_jid');
+            generateFingerprints(bare_jid).catch(e => log.error(e));
         });
 
         api.listen.on('clearSession', () => {
-            delete _converse.omemo_store
-            if (shouldClearCache() && _converse.devicelists) {
-                _converse.devicelists.clearStore();
-                delete _converse.devicelists;
+            delete _converse.state.omemo_store
+            if (shouldClearCache() && _converse.state.devicelists) {
+                _converse.state.devicelists.clearStore();
+                delete _converse.state.devicelists;
             }
         });
     }

+ 0 - 22
src/plugins/omemo/mixins/converse.js

@@ -1,22 +0,0 @@
-import { generateFingerprint, getDevicesForContact, } from '../utils.js';
-
-
-const ConverseMixins = {
-
-    async generateFingerprints (jid) {
-        const devices = await getDevicesForContact(jid);
-        return Promise.all(devices.map(d => generateFingerprint(d)));
-    },
-
-    getDeviceForContact (jid, device_id) {
-        return getDevicesForContact(jid).then(devices => devices.get(device_id));
-    },
-
-    async contactHasOMEMOSupport (jid) {
-        /* Checks whether the contact advertises any OMEMO-compatible devices. */
-        const devices = await getDevicesForContact(jid);
-        return devices.length > 0;
-    }
-}
-
-export default ConverseMixins;

+ 2 - 1
src/plugins/omemo/profile.js

@@ -10,7 +10,8 @@ const { Strophe, sizzle, u } = converse.env;
 export class Profile extends CustomElement {
 
     async initialize () {
-        this.devicelist = await api.omemo.devicelists.get(_converse.bare_jid, true);
+        const bare_jid = _converse.session.get('bare_jid');
+        this.devicelist = await api.omemo.devicelists.get(bare_jid, true);
         await this.setAttributes();
         this.listenTo(this.devicelist.devices, 'change:bundle', () => this.requestUpdate());
         this.listenTo(this.devicelist.devices, 'reset', () => this.requestUpdate());

+ 3 - 2
src/plugins/omemo/store.js

@@ -287,9 +287,10 @@ class OMEMOStore extends Model {
             'signature': u.arrayBufferToBase64(signed_prekey.signature)
         };
 
-        const devicelist = await api.omemo.devicelists.get(_converse.bare_jid);
+        const bare_jid = _converse.session.get('bare_jid');
+        const devicelist = await api.omemo.devicelists.get(bare_jid);
         const device = await devicelist.devices.create(
-            { 'id': bundle.device_id, 'jid': _converse.bare_jid },
+            { 'id': bundle.device_id, 'jid': bare_jid },
             { 'promise': true }
         );
         device.save('bundle', bundle);

+ 12 - 12
src/plugins/omemo/tests/corrections.js

@@ -22,8 +22,8 @@ describe("An OMEMO encrypted message", function() {
                         .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                             .c('device', {'id': '555'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
-        const devicelist = _converse.devicelists.get({'jid': contact_jid});
+        await u.waitUntil(() => _converse.state.omemo_store);
+        const devicelist = _converse.state.devicelists.get({'jid': contact_jid});
         await u.waitUntil(() => devicelist.devices.length === 1);
 
         const view = _converse.chatboxviews.get(contact_jid);
@@ -166,7 +166,7 @@ describe("An OMEMO encrypted message", function() {
                 .c('origin-id', {'id': first_rcvd_msg_id, 'xmlns': 'urn:xmpp:sid:0'}).up()
                 .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                     .c('header', {'sid':  '555'})
-                        .c('key', {'rid':  _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
+                        .c('key', {'rid':  _converse.state.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
                         .c('iv').t(obj.iv)
                         .up().up()
                     .c('payload').t(obj.payload)));
@@ -187,7 +187,7 @@ describe("An OMEMO encrypted message", function() {
                 .c('origin-id', {'id': msg_id, 'xmlns': 'urn:xmpp:sid:0'}).up()
                 .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                     .c('header', {'sid':  '555'})
-                        .c('key', {'rid':  _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
+                        .c('key', {'rid':  _converse.state.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
                         .c('iv').t(obj.iv)
                         .up().up()
                     .c('payload').t(obj.payload)));
@@ -271,11 +271,11 @@ describe("An OMEMO encrypted MUC message", function() {
                     .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                         .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
-        expect(_converse.devicelists.length).toBe(2);
+        await u.waitUntil(() => _converse.state.omemo_store);
+        expect(_converse.state.devicelists.length).toBe(2);
 
         await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
-        const devicelist = _converse.devicelists.get(contact_jid);
+        const devicelist = _converse.state.devicelists.get(contact_jid);
         expect(devicelist.devices.length).toBe(1);
         expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
         expect(view.model.get('omemo_active')).toBe(true);
@@ -419,7 +419,7 @@ describe("An OMEMO encrypted MUC message", function() {
             }).c('body').t(fallback_text).up()
                 .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                     .c('header', {'sid':  '555'})
-                        .c('key', {'rid':  _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(first_obj.key_and_tag)).up()
+                        .c('key', {'rid':  _converse.state.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(first_obj.key_and_tag)).up()
                         .c('iv').t(first_obj.iv)
                         .up().up()
                     .c('payload').t(first_obj.payload)));
@@ -427,9 +427,9 @@ describe("An OMEMO encrypted MUC message", function() {
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
         expect(view.model.messages.length).toBe(2);
         expect(view.querySelectorAll('.chat-msg__body')[1].textContent.trim()).toBe(first_received_message);
-        expect(_converse.devicelists.length).toBe(2);
-        expect(_converse.devicelists.at(0).get('jid')).toBe(_converse.bare_jid);
-        expect(_converse.devicelists.at(1).get('jid')).toBe(contact_jid);
+        expect(_converse.state.devicelists.length).toBe(2);
+        expect(_converse.state.devicelists.at(0).get('jid')).toBe(_converse.bare_jid);
+        expect(_converse.state.devicelists.at(1).get('jid')).toBe(contact_jid);
 
         const second_received_message = 'This is an edited encrypted message from the contact';
         const second_obj = await omemo.encryptMessage(second_received_message)
@@ -442,7 +442,7 @@ describe("An OMEMO encrypted MUC message", function() {
                 .c('replace',  {'id':first_received_id, 'xmlns': 'urn:xmpp:message-correct:0'})
                 .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                     .c('header', {'sid':  '555'})
-                        .c('key', {'rid':  _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(second_obj.key_and_tag)).up()
+                        .c('key', {'rid':  _converse.state.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(second_obj.key_and_tag)).up()
                         .c('iv').t(second_obj.iv)
                         .up().up()
                     .c('payload').t(second_obj.payload)));

+ 2 - 2
src/plugins/omemo/tests/media-sharing.js

@@ -38,8 +38,8 @@ describe("The OMEMO module", function() {
                         .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                             .c('device', {'id': '555'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
-        const devicelist = _converse.devicelists.get({'jid': contact_jid});
+        await u.waitUntil(() => _converse.state.omemo_store);
+        const devicelist = _converse.state.devicelists.get({'jid': contact_jid});
         await u.waitUntil(() => devicelist.devices.length === 1);
 
         const view = _converse.chatboxviews.get(contact_jid);

+ 13 - 13
src/plugins/omemo/tests/muc.js

@@ -65,11 +65,11 @@ describe("The OMEMO module", function() {
                     .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                         .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
-        expect(_converse.devicelists.length).toBe(2);
+        await u.waitUntil(() => _converse.state.omemo_store);
+        expect(_converse.state.devicelists.length).toBe(2);
 
         await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
-        const devicelist = _converse.devicelists.get(contact_jid);
+        const devicelist = _converse.state.devicelists.get(contact_jid);
         expect(devicelist.devices.length).toBe(1);
         expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
         expect(view.model.get('omemo_active')).toBe(true);
@@ -163,7 +163,7 @@ describe("The OMEMO module", function() {
             }).c('body').t('This is a fallback message').up()
                 .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                     .c('header', {'sid':  '555'})
-                        .c('key', {'rid':  _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
+                        .c('key', {'rid':  _converse.state.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
                         .c('iv').t(obj.iv)
                         .up().up()
                     .c('payload').t(obj.payload);
@@ -173,9 +173,9 @@ describe("The OMEMO module", function() {
         expect(view.querySelectorAll('.chat-msg__body')[1].textContent.trim())
             .toBe('This is an encrypted message from the contact');
 
-        expect(_converse.devicelists.length).toBe(2);
-        expect(_converse.devicelists.at(0).get('jid')).toBe(_converse.bare_jid);
-        expect(_converse.devicelists.at(1).get('jid')).toBe(contact_jid);
+        expect(_converse.state.devicelists.length).toBe(2);
+        expect(_converse.state.devicelists.at(0).get('jid')).toBe(_converse.bare_jid);
+        expect(_converse.state.devicelists.at(1).get('jid')).toBe(contact_jid);
     }));
 
     it("gracefully handles auth errors when trying to send encrypted groupchat messages",
@@ -244,10 +244,10 @@ describe("The OMEMO module", function() {
                         .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
 
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
-        expect(_converse.devicelists.length).toBe(2);
+        await u.waitUntil(() => _converse.state.omemo_store);
+        expect(_converse.state.devicelists.length).toBe(2);
 
-        const devicelist = _converse.devicelists.get(contact_jid);
+        const devicelist = _converse.state.devicelists.get(contact_jid);
         await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
         expect(devicelist.devices.length).toBe(1);
         expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
@@ -383,11 +383,11 @@ describe("The OMEMO module", function() {
                         .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
                         .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
-        expect(_converse.devicelists.length).toBe(2);
+        await u.waitUntil(() => _converse.state.omemo_store);
+        expect(_converse.state.devicelists.length).toBe(2);
 
         await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
-        const devicelist = _converse.devicelists.get(contact_jid);
+        const devicelist = _converse.state.devicelists.get(contact_jid);
         expect(devicelist.devices.length).toBe(2);
         expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
         expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');

+ 47 - 45
src/plugins/omemo/tests/omemo.js

@@ -35,8 +35,8 @@ describe("The OMEMO module", function() {
                         .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                             .c('device', {'id': '555'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
-        const devicelist = _converse.devicelists.get({'jid': contact_jid});
+        await u.waitUntil(() => _converse.state.omemo_store);
+        const devicelist = _converse.state.devicelists.get({'jid': contact_jid});
         await u.waitUntil(() => devicelist.devices.length === 1);
 
         const view = _converse.chatboxviews.get(contact_jid);
@@ -123,7 +123,7 @@ describe("The OMEMO module", function() {
             }).c('body').t('This is a fallback message').up()
                 .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                     .c('header', {'sid':  '555'})
-                        .c('key', {'rid':  _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
+                        .c('key', {'rid':  _converse.state.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
                         .c('iv').t(obj.iv)
                         .up().up()
                     .c('payload').t(obj.payload);
@@ -142,7 +142,7 @@ describe("The OMEMO module", function() {
                 'id': _converse.api.connection.get().getUniqueId()
             }).c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                 .c('header', {'sid':  '555'})
-                    .c('key', {'rid':  _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
+                    .c('key', {'rid':  _converse.state.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
                     .c('iv').t(obj.iv)
                     .up().up()
                 .c('payload').t(obj.payload);
@@ -174,7 +174,7 @@ describe("The OMEMO module", function() {
                             .c('device', {'id': '555'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
-        await u.waitUntil(() => _converse.omemo_store);
+        await u.waitUntil(() => _converse.state.omemo_store);
 
         const view = _converse.chatboxviews.get(contact_jid);
         view.model.set('omemo_active', true);
@@ -191,7 +191,7 @@ describe("The OMEMO module", function() {
             }).c('body').t('This is a fallback message').up()
                 .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                     .c('header', {'sid':  '555'})
-                        .c('key', {'rid':  _converse.omemo_store.get('device_id')})
+                        .c('key', {'rid':  _converse.state.omemo_store.get('device_id')})
                             .t(u.arrayBufferToBase64(obj.key_and_tag)).up()
                         .c('iv').t(obj.iv)
                         .up().up()
@@ -209,7 +209,7 @@ describe("The OMEMO module", function() {
             }).c('body').t('This is a fallback message').up()
                 .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                     .c('header', {'sid':  '555'})
-                        .c('key', {'rid':  _converse.omemo_store.get('device_id')})
+                        .c('key', {'rid':  _converse.state.omemo_store.get('device_id')})
                             .t(u.arrayBufferToBase64(obj.key_and_tag)).up()
                         .c('iv').t(obj.iv)
                         .up().up()
@@ -236,7 +236,7 @@ describe("The OMEMO module", function() {
         await mock.openChatBoxFor(_converse, contact_jid);
 
         let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
-        const my_devicelist = _converse.devicelists.get({'jid': _converse.bare_jid});
+        const my_devicelist = _converse.state.devicelists.get({'jid': _converse.bare_jid});
         expect(my_devicelist.devices.length).toBe(2);
 
         const stanza = $iq({
@@ -250,11 +250,9 @@ describe("The OMEMO module", function() {
                         .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                             .c('device', {'id': '555'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
+        const omemo_store = await u.waitUntil(() => _converse.state.omemo_store);
 
-        const { omemo_store } = _converse;
-
-        const contact_devicelist = _converse.devicelists.get({'jid': contact_jid});
+        const contact_devicelist = _converse.state.devicelists.get({'jid': contact_jid});
         await u.waitUntil(() => contact_devicelist.devices.length === 1);
 
         const view = _converse.chatboxviews.get(contact_jid);
@@ -371,20 +369,20 @@ describe("The OMEMO module", function() {
                     .c('header', {'sid':  '555'})
                         .c('key', {
                             'prekey': 'true',
-                            'rid':  _converse.omemo_store.get('device_id')
+                            'rid':  _converse.state.omemo_store.get('device_id')
                         }).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
                         .c('iv').t(obj.iv)
                         .up().up()
                     .c('payload').t(obj.payload);
 
-        const generateMissingPreKeys = _converse.omemo_store.generateMissingPreKeys;
-        spyOn(_converse.omemo_store, 'generateMissingPreKeys').and.callFake(() => {
+        const generateMissingPreKeys = _converse.state.omemo_store.generateMissingPreKeys;
+        spyOn(_converse.state.omemo_store, 'generateMissingPreKeys').and.callFake(() => {
             // Since it's difficult to override
             // decryptPreKeyWhisperMessage, where a prekey will be
             // removed from the store, we do it here, before the
             // missing prekeys are generated.
-            _converse.omemo_store.removePreKey(1);
-            return generateMissingPreKeys.apply(_converse.omemo_store, arguments);
+            _converse.state.omemo_store.removePreKey(1);
+            return generateMissingPreKeys.apply(_converse.state.omemo_store, arguments);
         });
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
@@ -405,7 +403,7 @@ describe("The OMEMO module", function() {
         // stanzas.
         _converse.api.connection.get().IQ_stanzas = [];
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
+        await u.waitUntil(() => _converse.state.omemo_store);
         iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse), 1000);
         expect(Strophe.serialize(iq_stanza)).toBe(
             `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" type="set" xmlns="jabber:client">`+
@@ -438,9 +436,11 @@ describe("The OMEMO module", function() {
                     `</publish-options>`+
                 `</pubsub>`+
             `</iq>`)
-        const own_device = _converse.devicelists.get(_converse.bare_jid).devices.get(_converse.omemo_store.get('device_id'));
+        const own_device = _converse.state.devicelists.get(_converse.bare_jid).devices.get(_converse.state.omemo_store.get('device_id'));
         expect(own_device.get('bundle').prekeys.length).toBe(5);
-        expect(_converse.omemo_store.generateMissingPreKeys).toHaveBeenCalled();
+        expect(_converse.state.omemo_store.generateMissingPreKeys).toHaveBeenCalled();
+
+        _converse.NUM_PREKEYS = 100;
     }));
 
     it("updates device lists based on PEP messages",
@@ -475,10 +475,10 @@ describe("The OMEMO module", function() {
                     .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                         .c('device', {'id': '555'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
+        await u.waitUntil(() => _converse.state.omemo_store);
         expect(_converse.chatboxes.length).toBe(1);
-        expect(_converse.devicelists.length).toBe(1);
-        const devicelist = _converse.devicelists.get(_converse.bare_jid);
+        expect(_converse.state.devicelists.length).toBe(1);
+        const devicelist = _converse.state.devicelists.get(_converse.bare_jid);
         expect(devicelist.devices.length).toBe(2);
         expect(devicelist.devices.at(0).get('id')).toBe('555');
         expect(devicelist.devices.at(1).get('id')).toBe('123456789');
@@ -535,9 +535,9 @@ describe("The OMEMO module", function() {
                             .c('device', {'id': '4223'})
         ));
 
-        await u.waitUntil(() => _converse.devicelists.length === 2);
+        await u.waitUntil(() => _converse.state.devicelists.length === 2);
 
-        const list = _converse.devicelists.get(contact_jid);
+        const list = _converse.state.devicelists.get(contact_jid);
         await list.initialized;
         await u.waitUntil(() => list.devices.length === 2);
 
@@ -558,7 +558,7 @@ describe("The OMEMO module", function() {
                         .c('device', {'id': '4224'})
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
-        expect(_converse.devicelists.length).toBe(2);
+        expect(_converse.state.devicelists.length).toBe(2);
         await u.waitUntil(() => list.devices.length === 3);
         expect(devices.models.map(d => d.attributes.id).sort().join()).toBe('1234,4223,4224');
         expect(devices.get('1234').get('active')).toBe(false);
@@ -580,8 +580,8 @@ describe("The OMEMO module", function() {
                         .c('device', {'id': '777'})
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
-        expect(_converse.devicelists.length).toBe(2);
-        devices = _converse.devicelists.get(_converse.bare_jid).devices;
+        expect(_converse.state.devicelists.length).toBe(2);
+        devices = _converse.state.devicelists.get(_converse.bare_jid).devices;
         await u.waitUntil(() => devices.length === 3);
         expect(devices.models.map(d => d.attributes.id).sort().join()).toBe('123456789,555,777');
         expect(devices.get('123456789').get('active')).toBe(true);
@@ -629,8 +629,8 @@ describe("The OMEMO module", function() {
                     `</publish-options>`+
                 `</pubsub>`+
             `</iq>`);
-        expect(_converse.devicelists.length).toBe(2);
-        devices = _converse.devicelists.get(_converse.bare_jid).devices;
+        expect(_converse.state.devicelists.length).toBe(2);
+        devices = _converse.state.devicelists.get(_converse.bare_jid).devices;
         // The device id for this device (123456789) was also generated and added to the list,
         // which is why we have 2 devices now.
         expect(devices.length).toBe(4);
@@ -674,9 +674,9 @@ describe("The OMEMO module", function() {
                         .c('device', {'id': '555'})
         ));
 
-        await await u.waitUntil(() => _converse.omemo_store);
-        expect(_converse.devicelists.length).toBe(1);
-        const own_device_list = _converse.devicelists.get(_converse.bare_jid);
+        await await u.waitUntil(() => _converse.state.omemo_store);
+        expect(_converse.state.devicelists.length).toBe(1);
+        const own_device_list = _converse.state.devicelists.get(_converse.bare_jid);
         expect(own_device_list.devices.length).toBe(2);
         expect(own_device_list.devices.at(0).get('id')).toBe('555');
         expect(own_device_list.devices.at(1).get('id')).toBe('123456789');
@@ -731,8 +731,8 @@ describe("The OMEMO module", function() {
                             .c('device', {'id': '1234'})
         ));
 
-        await u.waitUntil(() => _converse.devicelists.length === 2);
-        const list = _converse.devicelists.get(contact_jid);
+        await u.waitUntil(() => _converse.state.devicelists.length === 2);
+        const list = _converse.state.devicelists.get(contact_jid);
         await list.initialized;
         await u.waitUntil(() => list.devices.length);
         let device = list.devices.at(0);
@@ -763,7 +763,7 @@ describe("The OMEMO module", function() {
                             .c('preKeyPublic', {'preKeyId': '2003'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
-        expect(_converse.devicelists.length).toBe(2);
+        expect(_converse.state.devicelists.length).toBe(2);
         expect(list.devices.length).toBe(1);
         device = list.devices.at(0);
 
@@ -794,7 +794,7 @@ describe("The OMEMO module", function() {
                             .c('preKeyPublic', {'preKeyId': '3003'})
         ));
 
-        expect(_converse.devicelists.length).toBe(2);
+        expect(_converse.state.devicelists.length).toBe(2);
         expect(own_device_list.devices.length).toBe(2);
         expect(own_device_list.devices.at(0).get('id')).toBe('555');
         expect(own_device_list.devices.at(1).get('id')).toBe('123456789');
@@ -834,7 +834,7 @@ describe("The OMEMO module", function() {
                     .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                         .c('device', {'id': '482886413b977930064a5888b92134fe'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        expect(_converse.devicelists.length).toBe(1);
+        expect(_converse.state.devicelists.length).toBe(1);
         await mock.openChatBoxFor(_converse, contact_jid);
         iq_stanza = await mock.ownDeviceHasBeenPublished(_converse);
         stanza = $iq({
@@ -881,6 +881,8 @@ describe("The OMEMO module", function() {
             'type': 'result'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
         await _converse.api.waitUntil('OMEMOInitialized');
+
+        _converse.NUM_PREKEYS = 100;
     }));
 
 
@@ -915,9 +917,9 @@ describe("The OMEMO module", function() {
                     .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                         .c('device', {'id': '482886413b977930064a5888b92134fe'});
         _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => _converse.omemo_store);
-        expect(_converse.devicelists.length).toBe(1);
-        let devicelist = _converse.devicelists.get(_converse.bare_jid);
+        await u.waitUntil(() => _converse.state.omemo_store);
+        expect(_converse.state.devicelists.length).toBe(1);
+        let devicelist = _converse.state.devicelists.get(_converse.bare_jid);
         expect(devicelist.devices.length).toBe(2);
         expect(devicelist.devices.at(0).get('id')).toBe('482886413b977930064a5888b92134fe');
         expect(devicelist.devices.at(1).get('id')).toBe('123456789');
@@ -997,10 +999,10 @@ describe("The OMEMO module", function() {
                         .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'})
         ));
 
-        devicelist = _converse.devicelists.get(contact_jid);
+        devicelist = _converse.state.devicelists.get(contact_jid);
         await u.waitUntil(() => devicelist.devices.length);
-        expect(_converse.devicelists.length).toBe(2);
-        devicelist = _converse.devicelists.get(contact_jid);
+        expect(_converse.state.devicelists.length).toBe(2);
+        devicelist = _converse.state.devicelists.get(contact_jid);
         expect(devicelist.devices.length).toBe(4);
         expect(devicelist.devices.at(0).get('id')).toBe('368866411b877c30064a5f62b917cffe');
         expect(devicelist.devices.at(1).get('id')).toBe('3300659945416e274474e469a1f0154c');
@@ -1121,7 +1123,7 @@ describe("The OMEMO module", function() {
         );
         expect(modal.querySelectorAll('input[type="radio"]').length).toBe(2);
 
-        const devicelist = _converse.devicelists.get(contact_jid);
+        const devicelist = _converse.state.devicelists.get(contact_jid);
         expect(devicelist.devices.get('555').get('trusted')).toBe(0);
 
         let trusted_radio = modal.querySelector('input[type="radio"][name="555"][value="1"]');

+ 53 - 27
src/plugins/omemo/utils.js

@@ -29,6 +29,8 @@ import {
 } from '@converse/headless/utils/arraybuffer.js';
 import MUC from 'headless/plugins/muc/muc.js';
 import {IQError, UserFacingError} from 'shared/errors.js';
+import OMEMOStore from './store.js';
+import DeviceLists from './devicelists.js';
 
 const { Strophe, URI, sizzle, u } = converse.env;
 
@@ -76,6 +78,12 @@ export function handleMessageSendError (e, chat) {
     throw e;
 }
 
+export async function contactHasOMEMOSupport (jid) {
+    /* Checks whether the contact advertises any OMEMO-compatible devices. */
+    const devices = await getDevicesForContact(jid);
+    return devices.length > 0;
+}
+
 export function getOutgoingMessageAttributes (chat, attrs) {
     if (chat.get('omemo_active') && attrs.body) {
         attrs['is_encrypted'] = true;
@@ -263,7 +271,7 @@ function addEncryptedFiles(text, offset, richtext) {
 }
 
 export function handleEncryptedFiles (richtext) {
-    if (!_converse.config.get('trusted')) {
+    if (!_converse.state.config.get('trusted')) {
         return;
     }
     richtext.addAnnotations((text, offset) => addEncryptedFiles(text, offset, richtext));
@@ -314,7 +322,7 @@ export async function parseEncryptedMessage (stanza, attrs) {
 }
 
 export function onChatBoxesInitialized () {
-    _converse.chatboxes.on('add', chatbox => {
+    _converse.state.chatboxes.on('add', chatbox => {
         checkOMEMOSupported(chatbox);
         if (chatbox.get('type') === CHATROOMS_TYPE) {
             chatbox.occupants.on('add', o => onOccupantAdded(chatbox, o));
@@ -346,7 +354,7 @@ export function onChatInitialized (el) {
 export function getSessionCipher (jid, id) {
     const { libsignal } = /** @type WindowWithLibsignal */(window);
     const address = new libsignal.SignalProtocolAddress(jid, id);
-    return new libsignal.SessionCipher(_converse.omemo_store, address);
+    return new libsignal.SessionCipher(_converse.state.omemo_store, address);
 }
 
 function getJIDForDecryption (attrs) {
@@ -430,8 +438,9 @@ async function decryptPrekeyWhisperMessage (attrs) {
     // counter restarted from 0.
     try {
         const plaintext = await handleDecryptedWhisperMessage(attrs, key_and_tag);
-        await _converse.omemo_store.generateMissingPreKeys();
-        await _converse.omemo_store.publishBundle();
+        const { omemo_store } = _converse.state;
+        await omemo_store.generateMissingPreKeys();
+        await omemo_store.publishBundle();
         if (plaintext) {
             return Object.assign(attrs, { 'plaintext': plaintext });
         } else {
@@ -497,6 +506,11 @@ export function parseBundle (bundle_el) {
     };
 }
 
+export async function generateFingerprints (jid) {
+    const devices = await getDevicesForContact(jid);
+    return Promise.all(devices.map(d => generateFingerprint(d)));
+}
+
 export async function generateFingerprint (device) {
     if (device.get('bundle')?.fingerprint) {
         return;
@@ -514,11 +528,16 @@ export async function getDevicesForContact (jid) {
     return devicelist.devices;
 }
 
+export function getDeviceForContact (jid, device_id) {
+    return getDevicesForContact(jid).then(devices => devices.get(device_id));
+}
+
 export async function generateDeviceID () {
     const { libsignal } = /** @type WindowWithLibsignal */(window);
 
     /* Generates a device ID, making sure that it's unique */
-    const devicelist = await api.omemo.devicelists.get(_converse.bare_jid, true);
+    const bare_jid = _converse.session.get('bare_jid');
+    const devicelist = await api.omemo.devicelists.get(bare_jid, true);
     const existing_ids = devicelist.devices.pluck('id');
     let device_id = libsignal.KeyHelper.generateRegistrationId();
 
@@ -538,7 +557,7 @@ export async function generateDeviceID () {
 async function buildSession (device) {
     const { libsignal } = /** @type WindowWithLibsignal */(window);
     const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
-    const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address);
+    const sessionBuilder = new libsignal.SessionBuilder(_converse.state.omemo_store, address);
     const prekey = device.getRandomPreKey();
     const bundle = await device.getBundle();
 
@@ -565,7 +584,7 @@ export async function getSession (device) {
 
     const { libsignal } = /** @type WindowWithLibsignal */(window);
     const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
-    const session = await _converse.omemo_store.loadSession(address.toString());
+    const session = await _converse.state.omemo_store.loadSession(address.toString());
     if (session) {
         return session;
     } else {
@@ -605,8 +624,10 @@ async function updateDevicesFromStanza (stanza) {
     const devices = devicelist.devices;
     const removed_ids = devices.pluck('id').filter(id => !device_ids.includes(id));
 
+    const bare_jid = _converse.session.get('bare_jid');
+
     removed_ids.forEach(id => {
-        if (jid === _converse.bare_jid && id === _converse.omemo_store.get('device_id')) {
+        if (jid === bare_jid && id === _converse.state.omemo_store.get('device_id')) {
             return; // We don't set the current device as inactive
         }
         devices.get(id).save('active', false);
@@ -619,7 +640,7 @@ async function updateDevicesFromStanza (stanza) {
             devices.create({ 'id': device_id, 'jid': jid });
         }
     });
-    if (u.isSameBareJID(jid, _converse.bare_jid)) {
+    if (u.isSameBareJID(jid, jid)) {
         // Make sure our own device is on the list
         // (i.e. if it was removed, add it again).
         devicelist.publishCurrentDevice(device_ids);
@@ -648,20 +669,24 @@ export function registerPEPPushHandler () {
 }
 
 export async function restoreOMEMOSession () {
-    if (_converse.omemo_store === undefined) {
-        const id = `converse.omemosession-${_converse.bare_jid}`;
-        _converse.omemo_store = new _converse.OMEMOStore({ id });
-        initStorage(_converse.omemo_store, id);
+    const { state } = _converse;
+    if (state.omemo_store === undefined) {
+        const bare_jid = _converse.session.get('bare_jid');
+        const id = `converse.omemosession-${bare_jid}`;
+        state.omemo_store = new OMEMOStore({ id });
+        initStorage(state.omemo_store, id);
     }
-    await _converse.omemo_store.fetchSession();
+    await state.omemo_store.fetchSession();
 }
 
 async function fetchDeviceLists () {
-    _converse.devicelists = new _converse.DeviceLists();
-    const id = `converse.devicelists-${_converse.bare_jid}`;
-    initStorage(_converse.devicelists, id);
+    const bare_jid = _converse.session.get('bare_jid');
+
+    _converse.state.devicelists = new DeviceLists();
+    const id = `converse.devicelists-${bare_jid}`;
+    initStorage(_converse.state.devicelists, id);
     await new Promise(resolve => {
-        _converse.devicelists.fetch({
+        _converse.state.devicelists.fetch({
             'success': resolve,
             'error': (_m, e) => { log.error(e); resolve(); }
         })
@@ -669,21 +694,21 @@ async function fetchDeviceLists () {
     // Call API method to wait for our own device list to be fetched from the
     // server or to be created. If we have no pre-existing OMEMO session, this
     // will cause a new device and bundle to be generated and published.
-    await api.omemo.devicelists.get(_converse.bare_jid, true);
+    await api.omemo.devicelists.get(bare_jid, true);
 }
 
 export async function initOMEMO (reconnecting) {
     if (reconnecting) {
         return;
     }
-    if (!_converse.config.get('trusted') || api.settings.get('clear_cache_on_logout')) {
+    if (!_converse.state.config.get('trusted') || api.settings.get('clear_cache_on_logout')) {
         log.warn('Not initializing OMEMO, since this browser is not trusted or clear_cache_on_logout is set to true');
         return;
     }
     try {
         await fetchDeviceLists();
         await restoreOMEMOSession();
-        await _converse.omemo_store.publishBundle();
+        await _converse.state.omemo_store.publishBundle();
     } catch (e) {
         log.error('Could not initialize OMEMO support');
         log.error(e);
@@ -702,7 +727,7 @@ async function onOccupantAdded (chatroom, occupant) {
         return;
     }
     if (chatroom.get('omemo_active')) {
-        const supported = await _converse.contactHasOMEMOSupport(occupant.get('jid'));
+        const supported = await contactHasOMEMOSupport(occupant.get('jid'));
         if (!supported) {
             chatroom.createMessage({
                 'message': __(
@@ -723,7 +748,7 @@ async function checkOMEMOSupported (chatbox) {
         await api.waitUntil('OMEMOInitialized');
         supported = chatbox.features.get('nonanonymous') && chatbox.features.get('membersonly');
     } else if (chatbox.get('type') === PRIVATE_CHAT_TYPE) {
-        supported = await _converse.contactHasOMEMOSupport(chatbox.get('jid'));
+        supported = await contactHasOMEMOSupport(chatbox.get('jid'));
     }
     chatbox.set('omemo_supported', supported);
     if (supported && api.settings.get('omemo_default')) {
@@ -812,12 +837,13 @@ async function getBundlesAndBuildSessions (chatbox) {
         if (their_devices.length === 0) {
             throw new UserFacingError(no_devices_err);
         }
-        const own_list = await api.omemo.devicelists.get(_converse.bare_jid)
+        const bare_jid = _converse.session.get('bare_jid');
+        const own_list = await api.omemo.devicelists.get(bare_jid)
         const own_devices = own_list.devices;
         devices = [...own_devices.models, ...their_devices.models];
     }
     // Filter out our own device
-    const id = _converse.omemo_store.get('device_id');
+    const id = _converse.state.omemo_store.get('device_id');
     devices = devices.filter(d => d.get('id') !== id);
     // Fetch bundles if necessary
     await Promise.all(devices.map(d => d.getBundle()));
@@ -858,7 +884,7 @@ export async function createOMEMOMessageStanza (chat, data) {
     // and they are separately encrypted using the
     // session corresponding to the counterpart device.
     stanza.c('encrypted', { 'xmlns': Strophe.NS.OMEMO })
-        .c('header', { 'sid': _converse.omemo_store.get('device_id') });
+        .c('header', { 'sid': _converse.state.omemo_store.get('device_id') });
 
     const { key_and_tag, iv, payload } = await omemo.encryptMessage(message.get('plaintext'));
 

+ 1 - 3
src/plugins/profile/modals/chat-status.js

@@ -1,7 +1,7 @@
 import BaseModal from 'plugins/modal/modal.js';
 import tplChatStatusModal from '../templates/chat-status-modal.js';
 import { __ } from 'i18n';
-import { _converse, api, converse } from '@converse/headless';
+import { api, converse } from '@converse/headless';
 
 const u = converse.env.utils;
 
@@ -47,6 +47,4 @@ export default class ChatStatusModal extends BaseModal {
     }
 }
 
-_converse.ChatStatusModal = ChatStatusModal;
-
 api.elements.define('converse-chat-status-modal', ChatStatusModal);

+ 8 - 2
src/plugins/profile/modals/profile.js

@@ -1,3 +1,7 @@
+
+/**
+ * @typedef {import("@converse/headless/plugins/status/status").default} XMPPStatus
+ */
 import BaseModal from "plugins/modal/modal.js";
 import tplProfileModal from "../templates/profile_modal.js";
 import Compress from 'client-compress';
@@ -5,6 +9,7 @@ import { __ } from 'i18n';
 import { _converse, api, log } from "@converse/headless";
 import '../password-reset.js';
 
+
 const compress = new Compress({
     targetSize: 0.1,
     quality: 0.75,
@@ -25,7 +30,7 @@ export default class ProfileModal extends BaseModal {
         /**
          * Triggered when the _converse.ProfileModal has been created and initialized.
          * @event _converse#profileModalInitialized
-         * @type { _converse.XMPPStatus }
+         * @type {XMPPStatus}
          * @example _converse.api.listen.on('profileModalInitialized', status => { ... });
          */
         api.trigger('profileModalInitialized', this.model);
@@ -40,8 +45,9 @@ export default class ProfileModal extends BaseModal {
     }
 
     async setVCard (data) {
+        const bare_jid = _converse.session.get('bare_jid');
         try {
-            await api.vcard.set(_converse.bare_jid, data);
+            await api.vcard.set(bare_jid, data);
         } catch (err) {
             log.fatal(err);
             this.alert([

+ 3 - 2
src/plugins/profile/password-reset.js

@@ -38,7 +38,8 @@ class PasswordReset extends CustomElement {
 
         if (this.checkPasswordsMatch(ev)) return;
 
-        const iq = $iq({ 'type': 'get', 'to': _converse.domain }).c('query', { 'xmlns': Strophe.NS.REGISTER });
+        const domain = _converse.session.get('domain');
+        const iq = $iq({ 'type': 'get', 'to': domain }).c('query', { 'xmlns': Strophe.NS.REGISTER });
         const iq_response = await api.sendIQ(iq);
 
         if (iq_response === null) {
@@ -59,7 +60,7 @@ class PasswordReset extends CustomElement {
         const data = new FormData(ev.target);
         const password = data.get('password');
 
-        const reset_iq = $iq({ 'type': 'set', 'to': _converse.domain })
+        const reset_iq = $iq({ 'type': 'set', 'to': domain })
             .c('query', { 'xmlns': Strophe.NS.REGISTER })
                 .c('username', {}, username)
                 .c('password', {}, password);

+ 1 - 1
src/plugins/profile/statusview.js

@@ -4,7 +4,7 @@ import { _converse, api } from '@converse/headless';
 
 class Profile extends CustomElement {
     initialize () {
-        this.model = _converse.xmppstatus;
+        this.model = _converse.state.xmppstatus;
         this.listenTo(this.model, "change", () => this.requestUpdate());
         this.listenTo(this.model, "vcard:add", () => this.requestUpdate());
         this.listenTo(this.model, "vcard:change", () => this.requestUpdate());

+ 1 - 1
src/plugins/push/index.js

@@ -25,7 +25,7 @@ converse.plugins.add('converse-push', {
         api.listen.on('statusInitialized', () => enablePush());
 
         if (api.settings.get('enable_muc_push')) {
-            api.listen.on('chatBoxesInitialized', () => _converse.chatboxes.on('add', onChatBoxAdded));
+            api.listen.on('chatBoxesInitialized', () => _converse.state.chatboxes.on('add', onChatBoxAdded));
         }
     },
 });

+ 12 - 4
src/plugins/push/utils.js

@@ -7,12 +7,13 @@ async function disablePushAppServer (domain, push_app_server) {
     if (!push_app_server.jid) {
         return;
     }
-    if (!(await api.disco.supports(Strophe.NS.PUSH, domain || _converse.bare_jid))) {
+    const bare_jid = _converse.session.get('bare_jid');
+    if (!(await api.disco.supports(Strophe.NS.PUSH, domain || bare_jid))) {
         log.warn(`Not disabling push app server "${push_app_server.jid}", no disco support from your server.`);
         return;
     }
     const stanza = $iq({'type': 'set'});
-    if (domain !== _converse.bare_jid) {
+    if (domain !== bare_jid) {
         stanza.attrs({'to': domain});
     }
     stanza.c('disable', {
@@ -48,7 +49,8 @@ async function enablePushAppServer (domain, push_app_server) {
         return;
     }
     const stanza = $iq({'type': 'set'});
-    if (domain !== _converse.bare_jid) {
+    const bare_jid = _converse.session.get('bare_jid');
+    if (domain !== bare_jid) {
         stanza.attrs({'to': domain});
     }
     stanza.c('enable', {
@@ -66,8 +68,14 @@ async function enablePushAppServer (domain, push_app_server) {
     return api.sendIQ(stanza);
 }
 
+/**
+ * @param {string} [domain]
+ */
 export async function enablePush (domain) {
-    domain = domain || _converse.bare_jid;
+    if (!domain) {
+        const bare_jid = _converse.session.get('bare_jid');
+        domain = bare_jid;
+    }
     const push_enabled = _converse.session.get('push_enabled') || [];
     if (push_enabled.includes(domain)) {
         return;

+ 12 - 11
src/plugins/register/panel.js

@@ -131,7 +131,7 @@ class RegisterPanel extends CustomElement {
     /**
      * Handler for {@link _converse.RegisterPanel#getRegistrationFields}
      * @method _converse.RegisterPanel#onRegistrationFields
-     * @param { Element } stanza - The query stanza.
+     * @param {Element} stanza - The query stanza.
      */
     onRegistrationFields (stanza) {
         if (stanza.getAttribute("type") === "error") {
@@ -184,7 +184,7 @@ class RegisterPanel extends CustomElement {
     /**
      * Callback method that gets called when the user has chosen an XMPP provider
      * @method _converse.RegisterPanel#onProviderChosen
-     * @param { HTMLElement } form - The form that was submitted
+     * @param {HTMLElement} form - The form that was submitted
      */
     onProviderChosen (form) {
         const domain = /** @type {HTMLInputElement} */(form.querySelector('input[name=domain]'))?.value;
@@ -194,7 +194,7 @@ class RegisterPanel extends CustomElement {
     /**
      * Fetch a registration form from the requested domain
      * @method _converse.RegisterPanel#fetchRegistrationForm
-     * @param { String } domain_name - XMPP server domain
+     * @param {string} domain_name - XMPP server domain
      */
     fetchRegistrationForm (domain_name) {
         this.status = FETCHING_FORM;
@@ -213,7 +213,7 @@ class RegisterPanel extends CustomElement {
      * Callback function called by Strophe whenever the connection status changes.
      * Passed to Strophe specifically during a registration attempt.
      * @method _converse.RegisterPanel#onConnectStatusChanged
-     * @param { number } status_code - The Strophe.Status status code
+     * @param {number} status_code - The Strophe.Status status code
      */
     onConnectStatusChanged(status_code) {
         log.debug('converse-register: onConnectStatusChanged');
@@ -238,11 +238,12 @@ class RegisterPanel extends CustomElement {
             setActiveForm('login');
 
             if (this.fields.password && this.fields.username) {
+                const connection = api.connection.get();
                 // automatically log the user in
-                api.connection.get().connect(
+                connection.connect(
                     this.fields.username.toLowerCase()+'@'+this.domain.toLowerCase(),
                     this.fields.password,
-                    _converse.onConnectStatusChanged
+                    connection.onConnectStatusChanged
                 );
                 this.setFeedbackMessage(__('Now logging you in'));
             } else {
@@ -292,7 +293,7 @@ class RegisterPanel extends CustomElement {
      * Renders the registration form based on the XForm fields
      * received from the XMPP server.
      * @method _converse.RegisterPanel#renderRegistrationForm
-     * @param { Element } stanza - The IQ stanza received from the XMPP server.
+     * @param {Element} stanza - The IQ stanza received from the XMPP server.
      */
     renderRegistrationForm (stanza) {
         this.form_fields = this.getFormFields(stanza);
@@ -303,7 +304,7 @@ class RegisterPanel extends CustomElement {
      * Report back to the user any error messages received from the
      * XMPP server after attempted registration.
      * @method _converse.RegisterPanel#reportErrors
-     * @param { Element } stanza - The IQ stanza received from the XMPP server
+     * @param {Element} stanza - The IQ stanza received from the XMPP server
      */
     reportErrors (stanza) {
         const errors = Array.from(stanza.querySelectorAll('error'));
@@ -340,7 +341,7 @@ class RegisterPanel extends CustomElement {
      * Handler, when the user submits the registration form.
      * Provides form error feedback or starts the registration process.
      * @method _converse.RegisterPanel#submitRegistrationForm
-     * @param { HTMLElement } form - The HTML form that was submitted
+     * @param {HTMLElement} form - The HTML form that was submitted
      */
     submitRegistrationForm (form) {
         const inputs = sizzle(':input:not([type=button]):not([type=submit])', form);
@@ -365,7 +366,7 @@ class RegisterPanel extends CustomElement {
     /**
      * Stores the values that will be sent to the XMPP server during attempted registration.
      * @method _converse.RegisterPanel#setFields
-     * @param { Element } stanza - the IQ stanza that will be sent to the XMPP server.
+     * @param {Element} stanza - the IQ stanza that will be sent to the XMPP server.
      */
     setFields (stanza) {
         const query = stanza.querySelector('query');
@@ -413,7 +414,7 @@ class RegisterPanel extends CustomElement {
      * is received from the XMPP server, after attempting to
      * register a new user.
      * @method _converse.RegisterPanel#reportErrors
-     * @param { Element } stanza - The IQ stanza.
+     * @param {Element} stanza - The IQ stanza.
      */
     _onRegisterIQ (stanza) {
         const connection = api.connection.get();

+ 3 - 1
src/plugins/register/utils.js

@@ -2,7 +2,9 @@ import { _converse, api } from '@converse/headless';
 
 export async function setActiveForm (value) {
     await api.waitUntil('controlBoxInitialized');
-    const controlbox = _converse.chatboxes.get('controlbox');
+
+    const { chatboxes } = _converse.state;
+    const controlbox = chatboxes.get('controlbox');
     controlbox.set({ 'active-form': value });
 }
 

+ 6 - 2
src/plugins/roomslist/model.js

@@ -1,5 +1,6 @@
 import { Model } from '@converse/skeletor';
 import { _converse, api, converse } from "@converse/headless";
+import { OPENED } from '@converse/headless/shared/constants';
 
 const { Strophe } = converse.env;
 
@@ -8,8 +9,8 @@ class RoomsListModel extends Model {
     defaults () {  // eslint-disable-line class-methods-use-this
         return {
             'muc_domain': api.settings.get('muc_domain'),
-            'nick': _converse.getDefaultMUCNickname(),
-            'toggle_state':  _converse.OPENED,
+            'nick': _converse.exports.getDefaultMUCNickname(),
+            'toggle_state':  OPENED,
             'collapsed_domains': [],
         };
     }
@@ -19,6 +20,9 @@ class RoomsListModel extends Model {
         api.settings.listen.on('change:muc_domain', (muc_domain) => this.setDomain(muc_domain));
     }
 
+    /**
+     * @param {string} jid
+     */
     setDomain (jid) {
         if (!api.settings.get('locked_muc_domain')) {
             this.save('muc_domain', Strophe.getDomainFromJid(jid));

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

@@ -71,7 +71,7 @@ export function tplRoomItem (el, room) {
 
 export default (el) => {
     const group_by_domain = api.settings.get('muc_grouped_by_domain');
-    const { chatboxes } = _converse;
+    const { chatboxes } = _converse.state;
     const rooms = chatboxes.filter(m => m.get('type') === CHATROOMS_TYPE);
     rooms.sort((a, b) => (a.getDisplayName().toLowerCase() <= b.getDisplayName().toLowerCase() ? -1 : 1));
 

+ 12 - 9
src/plugins/roomslist/view.js

@@ -6,21 +6,24 @@ import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless";
 import { initStorage } from '@converse/headless/utils/storage.js';
 import { isChatRoom } from '@converse/headless/plugins/muc/utils.js';
+import { CLOSED, OPENED } from 'headless/shared/constants.js';
 
 const { Strophe, u } = converse.env;
 
 export class RoomsList extends CustomElement {
 
     initialize () {
-        const id = `converse.roomspanel${_converse.bare_jid}`;
+        const bare_jid = _converse.session.get('bare_jid');
+        const id = `converse.roomspanel${bare_jid}`;
         this.model = new RoomsListModel({ id });
         initStorage(this.model, id);
         this.model.fetch();
 
-        this.listenTo(_converse.chatboxes, 'add', this.renderIfChatRoom);
-        this.listenTo(_converse.chatboxes, 'remove', this.renderIfChatRoom);
-        this.listenTo(_converse.chatboxes, 'destroy', this.renderIfChatRoom);
-        this.listenTo(_converse.chatboxes, 'change', this.renderIfRelevantChange);
+        const { chatboxes } = _converse.state;
+        this.listenTo(chatboxes, 'add', this.renderIfChatRoom);
+        this.listenTo(chatboxes, 'remove', this.renderIfChatRoom);
+        this.listenTo(chatboxes, 'destroy', this.renderIfChatRoom);
+        this.listenTo(chatboxes, 'change', this.renderIfRelevantChange);
         this.listenTo(this.model, 'change', () => this.requestUpdate());
 
         this.requestUpdate();
@@ -44,7 +47,7 @@ export class RoomsList extends CustomElement {
 
     showRoomDetailsModal (ev) { // eslint-disable-line class-methods-use-this
         const jid = ev.currentTarget.getAttribute('data-room-jid');
-        const room = _converse.chatboxes.get(jid);
+        const room = _converse.state.chatboxes.get(jid);
         ev.preventDefault();
         api.modal.show('converse-muc-details-modal', {'model': room}, ev);
     }
@@ -73,10 +76,10 @@ export class RoomsList extends CustomElement {
     toggleRoomsList (ev) {
         ev?.preventDefault?.();
         const list_el = this.querySelector('.open-rooms-list');
-        if (this.model.get('toggle_state') === _converse.CLOSED) {
-            u.slideOut(list_el).then(() => this.model.save({'toggle_state': _converse.OPENED}));
+        if (this.model.get('toggle_state') === CLOSED) {
+            u.slideOut(list_el).then(() => this.model.save({'toggle_state': OPENED}));
         } else {
-            u.slideIn(list_el).then(() => this.model.save({'toggle_state': _converse.CLOSED}));
+            u.slideIn(list_el).then(() => this.model.save({'toggle_state': CLOSED}));
         }
     }
 

+ 1 - 1
src/plugins/rosterview/contactview.js

@@ -74,7 +74,7 @@ export default class RosterContact extends CustomElement {
     async acceptRequest (ev) {
         ev?.preventDefault?.();
 
-        await _converse.roster.sendContactAddIQ(
+        await _converse.state.roster.sendContactAddIQ(
             this.model.get('jid'),
             this.model.getFullname(),
             []

+ 5 - 6
src/plugins/rosterview/index.js

@@ -30,15 +30,14 @@ converse.plugins.add('converse-rosterview', {
         });
         api.promises.add('rosterViewInitialized');
 
-        _converse.RosterFilter = RosterFilter;
-        _converse.RosterContactView = RosterContactView;
+        const exports = { RosterFilter, RosterContactView };
+        Object.assign(_converse, exports); // DEPRECATED
+        Object.assign(_converse.exports, exports);
 
         /* -------- Event Handlers ----------- */
         api.listen.on('chatBoxesInitialized', () => {
-            _converse.chatboxes.on('destroy', chatbox => highlightRosterItem(chatbox));
-            _converse.chatboxes.on('change:hidden', chatbox => highlightRosterItem(chatbox));
+            _converse.state.chatboxes.on('destroy', c => highlightRosterItem(c));
+            _converse.state.chatboxes.on('change:hidden', c => highlightRosterItem(c));
         });
-
-        api.listen.on('afterTearDown', () => _converse.rotergroups?.off().reset());
     }
 });

+ 2 - 2
src/plugins/rosterview/modals/add-contact.js

@@ -31,7 +31,7 @@ export default class AddContactModal extends BaseModal {
         if (!jid || jid.split('@').filter((s) => !!s).length < 2) {
             this.model.set('error', __('Please enter a valid XMPP address'));
             return false;
-        } else if (_converse.roster.get(Strophe.getBareJidFromJid(jid))) {
+        } else if (_converse.state.roster.get(Strophe.getBareJidFromJid(jid))) {
             this.model.set('error', __('This contact has already been added'));
             return false;
         }
@@ -43,7 +43,7 @@ export default class AddContactModal extends BaseModal {
         if (group && !Array.isArray(group)) {
             group = [group];
         }
-        _converse.roster.addAndSubscribe(jid, name, group);
+        _converse.state.roster.addAndSubscribe(jid, name, group);
         this.model.clear();
         this.modal.hide();
     }

+ 4 - 3
src/plugins/rosterview/modals/templates/add-contact.js

@@ -1,11 +1,12 @@
 import { __ } from 'i18n';
-import { _converse, api } from '@converse/headless';
+import { api } from '@converse/headless';
 import {
     getGroupsAutoCompleteList,
     getJIDsAutoCompleteList,
     getNamesAutoCompleteList
 } from '@converse/headless/plugins/roster/utils.js';
 import { html } from "lit";
+import { FILTER_STARTSWITH } from 'shared/autocomplete/utils';
 
 
 export default (el) => {
@@ -27,7 +28,7 @@ export default (el) => {
                             .list=${getJIDsAutoCompleteList()}
                             .data=${(text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`}
                             position="below"
-                            filter=${_converse.FILTER_STARTSWITH}
+                            filter=${FILTER_STARTSWITH}
                             ?required=${(!api.settings.get('xhr_user_search_url'))}
                             value="${el.model.get('jid') || ''}"
                             placeholder="${i18n_contact_placeholder}"
@@ -46,7 +47,7 @@ export default (el) => {
                     ${api.settings.get('autocomplete_add_contact') && typeof api.settings.get('xhr_user_search_url') === 'string' ?
                         html`<converse-autocomplete
                             .getAutoCompleteList=${getNamesAutoCompleteList}
-                            filter=${_converse.FILTER_STARTSWITH}
+                            filter=${FILTER_STARTSWITH}
                             value="${el.model.get('nickname') || ''}"
                             placeholder="${i18n_contact_placeholder}"
                             name="name"></converse-autocomplete>` :

+ 10 - 8
src/plugins/rosterview/rosterview.js

@@ -4,6 +4,7 @@ import { Model } from '@converse/skeletor';
 import { _converse, api } from "@converse/headless";
 import { initStorage } from '@converse/headless/utils/storage.js';
 import { slideIn, slideOut } from 'utils/html.js';
+import { CLOSED, OPENED } from "@converse/headless/shared/constants.js";
 
 
 /**
@@ -14,14 +15,15 @@ import { slideIn, slideOut } from 'utils/html.js';
 export default class RosterView extends CustomElement {
 
     async initialize () {
-        const id = `converse.contacts-panel${_converse.bare_jid}`;
+        const bare_jid = _converse.session.get('bare_jid');
+        const id = `converse.contacts-panel${bare_jid}`;
         this.model = new Model({ id });
         initStorage(this.model, id);
         this.model.fetch();
 
         await api.waitUntil('rosterInitialized')
 
-        const { chatboxes, presences, roster } = _converse;
+        const { chatboxes, presences, roster } = _converse.state;
         this.listenTo(_converse, 'rosterContactsFetched', () => this.requestUpdate());
         this.listenTo(presences, 'change:show', () => this.requestUpdate());
         this.listenTo(chatboxes, 'change:hidden', () => this.requestUpdate());
@@ -43,13 +45,13 @@ export default class RosterView extends CustomElement {
         return tplRoster(this);
     }
 
-    showAddContactModal (ev) { // eslint-disable-line class-methods-use-this
+    showAddContactModal (ev) {
         api.modal.show('converse-add-contact-modal', {'model': new Model()}, ev);
     }
 
-    async syncContacts (ev) { // eslint-disable-line class-methods-use-this
+    async syncContacts (ev) {
         ev.preventDefault();
-        const { roster } = _converse;
+        const { roster } = _converse.state;
         this.syncing_contacts = true;
         this.requestUpdate();
 
@@ -64,10 +66,10 @@ export default class RosterView extends CustomElement {
     toggleRoster (ev) {
         ev?.preventDefault?.();
         const list_el = /** @type {HTMLElement} */(this.querySelector('.list-container.roster-contacts'));
-        if (this.model.get('toggle_state') === _converse.CLOSED) {
-            slideOut(list_el).then(() => this.model.save({'toggle_state': _converse.OPENED}));
+        if (this.model.get('toggle_state') === CLOSED) {
+            slideOut(list_el).then(() => this.model.save({'toggle_state': OPENED}));
         } else {
-            slideIn(list_el).then(() => this.model.save({'toggle_state': _converse.CLOSED}));
+            slideIn(list_el).then(() => this.model.save({'toggle_state': CLOSED}));
         }
     }
 }

+ 2 - 2
src/plugins/rosterview/templates/group.js

@@ -13,7 +13,7 @@ function renderContact (contact) {
     const jid = contact.get('jid');
     const extra_classes = [];
     if (isUniView()) {
-        const chatbox = _converse.chatboxes.get(jid);
+        const chatbox = _converse.state.chatboxes.get(jid);
         if (chatbox && !chatbox.get('hidden')) {
             extra_classes.push('open');
         }
@@ -50,7 +50,7 @@ function renderContact (contact) {
 
 export default  (o) => {
     const i18n_title = __('Click to hide these contacts');
-    const collapsed = _converse.roster.state.get('collapsed_groups');
+    const collapsed = _converse.state.roster.state.get('collapsed_groups');
     return html`
         <div class="roster-group" data-group="${o.name}">
             <a href="#" class="list-toggle group-toggle controlbox-padded" title="${i18n_title}" @click=${ev => toggleGroup(ev, o.name)}>

+ 5 - 4
src/plugins/rosterview/templates/roster.js

@@ -1,5 +1,6 @@
 import tplGroup from "./group.js";
 import tplRosterFilter from "./roster_filter.js";
+import { CLOSED } from "@converse/headless/shared/constants.js";
 import { __ } from 'i18n';
 import { _converse, api } from "@converse/headless";
 import { contactsComparator, groupsComparator } from '@converse/headless/plugins/roster/utils.js';
@@ -13,10 +14,10 @@ export default (el) => {
     const i18n_toggle_contacts = __('Click to toggle contacts');
     const i18n_title_add_contact = __('Add a contact');
     const i18n_title_sync_contacts = __('Re-sync your contacts');
-    const roster = _converse.roster || [];
+    const roster = _converse.state.roster || [];
     const contacts_map = roster.reduce((acc, contact) => populateContactsMap(acc, contact), {});
     const groupnames = Object.keys(contacts_map).filter(shouldShowGroup);
-    const is_closed = el.model.get('toggle_state') === _converse.CLOSED;
+    const is_closed = el.model.get('toggle_state') === CLOSED;
     groupnames.sort(groupsComparator);
 
     return html`
@@ -50,9 +51,9 @@ export default (el) => {
             <converse-list-filter
                     @update=${() => el.requestUpdate()}
                     .promise=${api.waitUntil('rosterInitialized')}
-                    .items=${_converse.roster}
+                    .items=${_converse.state.roster}
                     .template=${tplRosterFilter}
-                    .model=${_converse.roster_filter}></converse-list-filter>
+                    .model=${_converse.state.roster_filter}></converse-list-filter>
 
             ${ repeat(groupnames, (n) => n, (name) => {
                 const contacts = contacts_map[name].filter(c => shouldShowContact(c, name));

+ 13 - 12
src/plugins/rosterview/utils.js

@@ -15,21 +15,22 @@ export function removeContact (contact) {
 }
 
 export function highlightRosterItem (chatbox) {
-    _converse.roster?.get(chatbox.get('jid'))?.trigger('highlight');
+    _converse.state.roster?.get(chatbox.get('jid'))?.trigger('highlight');
 }
 
 export function toggleGroup (ev, name) {
     ev?.preventDefault?.();
-    const collapsed = _converse.roster.state.get('collapsed_groups');
+    const { roster } = _converse.state;
+    const collapsed = roster.state.get('collapsed_groups');
     if (collapsed.includes(name)) {
-        _converse.roster.state.save('collapsed_groups', collapsed.filter(n => n !== name));
+        roster.state.save('collapsed_groups', collapsed.filter(n => n !== name));
     } else {
-        _converse.roster.state.save('collapsed_groups', [...collapsed, name]);
+        roster.state.save('collapsed_groups', [...collapsed, name]);
     }
 }
 
 export function isContactFiltered (contact, groupname) {
-    const filter = _converse.roster_filter;
+    const filter = _converse.state.roster_filter;
     const type = filter.get('type');
     const q = (type === 'state') ?
         filter.get('state').toLowerCase() :
@@ -38,7 +39,7 @@ export function isContactFiltered (contact, groupname) {
     if (!q) return false;
 
     if (type === 'state') {
-        const sticky_groups = [_converse.HEADER_REQUESTING_CONTACTS, _converse.HEADER_UNREAD];
+        const sticky_groups = [_converse.labels.HEADER_REQUESTING_CONTACTS, _converse.labels.HEADER_UNREAD];
         if (sticky_groups.includes(groupname)) {
             // When filtering by chat state, we still want to
             // show sticky groups, even though they don't
@@ -71,7 +72,7 @@ export function shouldShowContact (contact, groupname) {
 }
 
 export function shouldShowGroup (group) {
-    const filter = _converse.roster_filter;
+    const filter = _converse.state.roster_filter;
     const type = filter.get('type');
     if (type === 'groups') {
         const q = filter.get('text')?.toLowerCase();
@@ -87,18 +88,18 @@ export function shouldShowGroup (group) {
 
 export function populateContactsMap (contacts_map, contact) {
     if (contact.get('requesting')) {
-        const name = _converse.HEADER_REQUESTING_CONTACTS;
+        const name = _converse.labels.HEADER_REQUESTING_CONTACTS;
         contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
     } else {
         let contact_groups;
         if (api.settings.get('roster_groups')) {
             contact_groups = contact.get('groups');
-            contact_groups = (contact_groups.length === 0) ? [_converse.HEADER_UNGROUPED] : contact_groups;
+            contact_groups = (contact_groups.length === 0) ? [_converse.labels.HEADER_UNGROUPED] : contact_groups;
         } else {
             if (contact.get('ask') === 'subscribe') {
-                contact_groups = [_converse.HEADER_PENDING_CONTACTS];
+                contact_groups = [_converse.labels.HEADER_PENDING_CONTACTS];
             } else {
-                contact_groups = [_converse.HEADER_CURRENT_CONTACTS];
+                contact_groups = [_converse.labels.HEADER_CURRENT_CONTACTS];
             }
         }
         for (const name of contact_groups) {
@@ -106,7 +107,7 @@ export function populateContactsMap (contacts_map, contact) {
         }
     }
     if (contact.get('num_unread')) {
-        const name = _converse.HEADER_UNREAD;
+        const name = _converse.labels.HEADER_UNREAD;
         contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
     }
     return contacts_map;

+ 3 - 4
src/shared/autocomplete/index.js

@@ -1,10 +1,9 @@
 import './component.js';
 import AutoComplete from './autocomplete.js';
-import { FILTER_CONTAINS, FILTER_STARTSWITH } from './utils.js';
 import { _converse } from '@converse/headless';
 
 import './styles/_autocomplete.scss';
 
-_converse.FILTER_CONTAINS = FILTER_CONTAINS;
-_converse.FILTER_STARTSWITH = FILTER_STARTSWITH;
-_converse.AutoComplete = AutoComplete;
+const exports = { AutoComplete };
+Object.assign(_converse, exports); // DEPRECATED
+Object.assign(_converse.exports, exports);

+ 2 - 2
src/shared/chat/baseview.js

@@ -23,13 +23,13 @@ export default class BaseChatView extends CustomElement {
 
     disconnectedCallback () {
         super.disconnectedCallback();
-        _converse.chatboxviews.remove(this.jid, this);
+        _converse.state.chatboxviews.remove(this.jid, this);
     }
 
     updated () {
         if (this.model && this.jid !== this.model.get('jid')) {
             this.stopListening();
-            _converse.chatboxviews.remove(this.model.get('jid'), this);
+            _converse.state.chatboxviews.remove(this.model.get('jid'), this);
             delete this.model;
             this.requestUpdate();
             this.initialize();

+ 4 - 2
src/shared/chat/emoji-dropdown.js

@@ -1,4 +1,5 @@
 import DropdownBase from "shared/components/dropdown.js";
+import EmojiPicker from "@converse/headless/plugins/emoji/picker";
 import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless";
 import { html } from "lit";
@@ -30,8 +31,9 @@ export default class EmojiDropdown extends DropdownBase {
         if (!this.init_promise) {
             this.init_promise = (async () => {
                 await api.emojis.initialize()
-                const id = `converse.emoji-${_converse.bare_jid}-${this.chatview.model.get('jid')}`;
-                this.model = new _converse.EmojiPicker({'id': id});
+                const bare_jid = _converse.session.get('bare_jid');
+                const id = `converse.emoji-${bare_jid}-${this.chatview.model.get('jid')}`;
+                this.model = new EmojiPicker({ id });
                 initStorage(this.model, id);
                 await new Promise(resolve => this.model.fetch({'success': resolve, 'error': resolve}));
                 // We never want still be in the autocompleting state upon page load

+ 3 - 2
src/shared/chat/emoji-picker-content.js

@@ -2,10 +2,11 @@
  * @typedef {module:emoji-picker.EmojiPicker} EmojiPicker
  */
 import { CustomElement } from 'shared/components/element.js';
-import { _converse, converse, api } from '@converse/headless';
+import { converse, api } from '@converse/headless';
 import { html } from 'lit';
 import { tplAllEmojis, tplSearchResults } from './templates/emoji-picker.js';
 import { getTonedEmojis } from './utils.js';
+import { FILTER_CONTAINS } from 'shared/autocomplete/utils.js';
 
 const { sizzle } = converse.env;
 
@@ -100,7 +101,7 @@ export default class EmojiPickerContent extends CustomElement {
                 return true;
             }
         }
-        if (this.query && !_converse.FILTER_CONTAINS(shortname, this.query)) {
+        if (this.query && !FILTER_CONTAINS(shortname, this.query)) {
             return true;
         }
         return false;

+ 4 - 3
src/shared/chat/emoji-picker.js

@@ -7,8 +7,9 @@ import './emoji-dropdown.js';
 import DOMNavigator from "shared/dom-navigator";
 import debounce from 'lodash-es/debounce';
 import { CustomElement } from 'shared/components/element.js';
+import { FILTER_CONTAINS } from "shared/autocomplete/utils.js";
 import { KEYCODES } from '@converse/headless/shared/constants.js';
-import { _converse, api, converse } from "@converse/headless";
+import { api, converse } from "@converse/headless";
 import { getTonedEmojis } from './utils.js';
 import { tplEmojiPicker } from "./templates/emoji-picker.js";
 
@@ -100,7 +101,7 @@ export default class EmojiPicker extends CustomElement {
 
     updateSearchResults (changed) {
         const old_query = changed.get('query');
-        const contains = _converse.FILTER_CONTAINS;
+        const contains = FILTER_CONTAINS;
         if (this.query) {
             if (this.query === old_query) {
                 return this.search_results;
@@ -195,7 +196,7 @@ export default class EmojiPicker extends CustomElement {
         if (ev.keyCode === KEYCODES.TAB) {
             if (ev.target.value) {
                 ev.preventDefault();
-                const match = converse.emojis.shortnames.find(sn => _converse.FILTER_CONTAINS(sn, ev.target.value));
+                const match = converse.emojis.shortnames.find(sn => FILTER_CONTAINS(sn, ev.target.value));
                 match && this.model.set({'query': match});
             } else if (!this.navigator.enabled) {
                 this.enableArrowNavigation(ev);

+ 4 - 4
src/shared/chat/message-actions.js

@@ -20,7 +20,7 @@ import './styles/message-actions.scss';
  * @property { String } name
  */
 
-const { Strophe, u } = converse.env;
+const { u } = converse.env;
 
 class MessageActions extends CustomElement {
     static get properties () {
@@ -144,12 +144,12 @@ class MessageActions extends CustomElement {
         if (result === null) {
             const err_msg = __(`A timeout occurred while trying to retract the message`);
             api.alert('error', __('Error'), err_msg);
-            log(err_msg, Strophe.LogLevel.WARN);
+            log.warn(err_msg);
         } else if (u.isErrorStanza(result)) {
             const err_msg = __(`Sorry, you're not allowed to retract this message.`);
             api.alert('error', __('Error'), err_msg);
-            log(err_msg, Strophe.LogLevel.WARN);
-            log(result, Strophe.LogLevel.WARN);
+            log.warn(err_msg);
+            log.error(result);
         }
     }
 

+ 3 - 2
src/shared/chat/message.js

@@ -13,8 +13,9 @@ import tplMessageText from './templates/message-text.js';
 import tplRetraction from './templates/retraction.js';
 import tplSpinner from 'templates/spinner.js';
 import { CustomElement } from 'shared/components/element.js';
+import { SUCCESS } from 'headless/shared/constants.js';
 import { __ } from 'i18n';
-import { _converse, api, converse, log } from  '@converse/headless';
+import { api, converse, log } from  '@converse/headless';
 import { getAppSettings } from '@converse/headless/shared/settings/utils.js';
 import { getHats } from './utils.js';
 
@@ -80,7 +81,7 @@ export default class Message extends CustomElement {
             return '';
         } else if (this.show_spinner) {
             return tplSpinner();
-        } else if (this.model.get('file') && this.model.get('upload') !== _converse.SUCCESS) {
+        } else if (this.model.get('file') && this.model.get('upload') !== SUCCESS) {
             return this.renderFileProgress();
         } else if (['mep'].includes(this.model.get('type'))) {
             return this.renderMEPMessage();

+ 5 - 4
src/shared/chat/toolbar.js

@@ -48,9 +48,9 @@ export class ChatToolbar extends CustomElement {
 
     firstUpdated () {
         /**
-         * Triggered once the _converse.ChatBoxView's toolbar has been rendered
+         * Triggered once the toolbar has been rendered
          * @event _converse#renderToolbar
-         * @type { _converse.ChatBoxView }
+         * @type { ChatToolbar }
          * @example _converse.api.listen.on('renderToolbar', this => { ... });
          */
         api.trigger('renderToolbar', this);
@@ -60,7 +60,7 @@ export class ChatToolbar extends CustomElement {
         const buttons = [];
 
         if (this.show_emoji_button) {
-            const chatview = _converse.chatboxviews.get(this.model.get('jid'));
+            const chatview = _converse.state.chatboxviews.get(this.model.get('jid'));
             buttons.push(html`<converse-emoji-dropdown .chatview=${chatview}></converse-emoji-dropdown>`);
         }
 
@@ -86,7 +86,8 @@ export class ChatToolbar extends CustomElement {
             buttons.push(this.getSpoilerButton());
         }
 
-        const http_upload_promise = api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain);
+        const domain = _converse.session.get('domain');
+        const http_upload_promise = api.disco.supports(Strophe.NS.HTTPUPLOAD, domain);
         buttons.push(html`${until(http_upload_promise.then(is_supported => this.getHTTPUploadButton(is_supported)),'')}`);
 
         if (this.is_groupchat && api.settings.get('visible_toolbar_buttons')?.toggle_occupants) {

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно