Преглед изворни кода

Fix embedded, singleton mode.

It's now necessary to add a `converse-root` element in the DOM where you
want Converse to render (previously it was any element with the id
`#conversejs`).

Also, turned `converse-chats` element into a Lit element and re-render
`converse-root` and `converse-chats` when the `view-mode` or `singleton`
settings change. This is a step towards being able to change the view
mode on the fly and have the entire chat re-render appropriately.

Fixes #2647
JC Brand пре 3 година
родитељ
комит
84c6a0039c

+ 2 - 0
CHANGES.md

@@ -2,6 +2,8 @@
 
 ## 9.0.0 (Unreleased)
 
+- #2647: Singleton mode doesn't work
+
 - Emit a `change` event when a configuration setting changes
 - 3 New configuration settings:
   - [render_media](https://conversejs.org/docs/html/configuration.html#render-media)

+ 1 - 1
demo/embedded.html

@@ -72,7 +72,7 @@
 			</h1>
                         <p class="intro-text">Embedded MUC chat demo</p>
                         <div class="converse-container">
-                            <div id="conversejs"></div>
+                            <converse-root></converse-root>
                         </div>
                     </div>
                 </div>

+ 4 - 0
src/headless/core.js

@@ -828,6 +828,10 @@ Object.assign(converse, {
         await initClientConfig(_converse);
         await i18n.initialize();
         initPlugins(_converse);
+
+        // Register all custom elements
+        api.elements.register();
+
         registerGlobalEventHandlers(_converse);
 
         try {

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

@@ -1,4 +1,4 @@
-import _converse from '@converse/headless/shared/_converse';
+import { _converse } from '@converse/headless/core.js';
 import assignIn from 'lodash-es/assignIn';
 import isEqual from "lodash-es/isEqual.js";
 import isObject from 'lodash-es/isObject';
@@ -14,7 +14,6 @@ let app_settings;
 let init_settings = {}; // Container for settings passed in via converse.initialize
 let user_settings; // User settings, populated via api.users.settings
 
-
 export function getAppSettings () {
     return app_settings;
 }

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

@@ -205,7 +205,13 @@ describe("A chat room", function () {
                 [{'category': 'pubsub', 'type': 'pep'}],
                 ['http://jabber.org/protocol/pubsub#publish-options']
             );
-            await _converse.api.rooms.open(`lounge@montague.lit`);
+
+            const nick = 'romeo';
+            const muc_jid = 'lounge@montague.lit';
+            await _converse.api.rooms.open(muc_jid);
+            await mock.getRoomFeatures(_converse, muc_jid);
+            await mock.waitForReservedNick(_converse, muc_jid, nick);
+
             const view = _converse.chatboxviews.get('lounge@montague.lit');
             expect(view.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null);
             _converse.bookmarks.create({
@@ -225,8 +231,12 @@ describe("A chat room", function () {
             const { u, Strophe } = converse.env;
             await mock.waitForRoster(_converse, 'current', 0);
             await mock.waitUntilBookmarksReturned(_converse);
+            const nick = 'romeo';
             const muc_jid = 'theplay@conference.shakespeare.lit';
             await _converse.api.rooms.open(muc_jid);
+            await mock.getRoomFeatures(_converse, muc_jid);
+            await mock.waitForReservedNick(_converse, muc_jid, nick);
+
             const view = _converse.chatboxviews.get(muc_jid);
             await u.waitUntil(() => view.querySelector('.toggle-bookmark'));
 

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

@@ -17,11 +17,6 @@ converse.plugins.add('converse-chatboxviews', {
     dependencies: ['converse-chatboxes', 'converse-vcard'],
 
     initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by converse.js's plugin machinery.
-         */
-        api.elements.register();
-
         api.promises.add(['chatBoxViewsInitialized']);
 
         // Configuration values for this plugin

+ 0 - 1
src/plugins/chatboxviews/styles/chats.scss

@@ -36,7 +36,6 @@
                 bottom: auto;
                 height: 100%;
                 width: 100%;
-                margin-left: -15px;
             }
         }
     }

+ 18 - 14
src/plugins/chatboxviews/view.js

@@ -1,26 +1,31 @@
 import tpl_background_logo from '../../templates/background_logo.js';
 import tpl_chats from './templates/chats.js';
-import { ElementView } from '@converse/skeletor/src/element.js';
+import { CustomElement } from 'shared/components/element.js';
 import { api, _converse } from '@converse/headless/core';
+import { getAppSettings } from '@converse/headless/shared/settings/utils.js';
 import { render } from 'lit';
 
 
-class ConverseChats extends ElementView {
+class ConverseChats extends CustomElement {
 
     initialize () {
         this.model = _converse.chatboxes;
-        this.listenTo(this.model, 'add', this.render);
-        this.listenTo(this.model, 'change:closed', this.render);
-        this.listenTo(this.model, 'change:hidden', this.render);
-        this.listenTo(this.model, 'change:jid', this.render);
-        this.listenTo(this.model, 'change:minimized', this.render);
-        this.listenTo(this.model, 'destroy', this.render);
+        this.listenTo(this.model, 'add', () => this.requestUpdate());
+        this.listenTo(this.model, 'change:closed', () => this.requestUpdate());
+        this.listenTo(this.model, 'change:hidden', () => this.requestUpdate());
+        this.listenTo(this.model, 'change:jid', () => this.requestUpdate());
+        this.listenTo(this.model, 'change:minimized', () => this.requestUpdate());
+        this.listenTo(this.model, 'destroy', () => this.requestUpdate());
 
         // Use listenTo instead of api.listen.to so that event handlers
         // automatically get deregistered when the component is dismounted
-        this.listenTo(_converse, 'connected', this.render);
-        this.listenTo(_converse, 'reconnected', this.render);
-        this.listenTo(_converse, 'disconnected', this.render);
+        this.listenTo(_converse, 'connected', () => this.requestUpdate());
+        this.listenTo(_converse, 'reconnected', () => this.requestUpdate());
+        this.listenTo(_converse, 'disconnected', () => this.requestUpdate());
+
+        const settings = getAppSettings();
+        this.listenTo(settings, 'change:view_mode', () => this.requestUpdate())
+        this.listenTo(settings, 'change:singleton', () => this.requestUpdate())
 
         const bg = document.getElementById('conversejs-bg');
         if (bg && !bg.innerHTML.trim()) {
@@ -28,7 +33,6 @@ class ConverseChats extends ElementView {
         }
         const body = document.querySelector('body');
         body.classList.add(`converse-${api.settings.get('view_mode')}`);
-        this.render();
 
         /**
          * Triggered once the _converse.ChatBoxViews view-colleciton has been initialized
@@ -38,8 +42,8 @@ class ConverseChats extends ElementView {
         api.trigger('chatBoxViewsInitialized');
     }
 
-    render () {
-        render(tpl_chats(), this);
+    render () { // eslint-disable-line class-methods-use-this
+        return tpl_chats();
     }
 }
 

+ 36 - 22
src/plugins/chatview/styles/index.scss

@@ -101,28 +101,6 @@
                 box-shadow: none;
                 overflow: hidden;
             }
-            &:not(#controlbox) {
-                .box-flyout {
-                    @include media-breakpoint-up(md) {
-                        max-width: 66.666667%;
-                    }
-                    @include media-breakpoint-up(lg) {
-                        max-width: 75%;
-                    }
-                    @include media-breakpoint-up(xl) {
-                        max-width: 83.333333%;
-                    }
-                }
-            }
-            @include media-breakpoint-up(md) {
-                @include make-col(8);
-            }
-            @include media-breakpoint-up(lg) {
-                @include make-col(9);
-            }
-            @include media-breakpoint-up(xl) {
-                @include make-col(10);
-            }
         }
 
         &.converse-singleton {
@@ -134,6 +112,14 @@
             }
             .chatbox {
                 margin: 0;
+                position: relative;
+            }
+        }
+    }
+
+    converse-chats.converse-fullscreen  {
+        &.converse-singleton {
+            .chatbox {
                 @include make-col-ready();
                 @include media-breakpoint-up(md) {
                     @include make-col(12);
@@ -146,6 +132,34 @@
                 }
             }
         }
+
+        &:not(.converse-singleton) {
+            .chatbox {
+                @include media-breakpoint-up(md) {
+                    @include make-col(8);
+                }
+                @include media-breakpoint-up(lg) {
+                    @include make-col(9);
+                }
+                @include media-breakpoint-up(xl) {
+                    @include make-col(10);
+                }
+
+                &:not(#controlbox) {
+                    .box-flyout {
+                        @include media-breakpoint-up(md) {
+                            max-width: 66.666667%;
+                        }
+                        @include media-breakpoint-up(lg) {
+                            max-width: 75%;
+                        }
+                        @include media-breakpoint-up(xl) {
+                            max-width: 83.333333%;
+                        }
+                    }
+                }
+            }
+        }
     }
 
     converse-chats.converse-embedded {

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

@@ -9,12 +9,12 @@ const sizzle = converse.env.sizzle;
 describe("The Controlbox", function () {
 
     it("can be opened by clicking a DOM element with class 'toggle-controlbox'",
-            mock.initConverse([], {}, function (_converse) {
+            mock.initConverse([], {}, 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 = document.querySelector("#controlbox");
+        const el = await u.waitUntil(() => document.querySelector("#controlbox"));
         expect(u.isVisible(el)).toBe(true);
     }));
 

+ 1 - 0
src/plugins/fullscreen/styles/fullscreen.scss

@@ -1,4 +1,5 @@
 body.converse-fullscreen {
     margin: 0;
     background-color: var(--global-background-color);
+    overflow: hidden;
 }

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

@@ -71,14 +71,15 @@ describe("A Groupchat", function () {
     it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
             mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
-        await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
-        const view = _converse.chatboxviews.get('lounge@montague.lit');
+        const muc_jid = 'lounge@conference.shakespeare.lit';
+        await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+        const view = _converse.chatboxviews.get(muc_jid);
         spyOn(_converse.api, "trigger").and.callThrough();
         const button = await u.waitUntil(() => view.querySelector('.toggle-chatbox-button'));
         button.click();
 
         expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
-        expect(u.isVisible(view)).toBeFalsy();
+        await u.waitUntil(() => !u.isVisible(view));
         expect(view.model.get('minimized')).toBeTruthy();
         const el = await u.waitUntil(() => document.querySelector("converse-minimized-chat a.restore-chat"));
         el.click();
@@ -107,7 +108,7 @@ describe("A Chatbox", function () {
 
         expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
         expect(_converse.api.trigger.calls.count(), 2);
-        expect(u.isVisible(chatview)).toBeFalsy();
+        await u.waitUntil(() => !u.isVisible(chatview));
         expect(chatview.model.get('minimized')).toBeTruthy();
         const restore_el = await u.waitUntil(() => document.querySelector("converse-minimized-chat a.restore-chat"));
         restore_el.click();

+ 6 - 7
src/plugins/muc-views/tests/muc-api.js

@@ -100,7 +100,6 @@ describe("Groupchats", function () {
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
             const { api } = _converse;
-
             // Mock 'getDiscoInfo', otherwise the room won't be
             // displayed as it waits first for the features to be returned
             // (when it's a new room being created).
@@ -116,14 +115,14 @@ describe("Groupchats", function () {
             let room = await _converse.api.rooms.open(jid);
             // Test on groupchat that's not yet open
             expect(room instanceof Model).toBeTruthy();
-            chatroomview = _converse.chatboxviews.get(jid);
+            chatroomview = await u.waitUntil(() => _converse.chatboxviews.get(jid));
             expect(chatroomview.is_chatroom).toBeTruthy();
             await u.waitUntil(() => u.isVisible(chatroomview));
 
             // Test again, now that the room exists.
             room = await _converse.api.rooms.open(jid);
             expect(room instanceof Model).toBeTruthy();
-            chatroomview = _converse.chatboxviews.get(jid);
+            chatroomview = await u.waitUntil(() => _converse.chatboxviews.get(jid));
             expect(chatroomview.is_chatroom).toBeTruthy();
             expect(u.isVisible(chatroomview)).toBeTruthy();
             await chatroomview.close();
@@ -132,19 +131,19 @@ describe("Groupchats", function () {
             jid = 'Leisure@montague.lit';
             room = await _converse.api.rooms.open(jid);
             expect(room instanceof Model).toBeTruthy();
-            chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
+            chatroomview = await u.waitUntil(() => _converse.chatboxviews.get(jid.toLowerCase()));
             await u.waitUntil(() => u.isVisible(chatroomview));
 
             jid = 'leisure@montague.lit';
             room = await _converse.api.rooms.open(jid);
             expect(room instanceof Model).toBeTruthy();
-            chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
+            chatroomview = await u.waitUntil(() => _converse.chatboxviews.get(jid.toLowerCase()));
             await u.waitUntil(() => u.isVisible(chatroomview));
 
             jid = 'leiSure@montague.lit';
             room = await _converse.api.rooms.open(jid);
             expect(room instanceof Model).toBeTruthy();
-            chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
+            chatroomview = await u.waitUntil(() => _converse.chatboxviews.get(jid.toLowerCase()));
             await u.waitUntil(() => u.isVisible(chatroomview));
             chatroomview.close();
 
@@ -168,7 +167,6 @@ describe("Groupchats", function () {
                 }
             });
             expect(room instanceof Model).toBeTruthy();
-            chatroomview = _converse.chatboxviews.get('room@conference.example.org');
 
             // We pretend this is a new room, so no disco info is returned.
             const features_stanza = $iq({
@@ -247,6 +245,7 @@ describe("Groupchats", function () {
                 </query>
                 </iq>`);
 
+            chatroomview = _converse.chatboxviews.get('room@conference.example.org');
             spyOn(chatroomview.model, 'sendConfiguration').and.callThrough();
             _converse.connection._dataRecv(mock.createRequest(node));
             await u.waitUntil(() => chatroomview.model.sendConfiguration.calls.count() === 1);

+ 11 - 11
src/plugins/muc-views/tests/muc.js

@@ -1214,7 +1214,7 @@ describe("Groupchats", function () {
             });
 
             await _converse.api.rooms.open('coven@chat.shakespeare.lit', {'nick': 'some1'});
-            const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
+            const view = await u.waitUntil(() => _converse.chatboxviews.get('coven@chat.shakespeare.lit'));
             await u.waitUntil(() => u.isVisible(view));
             // We pretend this is a new room, so no disco info is returned.
             const features_stanza = $iq({
@@ -2275,7 +2275,7 @@ describe("Groupchats", function () {
              *  </presence>
              */
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
-            var presence = $pres().attrs({
+            const presence = $pres().attrs({
                     from:'lounge@montague.lit/romeo',
                     to:'romeo@montague.lit/pda',
                     type:'unavailable'
@@ -2335,16 +2335,16 @@ describe("Groupchats", function () {
         it("can be closed again by clicking a DOM element with class 'close-chatbox-button'",
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
-            await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
-            const view = _converse.chatboxviews.get('lounge@montague.lit');
-            spyOn(view.model, 'close').and.callThrough();
+            const model = await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
+            spyOn(model, 'close').and.callThrough();
             spyOn(_converse.api, "trigger").and.callThrough();
-            spyOn(view.model, 'leave');
+            spyOn(model, 'leave');
             spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
+            const view = await u.waitUntil(() => _converse.chatboxviews.get('lounge@montague.lit'));
             const button = await u.waitUntil(() => view.querySelector('.close-chatbox-button'));
             button.click();
-            await u.waitUntil(() => view.model.close.calls.count());
-            expect(view.model.leave).toHaveBeenCalled();
+            await u.waitUntil(() => model.close.calls.count());
+            expect(model.leave).toHaveBeenCalled();
             await u.waitUntil(() => _converse.api.trigger.calls.count());
             expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
         }));
@@ -3980,7 +3980,7 @@ describe("Groupchats", function () {
             var new_list = [];
             var old_list = [];
             const muc_utils = converse.env.muc_utils;
-            var delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list);
+            let delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list);
             expect(delta.length).toBe(0);
 
             new_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}];
@@ -4272,7 +4272,7 @@ describe("Groupchats", function () {
             rooms[4].querySelector('.open-room').click();
             await u.waitUntil(() => _converse.chatboxes.length > 1);
             expect(sizzle('.chatroom', _converse.el).filter(u.isVisible).length).toBe(1); // There should now be an open chatroom
-            var view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit');
+            const view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit');
             expect(view.querySelector('.chatbox-title__text').textContent.trim()).toBe("Macbeth's Castle");
         }));
 
@@ -4577,7 +4577,7 @@ describe("Groupchats", function () {
                 // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
 
                 // <composing> state
-                var msg = $msg({
+                let msg = $msg({
                         from: muc_jid+'/newguy',
                         id: u.getUniqueId(),
                         to: 'romeo@montague.lit',

+ 2 - 1
src/plugins/muc-views/tests/muclist.js

@@ -201,7 +201,6 @@ describe("A groupchat shown in the groupchats list", function () {
         await mock.waitForRoster(_converse, 'current', 0);
         await mock.openControlBox(_converse);
         await _converse.api.rooms.open(room_jid, {'nick': 'some1'});
-        const view = _converse.chatboxviews.get(room_jid);
 
         const selector = `iq[to="${room_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`;
         const features_query = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).pop());
@@ -233,6 +232,8 @@ describe("A groupchat shown in the groupchats list", function () {
                     .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'})
                         .c('value').t(0);
         _converse.connection._dataRecv(mock.createRequest(features_stanza));
+
+        const view = _converse.chatboxviews.get(room_jid);
         await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)
         let presence = $pres({
                 to: _converse.connection.jid,

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

@@ -1,4 +1,4 @@
-import './root.js';
+import ConverseRoot from './root.js';
 import { api, converse } from '@converse/headless/core';
 import { ensureElement } from './utils.js';
 
@@ -8,5 +8,10 @@ converse.plugins.add('converse-rootview', {
     initialize () {
         api.settings.extend({ 'auto_insert': true });
         api.listen.on('chatBoxesInitialized', ensureElement);
+
+        // Only define the element now, otherwise it it's already in the DOM
+        // before `converse.initialized` has been called it will render too
+        // early.
+        api.elements.define('converse-root', ConverseRoot);
     }
 });

+ 15 - 6
src/plugins/rootview/root.js

@@ -1,6 +1,9 @@
 import tpl_root from "./templates/root.js";
 import { api } from '@converse/headless/core';
 import { CustomElement } from 'shared/components/element.js';
+import { getAppSettings } from '@converse/headless/shared/settings/utils.js';
+
+import './styles/root.scss';
 
 
 /**
@@ -10,19 +13,25 @@ import { CustomElement } from 'shared/components/element.js';
  * It can be inserted into the DOM before or after Converse has loaded or been
  * initialized.
  */
-class ConverseRoot extends CustomElement {
+export default class ConverseRoot extends CustomElement {
 
     render () { // eslint-disable-line class-methods-use-this
         return tpl_root();
     }
 
-    connectedCallback () {
-        super.connectedCallback();
+    initialize () {
+        this.setAttribute('id', 'conversejs');
+        this.setClasses();
+        const settings = getAppSettings();
+        this.listenTo(settings, 'change:view_mode', () => this.setClasses())
+        this.listenTo(settings, 'change:singleton', () => this.setClasses())
+    }
+
+    setClasses () {
+        this.className = "";
         this.classList.add('conversejs');
         this.classList.add(`converse-${api.settings.get('view_mode')}`);
         this.classList.add(`theme-${api.settings.get('theme')}`);
-        this.setAttribute('id', 'conversejs');
+        this.requestUpdate();
     }
 }
-
-customElements.define('converse-root', ConverseRoot);

+ 16 - 0
src/plugins/rootview/styles/root.scss

@@ -0,0 +1,16 @@
+converse-root.converse-js {
+    &.converse-fullpage,
+    &.converse-overlayed,
+    &.converse-mobile {
+        bottom: 0;
+        height: 100%;
+        padding-left: env(safe-area-inset-left);
+        padding-right: env(safe-area-inset-right);
+        position: fixed;
+        z-index: 1031; // One more than bootstrap navbar
+    }
+
+    &.converse-embedded {
+        position: relative;
+    }
+}

+ 3 - 3
src/plugins/rootview/templates/root.js

@@ -3,10 +3,10 @@ import { api } from '@converse/headless/core';
 import { html } from 'lit';
 
 export default () => {
-    let extra_classes = api.settings.get('singleton') ? 'converse-singleton' : '';
-    extra_classes += `converse-${api.settings.get('view_mode')}`;
+    const extra_classes = api.settings.get('singleton') ? ['converse-singleton'] : [];
+    extra_classes.push(`converse-${api.settings.get('view_mode')}`);
     return html`
-        <converse-chats class="converse-chatboxes row no-gutters ${extra_classes}"></converse-chats>
+        <converse-chats class="converse-chatboxes row no-gutters ${extra_classes.join(' ')}"></converse-chats>
         <div id="converse-modals" class="modals"></div>
         <converse-fontawesome></converse-fontawesome>
     `;

+ 1 - 1
src/plugins/rootview/utils.js

@@ -6,7 +6,7 @@ export function ensureElement () {
         return;
     }
     const root = api.settings.get('root');
-    if (!root.querySelector('converse-root#conversejs')) {
+    if (!root.querySelector('converse-root')) {
         const el = document.createElement('converse-root');
         const body = root.querySelector('body');
         if (body) {

+ 0 - 6
src/shared/styles/_core.scss

@@ -1,14 +1,8 @@
 .conversejs {
-    bottom: 0;
-    height: 100%;
-    position: fixed;
-    padding-left: env(safe-area-inset-left);
-    padding-right: env(safe-area-inset-right);
     color: var(--text-color);
     font-family: var(--normal-font);
     font-size: var(--font-size);
     direction: ltr;
-    z-index: 1031; // One more than bootstrap navbar
 
     .flyout {
       position: absolute;