Selaa lähdekoodia

Remember dragresize dimensions

Also:
- don't render the controlbox if it's not necessary
JC Brand 1 kuukausi sitten
vanhempi
commit
55f88d1dbc
39 muutettua tiedostoa jossa 195 lisäystä ja 136 poistoa
  1. 2 0
      CHANGES.md
  2. 28 12
      src/headless/shared/chatbox.js
  3. 1 0
      src/headless/types/shared/chatbox.d.ts
  4. 1 1
      src/plugins/bookmark-views/tests/bookmarks-list.js
  5. 2 2
      src/plugins/bookmark-views/tests/bookmarks.js
  6. 2 3
      src/plugins/bookmark-views/tests/deprecated.js
  7. 29 20
      src/plugins/chatboxviews/templates/chats.js
  8. 1 1
      src/plugins/chatboxviews/view.js
  9. 6 3
      src/plugins/chatview/templates/chat.js
  10. 3 3
      src/plugins/chatview/tests/chatbox.js
  11. 0 16
      src/plugins/controlbox/controlbox.js
  12. 4 3
      src/plugins/controlbox/templates/controlbox.js
  13. 3 3
      src/plugins/controlbox/tests/controlbox.js
  14. 2 2
      src/plugins/controlbox/tests/login.js
  15. 2 1
      src/plugins/disco-views/tests/disco-browser.js
  16. 2 1
      src/plugins/dragresize/index.js
  17. 6 6
      src/plugins/dragresize/mixin.js
  18. 15 4
      src/plugins/dragresize/utils.js
  19. 9 2
      src/plugins/headlines-view/templates/feeds-list.js
  20. 8 9
      src/plugins/headlines-view/templates/headlines.js
  21. 1 1
      src/plugins/minimize/index.js
  22. 1 2
      src/plugins/minimize/utils.js
  23. 1 1
      src/plugins/muc-views/heading.js
  24. 1 1
      src/plugins/muc-views/muc.js
  25. 14 12
      src/plugins/muc-views/templates/muc.js
  26. 9 6
      src/plugins/muc-views/tests/muc-list-modal.js
  27. 1 1
      src/plugins/notifications/tests/notification.js
  28. 7 7
      src/plugins/register/tests/register.js
  29. 1 1
      src/plugins/rosterview/tests/add-contact-modal.js
  30. 1 1
      src/plugins/rosterview/tests/presence.js
  31. 1 2
      src/plugins/rosterview/tests/protocol.js
  32. 1 1
      src/plugins/rosterview/tests/roster.js
  33. 14 0
      src/shared/chat/utils.js
  34. 3 3
      src/shared/modals/templates/user-details.js
  35. 2 3
      src/shared/modals/user-details.js
  36. 1 1
      src/shared/tests/mock.js
  37. 5 0
      src/types/plugins/dragresize/utils.d.ts
  38. 1 1
      src/types/plugins/headlines-view/templates/feeds-list.d.ts
  39. 4 0
      src/types/shared/chat/utils.d.ts

+ 2 - 0
CHANGES.md

@@ -6,10 +6,12 @@
 - #3676: Flyout box is not positioned correctly on mobile devices in "overlayed" mode
 - Remove modal from the DOM when it's closed
 - Fix login form style for `classic` theme
+- Fix css for the `headlines` feed
 - Properly handle OGP metadata that doesn't have an image
 - Fix TypeError which prevents logging out
 - Fix auto zoom in when input message in ios safari
 - Add a service discovery browser to the settings modal
+- Add a modal to view blocked XMPP addresses
 
 ## 11.0.0 (2025-05-21)
 

+ 28 - 12
src/headless/shared/chatbox.js

@@ -61,28 +61,44 @@ export default class ChatBoxBase extends ModelWithMessages(Model) {
             return this;
         }
         // Overlayed view mode
-        u.safeSave(this, { hidden: false });
+        u.safeSave(this, { hidden: false, closed: false });
         this.trigger('show');
         return this;
     }
 
+    async shouldDestroyOnClose() {
+        /**
+         * *Hook* which allows plugins to determine whether a chat should be destroyed when it's closed.
+         * For example, used by the converse-dragresize plugin to prevent resized chats
+         * from being destroyed, thereby losing the resize dimensions.
+         * @event _converse#shouldDestroyOnClose
+         * @param {ChatBoxBase} chatbox
+         * @param {boolean} should_destroy
+         */
+        return await api.hook('shouldDestroyOnClose', this, true);
+    }
+
     /**
      * @param {Object} [_ev]
      */
     async close(_ev) {
-        try {
-            await new Promise((success, reject) => {
-                return this.destroy({
-                    success,
-                    error: (_m, e) => reject(e),
+        if (await this.shouldDestroyOnClose()) {
+            try {
+                await new Promise((success, reject) => {
+                    return this.destroy({
+                        success,
+                        error: (_m, e) => reject(e),
+                    });
                 });
-            });
-        } catch (e) {
-            log.debug(e);
-        } finally {
-            if (api.settings.get('clear_messages_on_reconnection')) {
-                await this.clearMessages();
+            } catch (e) {
+                log.debug(e);
             }
+        } else {
+            u.safeSave(this, { closed: true });
+        }
+
+        if (api.settings.get('clear_messages_on_reconnection')) {
+            await this.clearMessages();
         }
         /**
          * Triggered once a chatbox has been closed.

+ 1 - 0
src/headless/types/shared/chatbox.d.ts

@@ -139,6 +139,7 @@ export default class ChatBoxBase extends ChatBoxBase_base {
      * @param {boolean} force
      */
     maybeShow(force: boolean): this;
+    shouldDestroyOnClose(): Promise<any>;
     /**
      * @param {Object} [_ev]
      */

+ 1 - 1
src/plugins/bookmark-views/tests/bookmarks-list.js

@@ -18,7 +18,7 @@ describe("The bookmarks list modal", function () {
         );
         mock.openControlBox(_converse);
 
-        const controlbox = _converse.chatboxviews.get('controlbox');
+        const controlbox = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
         const button = await u.waitUntil(() => controlbox.querySelector('.show-bookmark-list-modal'));
         button.click();
 

+ 2 - 2
src/plugins/bookmark-views/tests/bookmarks.js

@@ -108,7 +108,7 @@ describe("Bookmarks", function () {
             ['Second bookmark', "The Play's the Thing", 'Yet another bookmark']
         );
         expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined();
-        expect(Object.keys(_converse.chatboxviews.getAll()).length).toBe(2);
+        expect(Object.keys(_converse.chatboxviews.getAll()).length).toBe(1);
 
         // Check that MUC is left when autojoin is set to false
         stanza = stx`<message from="romeo@montague.lit"
@@ -143,7 +143,7 @@ describe("Bookmarks", function () {
             ['Second bookmark', "The Play's the Thing", 'Yet another bookmark']
         );
         expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).toBeUndefined();
-        expect(Object.keys(_converse.chatboxviews.getAll()).length).toBe(1);
+        expect(Object.keys(_converse.chatboxviews.getAll()).length).toBe(0);
     }));
 
     it("can be retrieved from the XMPP server", mock.initConverse(

+ 2 - 3
src/plugins/bookmark-views/tests/deprecated.js

@@ -72,7 +72,7 @@ describe("Bookmarks", function () {
             ['Second bookmark', "The Play's the Thing", 'Yet another bookmark']
         );
         expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined();
-        expect(Object.keys(_converse.chatboxviews.getAll()).length).toBe(2);
+        expect(Object.keys(_converse.chatboxviews.getAll()).length).toBe(1);
     }));
 
 
@@ -144,8 +144,7 @@ describe("The bookmarks list modal", function () {
             ['http://jabber.org/protocol/pubsub#publish-options'],
         );
         mock.openControlBox(_converse);
-
-        const controlbox = _converse.chatboxviews.get('controlbox');
+        const controlbox = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
         const button = await u.waitUntil(() => controlbox.querySelector('.show-bookmark-list-modal'));
         button.click();
 

+ 29 - 20
src/plugins/chatboxviews/templates/chats.js

@@ -1,60 +1,69 @@
-import { html } from 'lit';
+import { html, nothing } from 'lit';
 import { repeat } from 'lit/directives/repeat.js';
 import { _converse, api, constants } from '@converse/headless';
+import { isMobileViewport } from 'shared/chat/utils';
 
 const { CONTROLBOX_TYPE, CHATROOMS_TYPE, HEADLINES_TYPE, CONNECTION_STATUS } = constants;
 
+/**
+ * @param {import('@converse/headless/types/shared/chatbox').default} c
+ */
 function shouldShowChat(c) {
     const is_minimized = api.settings.get('view_mode') === 'overlayed' && c.get('hidden');
-    return c.get('type') === CONTROLBOX_TYPE || !(c.get('hidden') || is_minimized);
+    return c.get('type') === CONTROLBOX_TYPE || (!c.get('hidden') && !c.get('closed') && !is_minimized);
 }
 
 export default () => {
     const { chatboxes, connfeedback } = _converse.state;
     const view_mode = api.settings.get('view_mode');
+    const is_overlayed = view_mode === 'overlayed';
+    const is_mobile = isMobileViewport();
     const connection = api.connection.get();
     const logged_out = !connection?.connected || !connection?.authenticated || connection?.disconnecting;
     const connection_status = connfeedback.get('connection_status');
     const connecting = ['CONNECTED', 'CONNECTING', 'AUTHENTICATING', 'RECONNECTING'].includes(
         CONNECTION_STATUS[connection_status]
     );
-
     return html`
-        ${!logged_out && view_mode === 'overlayed'
+        ${!logged_out && is_overlayed
             ? html`<converse-minimized-chats class="col-auto"></converse-minimized-chats>`
             : ''}
         ${repeat(
             chatboxes.filter(shouldShowChat),
             (m) => m.get('jid'),
             (m) => {
+                const width = m.get('width');
+                const style = !is_mobile && is_overlayed && width ? `width: ${width}px` : nothing;
                 if (m.get('type') === CONTROLBOX_TYPE) {
-                    return html`
-                        ${view_mode === 'overlayed'
-                            ? html`<converse-controlbox-toggle
-                                  class="${!m.get('closed') ? 'hidden' : 'col-auto'}"
-                              ></converse-controlbox-toggle>`
-                            : ''}
-                        <converse-controlbox
-                            id="controlbox"
-                            class="col-auto chatbox ${view_mode === 'overlayed' && m.get('closed')
-                                ? 'hidden'
-                                : ''} ${logged_out && !connecting ? 'logged-out' : ''}"
-                            style="${m.get('width') ? `width: ${m.get('width')}` : ''}"
-                        ></converse-controlbox>
-                    `;
+                    return is_overlayed && m.get('closed')
+                        ? html`<converse-controlbox-toggle class="col-auto"></converse-controlbox-toggle>`
+                        : html`<converse-controlbox
+                              id="controlbox"
+                              class="col-auto chatbox ${logged_out && !connecting ? 'logged-out' : ''}"
+                              style="${style}"
+                          ></converse-controlbox>`;
                 } else if (m.get('type') === CHATROOMS_TYPE) {
                     return html`
-                        <converse-muc jid="${m.get('jid')}" class="col-auto chatbox chatroom"></converse-muc>
+                        <converse-muc
+                            jid="${m.get('jid')}"
+                            class="col-auto chatbox chatroom"
+                            style="${style}"
+                        ></converse-muc>
                     `;
                 } else if (m.get('type') === HEADLINES_TYPE) {
                     return html`
                         <converse-headlines
                             jid="${m.get('jid')}"
                             class="col-auto chatbox headlines"
+                            style="${style}"
                         ></converse-headlines>
                     `;
                 } else {
-                    return html`<converse-chat jid="${m.get('jid')}" class="col-auto chatbox"></converse-chat> `;
+                    return html`<converse-chat
+                        jid="${m.get('jid')}"
+                        class="col-auto chatbox"
+                        style="${style}"
+                    ></converse-chat> `;
                 }
             }
         )}

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

@@ -1,6 +1,6 @@
 import { api, _converse } from '@converse/headless';
-import tplChats from './templates/chats.js';
 import { CustomElement } from 'shared/components/element.js';
+import tplChats from './templates/chats.js';
 
 
 class ConverseChats extends CustomElement {

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

@@ -1,5 +1,6 @@
-import { html } from 'lit';
+import { html, nothing } from 'lit';
 import { api, constants } from '@converse/headless';
+import { getChatStyle } from 'shared/chat/utils';
 
 const { CHATROOMS_TYPE } = constants;
 
@@ -9,9 +10,11 @@ const { CHATROOMS_TYPE } = constants;
 export default (el) => {
     const help_messages = el.getHelpMessages();
     const show_help_messages = el.model.get('show_help_messages');
+    const is_overlayed = api.settings.get('view_mode') === 'overlayed';
+    const style = getChatStyle(el.model);
     return html`
-        <div class="flyout box-flyout">
-            ${api.settings.get('view_mode') === 'overlayed' ? html`<converse-dragresize></converse-dragresize>` : ''}
+        <div class="flyout box-flyout" style="${style || nothing}">
+            ${is_overlayed ? html`<converse-dragresize></converse-dragresize>` : ''}
             ${el.model
                 ? html`
                       <converse-chat-heading

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

@@ -117,7 +117,7 @@ describe("Chatboxes", function () {
             api.connection.get()._dataRecv(mock.createRequest(stanza));
             await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve));
             await u.waitUntil(() => message_promise);
-            expect(_converse.chatboxviews.keys().length).toBe(2);
+            expect(_converse.chatboxviews.keys().length).toBe(1);
             expect(_converse.chatboxviews.keys().pop()).toBe(sender_jid);
         }));
 
@@ -134,7 +134,7 @@ describe("Chatboxes", function () {
             const message_promise = new Promise(resolve => _converse.api.listen.on('message', resolve))
             api.connection.get()._dataRecv(mock.createRequest(stanza));
             await u.waitUntil(() => message_promise);
-            expect(_converse.chatboxviews.keys().length).toBe(1);
+            expect(_converse.chatboxviews.keys().length).toBe(0);
         }));
 
         it("is focused if its already open and you click on its corresponding roster item",
@@ -323,7 +323,7 @@ describe("Chatboxes", function () {
                 api.settings.set('visible_toolbar_buttons', Object.assign({}, buttons, {'call': true}));
 
                 await mock.openChatBoxFor(_converse, contact_jid);
-                view = _converse.chatboxviews.get(contact_jid);
+                view = await u.waitUntil(() => _converse.chatboxviews.get(contact_jid));
                 toolbar = view.querySelector('.chat-toolbar');
                 call_button = toolbar.querySelector('.toggle-call');
                 call_button.click();

+ 0 - 16
src/plugins/controlbox/controlbox.js

@@ -20,8 +20,6 @@ class ControlBoxView extends CustomElement {
         if (this.model.get('connected') && this.model.get('closed') === undefined) {
             this.model.set('closed', !api.settings.get('show_controlbox_by_default'));
         }
-        this.requestUpdate();
-
         /**
          * Triggered when the _converse.ControlBoxView has been initialized and therefore
          * exists. The controlbox contains the login and register forms when the user is
@@ -38,10 +36,6 @@ class ControlBoxView extends CustomElement {
         this.listenTo(_converse.state.connfeedback, 'change:connection_status', () => this.requestUpdate());
         this.listenTo(this.model, 'change:active-form', () => this.requestUpdate());
         this.listenTo(this.model, 'change:connected', () => this.requestUpdate());
-        this.listenTo(this.model, 'change:closed', () => {
-            this.requestUpdate();
-            if (!this.model.get('closed')) this.afterShown();
-        });
         this.requestUpdate();
     }
 
@@ -68,16 +62,6 @@ class ControlBoxView extends CustomElement {
         api.trigger('controlBoxClosed', this);
         return this;
     }
-
-    afterShown() {
-        /**
-         * Triggered once the controlbox has been opened
-         * @event _converse#controlBoxOpened
-         * @type {ControlBoxView}
-         */
-        api.trigger('controlBoxOpened', this);
-        return this;
-    }
 }
 
 api.elements.define('converse-controlbox', ControlBoxView);

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

@@ -1,5 +1,6 @@
+import { html, nothing } from 'lit';
 import { _converse, api, converse, constants } from '@converse/headless';
-import { html } from 'lit';
+import { getChatStyle } from 'shared/chat/utils.js';
 
 const { Strophe } = converse.env;
 const { ANONYMOUS } = constants;
@@ -13,7 +14,6 @@ function whenNotConnected(el) {
     const connecting = [Strophe.Status.RECONNECTING, Strophe.Status.CONNECTING].includes(connection_status);
     const view_mode = api.settings.get('view_mode');
     const show_bg = api.settings.get('show_background');
-
     return html`
         ${show_bg && view_mode === 'fullscreen' ? html`<converse-bg></converse-bg>` : ''}
         <converse-controlbox-buttons class="controlbox-padded"></converse-controlbox-buttons>
@@ -34,7 +34,8 @@ function whenNotConnected(el) {
  * @param {import('../controlbox').default} el
  */
 export default (el) => {
-    return html`<div class="flyout box-flyout">
+    const style = getChatStyle(el.model);
+    return html`<div class="flyout box-flyout" style="${style || nothing}">
         <converse-dragresize></converse-dragresize>
         ${el.model.get('connected')
             ? html`<converse-user-profile></converse-user-profile>

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

@@ -6,11 +6,10 @@ const u = converse.env.utils;
 describe("The Controlbox", function () {
 
     it("can be opened by clicking a DOM element with class 'toggle-controlbox'",
-            mock.initConverse([], {}, async function (_converse) {
+            mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
         spyOn(_converse.api, "trigger").and.callThrough();
         document.querySelector('.toggle-controlbox').click();
-        expect(_converse.api.trigger).toHaveBeenCalledWith('controlBoxOpened', jasmine.any(Object));
         const el = await u.waitUntil(() => document.querySelector("#controlbox"));
         expect(u.isVisible(el)).toBe(true);
     }));
@@ -25,7 +24,8 @@ describe("The Controlbox", function () {
         spyOn(view, 'close').and.callThrough();
         spyOn(_converse.api, "trigger").and.callThrough();
 
-        view.querySelector(".controlbox-heading__btn.close").click();
+        const button = await u.waitUntil(() => view.querySelector(".controlbox-heading__btn.close"));
+        button.click();
         expect(view.close).toHaveBeenCalled();
         expect(_converse.api.trigger).toHaveBeenCalledWith('controlBoxClosed', jasmine.any(Object));
     }));

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

@@ -11,8 +11,8 @@ describe("The Login Form", function () {
               allow_registration: false },
             async function (_converse) {
 
-        const cbview = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
         mock.toggleControlBox();
+        const cbview = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
         await u.waitUntil(() => cbview.querySelectorAll('input[type="checkbox"]').length);
 
         const checkboxes = cbview.querySelectorAll('input[type="checkbox"]');
@@ -46,8 +46,8 @@ describe("The Login Form", function () {
               allow_registration: false },
             async function (_converse) {
 
-        const cbview = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'))
         mock.toggleControlBox();
+        const cbview = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'))
         await u.waitUntil(() => cbview.querySelectorAll('input[type="checkbox"]').length);
 
         const checkboxes = cbview.querySelectorAll('input[type="checkbox"]');

+ 2 - 1
src/plugins/disco-views/tests/disco-browser.js

@@ -9,7 +9,8 @@ describe('DiscoBrowser', function () {
             const { api } = _converse;
             await mock.openControlBox(_converse);
             const cbview = _converse.chatboxviews.get('controlbox');
-            cbview.querySelector('a.show-client-info')?.click();
+            const button = await u.waitUntil(() => cbview.querySelector('a.show-client-info'));
+            button.click();
             const modal = api.modal.get('converse-user-settings-modal');
             modal.tab = 'disco';
             await u.waitUntil(() => modal.querySelector('converse-disco-browser'));

+ 2 - 1
src/plugins/dragresize/index.js

@@ -1,5 +1,6 @@
 import './components/dragresize.js';
 import {
+    shouldDestroyOnClose,
     initializeDragResize,
     dragresizeOverIframeHandler,
     registerGlobalEventHandlers,
@@ -36,12 +37,12 @@ converse.plugins.add('converse-dragresize', {
         api.listen.on('headlinesFeedInitialized', initializeDragResize);
         api.listen.on('chatBoxInitialized', initializeDragResize);
         api.listen.on('chatRoomInitialized', initializeDragResize);
-
         api.listen.on('registeredGlobalEventHandlers', registerGlobalEventHandlers);
         api.listen.on('unregisteredGlobalEventHandlers', unregisterGlobalEventHandlers);
         api.listen.on('beforeShowingChatView', (view) => view.initDragResize().setDimensions());
         api.listen.on('startDiagonalResize', dragresizeOverIframeHandler);
         api.listen.on('startHorizontalResize', dragresizeOverIframeHandler);
         api.listen.on('startVerticalResize', dragresizeOverIframeHandler);
+        api.listen.on('shouldDestroyOnClose', shouldDestroyOnClose);
     },
 });

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

@@ -1,5 +1,6 @@
 import debounce from 'lodash-es/debounce';
 import { api } from '@converse/headless';
+import log from '@converse/log';
 import { applyDragResistance, getResizingDirection } from './utils.js';
 
 const DragResizableMixin = {
@@ -47,13 +48,12 @@ const DragResizableMixin = {
             diff = ev.pageY - this.prev_pageY;
 
             if (diff) {
-
                 const new_height = this.height - diff;
-                console.log('------------');
-                console.log(`window.innerHeight: ${window.innerHeight}`);
-                console.log(`max_height: ${max_height}`);
-                console.log(`new_height: ${new_height}`);
-                console.log(`diff: ${diff}`);
+                log.debug('------------');
+                log.debug(`window.innerHeight: ${window.innerHeight}`);
+                log.debug(`max_height: ${max_height}`);
+                log.debug(`new_height: ${new_height}`);
+                log.debug(`diff: ${diff}`);
 
                 this.height =
                     this.height - diff > (this.model.get('min_height') || 0)

+ 15 - 4
src/plugins/dragresize/utils.js

@@ -178,12 +178,23 @@ export function onMouseUp(ev) {
     const height = applyDragResistance(resizing.chatbox.height, resizing.chatbox.model.get('default_height'));
     const width = applyDragResistance(resizing.chatbox.width, resizing.chatbox.model.get('default_width'));
     if (api.connection.connected()) {
-        resizing.chatbox.model.save({ 'height': height });
-        resizing.chatbox.model.save({ 'width': width });
+        resizing.chatbox.model.save({ height });
+        resizing.chatbox.model.save({ width });
     } else {
-        resizing.chatbox.model.set({ 'height': height });
-        resizing.chatbox.model.set({ 'width': width });
+        resizing.chatbox.model.set({ height });
+        resizing.chatbox.model.set({ width });
     }
     delete resizing.chatbox;
     delete resizing.direction;
 }
+
+/**
+ * @param {import('@converse/headless/types/shared/chatbox').default} chatbox
+ * @param {boolean} should_destroy
+ */
+export function shouldDestroyOnClose(chatbox, should_destroy) {
+    if (chatbox.get('height') || chatbox.get('width')) {
+        return false;
+    }
+    return should_destroy;
+}

+ 9 - 2
src/plugins/headlines-view/templates/feeds-list.js

@@ -4,6 +4,10 @@ import { constants } from '@converse/headless';
 
 const { HEADLINES_TYPE } = constants;
 
+/**
+ * @param {import('../feed-list').HeadlinesFeedsList} el
+ * @param {import('@converse/headless/types/plugins/headlines/feed').default} feed
+ */
 function tplHeadlinesFeedsListItem(el, feed) {
     const open_title = __('Click to open this server message');
     return html`
@@ -12,7 +16,7 @@ function tplHeadlinesFeedsListItem(el, feed) {
                 class="list-item-link open-headline available-room w-100"
                 data-headline-jid="${feed.get('jid')}"
                 title="${open_title}"
-                @click=${(ev) => el.openHeadline(ev)}
+                @click="${(ev) => el.openHeadline(ev)}"
                 href="#"
                 >${feed.get('jid')}</a
             >
@@ -20,10 +24,13 @@ function tplHeadlinesFeedsListItem(el, feed) {
     `;
 }
 
+/**
+ * @param {import('../feed-list').HeadlinesFeedsList} el
+ */
 export default (el) => {
     const feeds = el.model.filter((m) => m.get('type') === HEADLINES_TYPE);
     const heading_headline = __('Announcements');
-    return html` <div class="controlbox-section" id="headline">
+    return html`<div class="controlbox-section" id="headline">
             <div class="d-flex controlbox-padded ${feeds.length ? '' : 'hidden'}">
                 <span class="w-100 controlbox-heading controlbox-heading--headline" role="heading" aria-level="3"
                     >${heading_headline}</span

+ 8 - 9
src/plugins/headlines-view/templates/headlines.js

@@ -1,17 +1,16 @@
+import { html, nothing } from 'lit';
+import { getChatStyle } from 'shared/chat/utils.js';
 import '../heading.js';
-import { html } from 'lit';
 
 /**
  * @param {import('../view').default} el
  */
-export default (el) => html`
-    <div class="flyout box-flyout">
+export default (el) => {
+    const style = getChatStyle(el.model);
+    return html`<div class="flyout box-flyout" style="${style || nothing}">
         <converse-dragresize></converse-dragresize>
         ${el.model
-            ? html`<converse-headlines-heading
-                      jid="${el.model.get('jid')}"
-                      class="chat-head chat-head-chatbox row g-0"
-                  >
+            ? html`<converse-headlines-heading jid="${el.model.get('jid')}" class="chat-head chat-head-chatbox row g-0">
                   </converse-headlines-heading>
                   <div class="chat-body">
                       <div class="chat-content" aria-live="polite">
@@ -19,5 +18,5 @@ export default (el) => html`
                       </div>
                   </div>`
             : ''}
-    </div>
-`;
+    </div> `;
+};

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

@@ -60,7 +60,7 @@ converse.plugins.add('converse-minimize', {
 
         api.listen.on('chatBoxViewInitialized', view => trimChats(view));
         api.listen.on('chatRoomViewInitialized', view => trimChats(view));
-        api.listen.on('controlBoxOpened', view => trimChats(view));
+        api.listen.on('controlBoxInitialized', view => trimChats(view));
         api.listen.on('chatBoxInitialized', initializeChat);
         api.listen.on('chatRoomInitialized', initializeChat);
 

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

@@ -6,10 +6,9 @@
  * @typedef {import('plugins/controlbox/controlbox').default} ControlBoxView
  * @typedef {import('plugins/headlines-view/view').default} HeadlinesFeedView
  */
-import { _converse, api, converse, u, constants } from '@converse/headless';
+import { _converse, api, u, constants } from '@converse/headless';
 import { __ } from 'i18n';
 
-const { dayjs } = converse.env;
 const { ACTIVE } = constants;
 
 /**

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

@@ -37,7 +37,7 @@ export default class MUCHeading extends CustomElement {
     }
 
     render () {
-        return (this.model && this.user_settings) ? tplMUCHead(this) : '';
+        return this.model ? tplMUCHead(this) : '';
     }
 
     /**

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

@@ -1,6 +1,6 @@
+import { _converse, api, converse } from '@converse/headless';
 import BaseChatView from 'shared/chat/baseview.js';
 import tplMuc from './templates/muc.js';
-import { _converse, api, converse } from '@converse/headless';
 
 
 export default class MUCView extends BaseChatView {

+ 14 - 12
src/plugins/muc-views/templates/muc.js

@@ -1,5 +1,6 @@
-import { html } from "lit";
+import { html, nothing } from 'lit';
 import { api } from '@converse/headless';
+import { getChatStyle } from 'shared/chat/utils.js';
 import { getChatRoomBodyTemplate } from '../utils.js';
 import '../chatarea.js';
 import '../destroyed.js';
@@ -8,18 +9,19 @@ import '../heading.js';
 import '../nickname-form.js';
 import '../password-form.js';
 
-
 /**
  * @param {import('../muc').default} el
  */
 export default (el) => {
-    return html`
-        <div class="flyout box-flyout">
-            ${ api.settings.get('view_mode') === 'overlayed' ? html`<converse-dragresize></converse-dragresize>` : '' }
-            ${ el.model ? html`
-                <converse-muc-heading jid="${el.model.get('jid')}" class="chat-head chat-head-chatroom row g-0">
-                </converse-muc-heading>
-                <div class="chat-body chatroom-body row g-0">${getChatRoomBodyTemplate(el.model)}</div>
-            ` : '' }
-        </div>`;
-}
+    const style = el.model ? getChatStyle(el.model) : '';
+    return html` <div class="flyout box-flyout" style="${style || nothing}">
+        ${api.settings.get('view_mode') === 'overlayed' ? html`<converse-dragresize></converse-dragresize>` : ''}
+        ${el.model
+            ? html`
+                  <converse-muc-heading jid="${el.model.get('jid')}" class="chat-head chat-head-chatroom row g-0">
+                  </converse-muc-heading>
+                  <div class="chat-body chatroom-body row g-0">${getChatRoomBodyTemplate(el.model)}</div>
+              `
+            : ''}
+    </div>`;
+};

+ 9 - 6
src/plugins/muc-views/tests/muc-list-modal.js

@@ -7,8 +7,9 @@ describe('The "Groupchats" List modal', function () {
     it('can be opened from a link in the "Groupchats" section of the controlbox',
         mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
             await mock.openControlBox(_converse);
-            const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
-            roomspanel.querySelector('.show-list-muc-modal').click();
+            const cbview = _converse.chatboxviews.get('controlbox');
+            const button = await u.waitUntil(() => cbview.querySelector('converse-rooms-list .show-list-muc-modal'));
+            button.click();
             mock.closeControlBox(_converse);
             const modal = _converse.api.modal.get('converse-muc-list-modal');
             await u.waitUntil(() => u.isVisible(modal), 1000);
@@ -87,8 +88,9 @@ describe('The "Groupchats" List modal', function () {
     it('is pre-filled with the muc_domain',
         mock.initConverse(['chatBoxesFetched'], { 'muc_domain': 'muc.example.org' }, async function (_converse) {
             await mock.openControlBox(_converse);
-            const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
-            roomspanel.querySelector('.show-list-muc-modal').click();
+            const cbview = _converse.chatboxviews.get('controlbox');
+            const button = await u.waitUntil(() => cbview.querySelector('converse-rooms-list .show-list-muc-modal'));
+            button.click();
             mock.closeControlBox(_converse);
             const modal = _converse.api.modal.get('converse-muc-list-modal');
             await u.waitUntil(() => u.isVisible(modal), 1000);
@@ -103,8 +105,9 @@ describe('The "Groupchats" List modal', function () {
             { 'muc_domain': 'chat.shakespeare.lit', 'locked_muc_domain': true },
             async function (_converse) {
                 await mock.openControlBox(_converse);
-                const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
-                roomspanel.querySelector('.show-list-muc-modal').click();
+                const cbview = _converse.chatboxviews.get('controlbox');
+                const button = await u.waitUntil(() => cbview.querySelector('converse-rooms-list .show-list-muc-modal'));
+                button.click();
                 mock.closeControlBox(_converse);
                 const modal = _converse.api.modal.get('converse-muc-list-modal');
                 await u.waitUntil(() => u.isVisible(modal), 1000);

+ 1 - 1
src/plugins/notifications/tests/notification.js

@@ -89,7 +89,7 @@ describe("Notifications", function () {
                         .c('url').t('imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18');
                     _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
-                    await u.waitUntil(() => _converse.chatboxviews.keys().length === 2);
+                    await u.waitUntil(() => _converse.chatboxviews.keys().length === 1);
                     expect(_converse.chatboxviews.keys().includes('notify.example.com')).toBeTruthy();
                     expect(window.Notification).toHaveBeenCalled();
                 }));

+ 7 - 7
src/plugins/register/tests/register.js

@@ -19,8 +19,8 @@ describe("The Registration Form", function () {
               allow_registration: false },
             async function (_converse) {
 
-        await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
-        const cbview = _converse.api.controlbox.get();
+        await mock.toggleControlBox();
+        const cbview = await u.waitUntil(() => _converse.api.controlbox.get());
         expect(cbview.querySelectorAll('a.register-account').length).toBe(0);
     }));
 
@@ -38,7 +38,7 @@ describe("The Registration Form", function () {
             }
             toggle.click();
         }
-        const cbview = _converse.chatboxviews.get('controlbox');
+        const cbview = await u.waitUntil(() => _converse.api.controlbox.get());
         expect(cbview.querySelector('converse-registration-form')).toBe(null);
 
         const register_link = await u.waitUntil(() => cbview.querySelector('a.register-account'));
@@ -61,7 +61,7 @@ describe("The Registration Form", function () {
         const toggle = await u.waitUntil(() => document.querySelector(".toggle-controlbox"));
         toggle.click();
 
-        const cbview = _converse.api.controlbox.get();
+        const cbview = await u.waitUntil(() => _converse.api.controlbox.get());
         await u.waitUntil(() => u.isVisible(cbview));
 
         cbview.querySelector('.toggle-register-login').click();
@@ -215,7 +215,7 @@ describe("The Registration Form", function () {
             }
             toggle.click();
         }
-        const cbview = _converse.api.controlbox.get();
+        const cbview = await u.waitUntil(() => _converse.api.controlbox.get());
         const login_form = await u.waitUntil(() => cbview.querySelector('.toggle-register-login'));
         login_form.click();
 
@@ -277,7 +277,7 @@ describe("The Registration Form", function () {
             if (!u.isVisible(toggle)) u.removeClass('hidden', toggle);
             toggle.click();
         }
-        const cbview = _converse.api.controlbox.get();
+        const cbview = await u.waitUntil(() => _converse.api.controlbox.get());
         const login_form = await u.waitUntil(() => cbview.querySelector('.toggle-register-login'));
         login_form.click();
 
@@ -360,7 +360,7 @@ describe("The Registration Form", function () {
             }
             toggle.click();
         }
-        const cbview = _converse.chatboxviews.get('controlbox');
+        const cbview = await u.waitUntil(() => _converse.api.controlbox.get());
         const login_form = await u.waitUntil(() => cbview.querySelector('.toggle-register-login'));
         login_form.click();
 

+ 1 - 1
src/plugins/rosterview/tests/add-contact-modal.js

@@ -67,7 +67,7 @@ describe("The 'Add Contact' widget", function () {
         });
 
         await mock.openControlBox(_converse);
-        const cbview = _converse.chatboxviews.get('controlbox');
+        const cbview = await u.waitUntil(() => _converse.api.controlbox.get());
         cbview.querySelector('.add-contact').click()
         const modal = _converse.api.modal.get('converse-add-contact-modal');
         await u.waitUntil(() => u.isVisible(modal), 1000);

+ 1 - 1
src/plugins/rosterview/tests/presence.js

@@ -15,7 +15,7 @@ describe("A sent presence stanza", function () {
         mock.openControlBox(_converse);
         spyOn(_converse.api.connection.get(), 'send').and.callThrough();
 
-        const cbview = _converse.chatboxviews.get('controlbox');
+        const cbview = await u.waitUntil(() => _converse.api.controlbox.get());
         const change_status_el = await u.waitUntil(() => cbview.querySelector('.change-status'));
         change_status_el.click()
         let modal = _converse.api.modal.get('converse-profile-modal');

+ 1 - 2
src/plugins/rosterview/tests/protocol.js

@@ -48,11 +48,10 @@ describe("Presence subscriptions", function () {
              * the interaction between roster items and subscription states.
              */
             mock.openControlBox(_converse);
-            const cbview = _converse.chatboxviews.get('controlbox');
-
             spyOn(_converse.roster, "sendContactAddIQ").and.callThrough();
             spyOn(_converse.api.vcard, "get").and.callThrough();
 
+            const cbview = await u.waitUntil(() => _converse.api.controlbox.get());
             const add_contact_button = await u.waitUntil(() => cbview.querySelector('.add-contact'));
             add_contact_button.click()
 

+ 1 - 1
src/plugins/rosterview/tests/roster.js

@@ -916,7 +916,7 @@ describe("The Contacts Roster", function () {
 
             await mock.waitForRoster(_converse, 'current', 1);
             await mock.openControlBox(_converse);
-            const icon_el = document.querySelector('converse-roster-contact converse-icon');
+            const icon_el = await u.waitUntil(() => document.querySelector('converse-roster-contact converse-icon'));
             expect(icon_el.getAttribute('color')).toBe('var(--chat-status-offline)');
 
             let pres = stx`<presence from="mercutio@montague.lit/resource" xmlns="jabber:client"/>`;

+ 14 - 0
src/shared/chat/utils.js

@@ -13,6 +13,20 @@ import tplNewDay from './templates/new-day.js';
 const { dayjs, u } = converse.env;
 const { convertASCII2Emoji, getShortnameReferences, getCodePointReferences } = u;
 
+export function isMobileViewport() {
+    return window.innerWidth <= 768;
+}
+
+/**
+ * @param {import('@converse/headless/types/shared/chatbox').default} model
+ */
+export function getChatStyle(model) {
+    if (isMobileViewport()) return '';
+    const { height, width } = model.toJSON();
+    const is_overlayed = api.settings.get('view_mode') === 'overlayed';
+    return is_overlayed ? `${width ? `width: ${width}px;` : ''}${height ? `height: ${height}px;` : ''}` : '';
+}
+
 /**
  * @param {Model} model
  */

+ 3 - 3
src/shared/modals/templates/user-details.js

@@ -201,7 +201,7 @@ export function tplUserDetailsModal(el) {
         );
     }
 
-    const name = contact?.get('nickname') || contact.vcard?.get('fullname');
+    const name = contact?.get('nickname') || contact?.vcard?.get('fullname');
     const groups = contact?.get('groups') || [];
 
     return html`
@@ -289,8 +289,8 @@ export function tplUserDetailsModal(el) {
                           </div>
                       `
                     : ''}
-                ${contact.get('requesting') || !is_roster_contact || !contact ? html`<hr />` : ''}
-                ${contact.get('requesting')
+                ${contact?.get('requesting') || !is_roster_contact || !contact ? html`<hr />` : ''}
+                ${contact?.get('requesting')
                     ? html`<div class="row mb-2">
                           <div class="col-sm-4"><label>${__('Contact Request')}:</label></div>
                           <div class="col-sm-8">${tplAcceptButton(el)} ${tplDeclineButton(el)}</div>

+ 2 - 3
src/shared/modals/user-details.js

@@ -29,9 +29,7 @@ export default class UserDetailsModal extends BaseModal {
 
         if (this.model instanceof _converse.exports.ChatBox) {
             this.model.rosterContactAdded.then(() => this.registerContactEventHandlers(this.model.contact));
-            if (this.model.contact !== undefined) {
-                this.registerContactEventHandlers(this.model.contact);
-            }
+            this.registerContactEventHandlers(this.model.contact);
         } else {
             this.registerContactEventHandlers(this.model);
         }
@@ -70,6 +68,7 @@ export default class UserDetailsModal extends BaseModal {
      * @param {import('@converse/headless/types/plugins/roster/contact').default} contact
      */
     registerContactEventHandlers(contact) {
+        if (!contact) return; // happens during tests
         this.listenTo(contact, 'change', () => this.requestUpdate());
         this.listenTo(contact, 'destroy', () => this.close());
         this.listenTo(contact.vcard, 'change', () => this.requestUpdate());

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

@@ -419,7 +419,7 @@ async function receiveOwnMUCPresence (_converse, muc_jid, nick, affiliation='own
 
 async function openAddMUCModal (_converse) {
     await mock.openControlBox(_converse);
-    const controlbox = _converse.chatboxviews.get('controlbox');
+    const controlbox = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
     controlbox.querySelector('converse-rooms-list .show-add-muc-modal').click();
     const modal = _converse.api.modal.get('converse-add-muc-modal');
     await u.waitUntil(() => u.isVisible(modal), 1000);

+ 5 - 0
src/types/plugins/dragresize/utils.d.ts

@@ -46,6 +46,11 @@ export function onMouseMove(ev: MouseEvent): boolean;
  * @param {MouseEvent} ev
  */
 export function onMouseUp(ev: MouseEvent): boolean;
+/**
+ * @param {import('@converse/headless/types/shared/chatbox').default} chatbox
+ * @param {boolean} should_destroy
+ */
+export function shouldDestroyOnClose(chatbox: import("@converse/headless/types/shared/chatbox").default, should_destroy: boolean): boolean;
 export type ResizingData = {
     chatbox: HTMLElement;
     direction: string;

+ 1 - 1
src/types/plugins/headlines-view/templates/feeds-list.d.ts

@@ -1,3 +1,3 @@
-declare function _default(el: any): import("lit-html").TemplateResult<1>;
+declare function _default(el: import("../feed-list").HeadlinesFeedsList): import("lit-html").TemplateResult<1>;
 export default _default;
 //# sourceMappingURL=feeds-list.d.ts.map

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

@@ -1,3 +1,7 @@
+/**
+ * @param {import('@converse/headless/types/shared/chatbox').default} model
+ */
+export function getChatStyle(model: import("@converse/headless/types/shared/chatbox").default): string;
 /**
  * @param {Model} model
  */