Переглянути джерело

Some work on componentizing the minimized chats UI

JC Brand 4 роки тому
батько
коміт
c41bdac6c8

+ 14 - 6
package-lock.json

@@ -3199,7 +3199,8 @@
 			"dependencies": {
 				"filesize": {
 					"version": "6.1.0",
-					"resolved": false
+					"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
+					"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg=="
 				},
 				"fs-extra": {
 					"version": "8.1.0",
@@ -3233,7 +3234,8 @@
 				},
 				"jed": {
 					"version": "1.1.1",
-					"resolved": false
+					"resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz",
+					"integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ="
 				},
 				"jsonfile": {
 					"version": "5.0.0",
@@ -3254,7 +3256,8 @@
 				},
 				"localforage": {
 					"version": "1.7.3",
-					"resolved": false,
+					"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
+					"integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
 					"requires": {
 						"lie": "3.1.1"
 					}
@@ -3266,13 +3269,14 @@
 				},
 				"pluggable.js": {
 					"version": "2.0.1",
-					"resolved": false,
+					"resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-2.0.1.tgz",
+					"integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==",
 					"requires": {
 						"lodash": "^4.17.11"
 					}
 				},
 				"skeletor.js": {
-					"version": "0.0.1",
+					"version": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561",
 					"from": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561",
 					"requires": {
 						"lodash": "^4.17.14"
@@ -3280,7 +3284,11 @@
 				},
 				"strophe.js": {
 					"version": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
-					"from": "strophe.js@github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f"
+					"from": "strophe.js@github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
+					"requires": {
+						"abab": "^2.0.3",
+						"xmldom": "^0.1.27"
+					}
 				},
 				"twemoji": {
 					"version": "12.1.5",

+ 0 - 190
spec/chatbox.js

@@ -193,82 +193,6 @@ describe("Chatboxes", function () {
             done();
         }));
 
-        it("can be trimmed to conserve space",
-            mock.initConverse(['rosterGroupsFetched'], {},
-            async function (done, _converse) {
-
-            spyOn(_converse.chatboxviews, 'trimChats');
-
-            const trimmed_chatboxes = _converse.minimized_chats;
-            spyOn(trimmed_chatboxes, 'addChat').and.callThrough();
-            spyOn(trimmed_chatboxes, 'removeChat').and.callThrough();
-
-            await mock.waitForRoster(_converse, 'current');
-            await mock.openControlBox(_converse);
-            expect(_converse.chatboxviews.trimChats.calls.count()).toBe(1);
-
-            let jid, chatboxview;
-            // openControlBox was called earlier, so the controlbox is
-            // visible, but no other chat boxes have been created.
-            expect(_converse.chatboxes.length).toEqual(1);
-            expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
-
-            _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attached.
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length);
-            // Test that they can be maximized again
-            const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
-            expect(online_contacts.length).toBe(17);
-            let i;
-            for (i=0; i<online_contacts.length; i++) {
-                const el = online_contacts[i];
-                el.click();
-            }
-            await u.waitUntil(() => _converse.chatboxes.length == 16);
-            expect(_converse.chatboxviews.trimChats.calls.count()).toBe(16);
-
-            _converse.api.chatviews.get().forEach(v => spyOn(v, 'onMinimized').and.callThrough());
-            for (i=0; i<online_contacts.length; i++) {
-                const el = online_contacts[i];
-                jid = _.trim(el.textContent.trim()).replace(/ /g,'.').toLowerCase() + '@montague.lit';
-                chatboxview = _converse.chatboxviews.get(jid);
-                chatboxview.model.set({'minimized': true});
-                expect(trimmed_chatboxes.addChat).toHaveBeenCalled();
-                expect(chatboxview.onMinimized).toHaveBeenCalled();
-            }
-            await u.waitUntil(() => _converse.chatboxviews.keys().length);
-            var key = _converse.chatboxviews.keys()[1];
-            const trimmedview = trimmed_chatboxes.get(key);
-            const chatbox = trimmedview.model;
-            spyOn(chatbox, 'maximize').and.callThrough();
-            spyOn(trimmedview, 'restore').and.callThrough();
-            trimmedview.delegateEvents();
-            trimmedview.el.querySelector("a.restore-chat").click();
-
-            expect(trimmedview.restore).toHaveBeenCalled();
-            expect(chatbox.maximize).toHaveBeenCalled();
-            expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17);
-            done();
-        }));
-
-        it("can be opened in minimized mode initially",
-            mock.initConverse(
-                ['rosterGroupsFetched'], {},
-                async function (done, _converse) {
-
-            await mock.waitForRoster(_converse, 'current');
-            const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await _converse.api.chats.create(sender_jid, {'minimized': true});
-            await u.waitUntil(() => _converse.chatboxes.length > 1);
-            const chatBoxView = _converse.chatboxviews.get(sender_jid);
-            expect(u.isVisible(chatBoxView.el)).toBeFalsy();
-
-            const minimized_chat = _converse.minimized_chats.get(sender_jid);
-            expect(minimized_chat).toBeTruthy();
-            expect(u.isVisible(minimized_chat.el)).toBeTruthy();
-            done();
-        }));
-
-
         it("is focused if its already open and you click on its corresponding roster item",
             mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
                 async function (done, _converse) {
@@ -364,42 +288,6 @@ describe("Chatboxes", function () {
             done();
         }));
 
-        it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
-
-            await mock.waitForRoster(_converse, 'current');
-            await mock.openControlBox(_converse);
-
-            const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
-            await mock.openChatBoxFor(_converse, contact_jid);
-            const trimmed_chatboxes = _converse.minimized_chats;
-            const chatview = _converse.chatboxviews.get(contact_jid);
-            spyOn(chatview, 'minimize').and.callThrough();
-            spyOn(_converse.api, "trigger").and.callThrough();
-            // We need to rebind all events otherwise our spy won't be called
-            chatview.delegateEvents();
-            chatview.el.querySelector('.toggle-chatbox-button').click();
-
-            expect(chatview.minimize).toHaveBeenCalled();
-            expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
-            expect(_converse.api.trigger.calls.count(), 2);
-            expect(u.isVisible(chatview.el)).toBeFalsy();
-            expect(chatview.model.get('minimized')).toBeTruthy();
-            chatview.el.querySelector('.toggle-chatbox-button').click();
-            const trimmedview = trimmed_chatboxes.get(chatview.model.get('id'));
-            spyOn(trimmedview, 'restore').and.callThrough();
-            trimmedview.delegateEvents();
-            trimmedview.el.querySelector("a.restore-chat").click();
-
-            expect(trimmedview.restore).toHaveBeenCalled();
-            expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
-            expect(chatview.model.get('minimized')).toBeFalsy();
-            done();
-        }));
-
         it("will be removed from browserStorage when closed",
             mock.initConverse(
                 ['rosterGroupsFetched', 'chatBoxesFetched'], {},
@@ -1591,82 +1479,4 @@ describe("Chatboxes", function () {
             done();
         }));
     });
-
-    describe("A Minimized ChatBoxView's Unread Message Count", function () {
-
-        it("is displayed when scrolled up chatbox is minimized after receiving unread messages",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
-
-            await mock.waitForRoster(_converse, 'current', 1);
-            const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await mock.openChatBoxFor(_converse, sender_jid);
-            const msgFactory = function () {
-                return mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
-            };
-            const selectUnreadMsgCount = function () {
-                const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid);
-                return minimizedChatBoxView.el.querySelector('.message-count');
-            };
-            const chatbox = _converse.chatboxes.get(sender_jid);
-            chatbox.save('scrolled', true);
-            _converse.handleMessageStanza(msgFactory());
-            await u.waitUntil(() => chatbox.messages.length);
-            const chatboxview = _converse.chatboxviews.get(sender_jid);
-            chatboxview.minimize();
-
-            const unread_count = selectUnreadMsgCount();
-            expect(u.isVisible(unread_count)).toBeTruthy();
-            expect(unread_count.innerHTML.replace(/<!---->/g, '')).toBe('1');
-            done();
-        }));
-
-        it("is incremented when message is received and windows is not focused",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
-
-            await mock.waitForRoster(_converse, 'current', 1);
-            const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            const view = await mock.openChatBoxFor(_converse, sender_jid)
-            const msgFactory = function () {
-                return mock.createChatMessage(_converse, sender_jid,
-                    'This message will be received as unread, but eventually will be read');
-            };
-            const selectUnreadMsgCount = function () {
-                const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid);
-                return minimizedChatBoxView.el.querySelector('.message-count');
-            };
-            view.minimize();
-            _converse.handleMessageStanza(msgFactory());
-            await u.waitUntil(() => view.model.messages.length);
-            const unread_count = selectUnreadMsgCount();
-            expect(u.isVisible(unread_count)).toBeTruthy();
-            expect(unread_count.innerHTML.replace(/<!---->/g, '')).toBe('1');
-            done();
-        }));
-
-        it("will render Openstreetmap-URL from geo-URI",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
-
-            await mock.waitForRoster(_converse, 'current', 1);
-
-            const message = "geo:37.786971,-122.399677";
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await mock.openChatBoxFor(_converse, contact_jid);
-            const view = _converse.chatboxviews.get(contact_jid);
-            spyOn(view.model, 'sendMessage').and.callThrough();
-            mock.sendMessage(view, message);
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length, 1000);
-            expect(view.model.sendMessage).toHaveBeenCalled();
-            const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
-            await u.waitUntil(() => msg.innerHTML.replace(/\<!----\>/g, '') ===
-                '<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&amp;'+
-                'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.786971&amp;mlon=-122.399677#map=18/37.786971/-122.399677</a>');
-            done();
-        }));
-    });
 });

+ 0 - 62
spec/messages.js

@@ -700,68 +700,6 @@ describe("A Chat Message", function () {
         done();
     }));
 
-    it("received for a minimized chat box will increment a counter on its header",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
-
-        if (_converse.view_mode === 'fullscreen') {
-            return done();
-        }
-        await mock.waitForRoster(_converse, 'current');
-        const contact_name = mock.cur_names[0];
-        const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
-        await mock.openControlBox(_converse);
-        spyOn(_converse.api, "trigger").and.callThrough();
-
-        await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
-        await mock.openChatBoxFor(_converse, contact_jid);
-        const chatview = _converse.api.chatviews.get(contact_jid);
-        expect(u.isVisible(chatview.el)).toBeTruthy();
-        expect(chatview.model.get('minimized')).toBeFalsy();
-        chatview.el.querySelector('.toggle-chatbox-button').click();
-        expect(chatview.model.get('minimized')).toBeTruthy();
-        var message = 'This message is sent to a minimized chatbox';
-        var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-        var msg = $msg({
-            from: sender_jid,
-            to: _converse.connection.jid,
-            type: 'chat',
-            id: u.getUniqueId()
-        }).c('body').t(message).up()
-        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-        await _converse.handleMessageStanza(msg);
-
-        await u.waitUntil(() => chatview.model.messages.length);
-        expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
-        const trimmed_chatboxes = _converse.minimized_chats;
-        const trimmedview = trimmed_chatboxes.get(contact_jid);
-        let count = trimmedview.el.querySelector('.message-count');
-        expect(u.isVisible(chatview.el)).toBeFalsy();
-        expect(trimmedview.model.get('minimized')).toBeTruthy();
-        expect(u.isVisible(count)).toBeTruthy();
-        expect(count.textContent).toBe('1');
-        _converse.handleMessageStanza(
-            $msg({
-                from: mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
-                to: _converse.connection.jid,
-                type: 'chat',
-                id: u.getUniqueId()
-            }).c('body').t('This message is also sent to a minimized chatbox').up()
-            .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
-        );
-
-        await u.waitUntil(() => (chatview.model.messages.length > 1));
-        expect(u.isVisible(chatview.el)).toBeFalsy();
-        expect(trimmedview.model.get('minimized')).toBeTruthy();
-        count = trimmedview.el.querySelector('.message-count');
-        expect(u.isVisible(count)).toBeTruthy();
-        expect(count.textContent).toBe('2');
-        trimmedview.el.querySelector('.restore-chat').click();
-        expect(trimmed_chatboxes.keys().length).toBe(0);
-        done();
-    }));
-
     it("will indicate when it has a time difference of more than a day between it and its predecessor",
         mock.initConverse(
             ['rosterGroupsFetched', 'chatBoxesFetched'], {},

+ 300 - 26
spec/minchats.js

@@ -1,8 +1,280 @@
-/*global mock */
+/*global mock, converse */
 
-const _ = converse.env._;
 const  $msg = converse.env.$msg;
 const u = converse.env.utils;
+const sizzle = converse.env.sizzle;
+
+
+describe("A chat message", function () {
+
+    it("received for a minimized chat box will increment a counter on its header",
+        mock.initConverse(
+            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            async function (done, _converse) {
+
+        if (_converse.view_mode === 'fullscreen') {
+            return done();
+        }
+        await mock.waitForRoster(_converse, 'current');
+        const contact_name = mock.cur_names[0];
+        const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        await mock.openControlBox(_converse);
+        spyOn(_converse.api, "trigger").and.callThrough();
+
+        await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+        await mock.openChatBoxFor(_converse, contact_jid);
+        const chatview = _converse.api.chatviews.get(contact_jid);
+        expect(u.isVisible(chatview.el)).toBeTruthy();
+        expect(chatview.model.get('minimized')).toBeFalsy();
+        chatview.el.querySelector('.toggle-chatbox-button').click();
+        expect(chatview.model.get('minimized')).toBeTruthy();
+        var message = 'This message is sent to a minimized chatbox';
+        var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        var msg = $msg({
+            from: sender_jid,
+            to: _converse.connection.jid,
+            type: 'chat',
+            id: u.getUniqueId()
+        }).c('body').t(message).up()
+        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
+        await _converse.handleMessageStanza(msg);
+
+        await u.waitUntil(() => chatview.model.messages.length);
+
+        expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
+        const trimmed_chatboxes = _converse.minimized_chats;
+        let count = trimmed_chatboxes.el.querySelector('converse-minimized-chat .message-count');
+        expect(u.isVisible(chatview.el)).toBeFalsy();
+        expect(chatview.model.get('minimized')).toBeTruthy();
+
+        expect(u.isVisible(count)).toBeTruthy();
+        expect(count.textContent).toBe('1');
+        _converse.handleMessageStanza(
+            $msg({
+                from: mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
+                to: _converse.connection.jid,
+                type: 'chat',
+                id: u.getUniqueId()
+            }).c('body').t('This message is also sent to a minimized chatbox').up()
+            .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
+        );
+
+        await u.waitUntil(() => (chatview.model.messages.length > 1));
+        expect(u.isVisible(chatview.el)).toBeFalsy();
+        expect(chatview.model.get('minimized')).toBeTruthy();
+        count = trimmed_chatboxes.el.querySelector('converse-minimized-chat .message-count');
+        expect(u.isVisible(count)).toBeTruthy();
+        expect(count.textContent).toBe('2');
+        _converse.minimized_chats.el.querySelector("a.restore-chat").click();
+        expect(_converse.chatboxes.filter('minimized').length).toBe(0);
+        done();
+    }));
+
+});
+
+describe("A Groupcaht", function () {
+
+    it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
+        mock.initConverse(
+            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            async function (done, _converse) {
+
+        await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
+        const view = _converse.chatboxviews.get('lounge@montague.lit');
+        spyOn(view, 'onMinimized').and.callThrough();
+        spyOn(view, 'onMaximized').and.callThrough();
+        spyOn(_converse.api, "trigger").and.callThrough();
+        view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
+        const button = await u.waitUntil(() => view.el.querySelector('.toggle-chatbox-button'));
+        button.click();
+
+        expect(view.onMinimized).toHaveBeenCalled();
+        expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
+        expect(u.isVisible(view.el)).toBeFalsy();
+        expect(view.model.get('minimized')).toBeTruthy();
+        expect(view.onMinimized).toHaveBeenCalled();
+        const el = await u.waitUntil(() => _converse.minimized_chats.el.querySelector("a.restore-chat"));
+        el.click();
+        expect(view.onMaximized).toHaveBeenCalled();
+        expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
+        expect(view.model.get('minimized')).toBeFalsy();
+        expect(_converse.api.trigger.calls.count(), 3);
+        done();
+    }));
+});
+
+
+describe("A Chatbox", function () {
+
+    it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
+        mock.initConverse(
+            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            async function (done, _converse) {
+
+        await mock.waitForRoster(_converse, 'current');
+        await mock.openControlBox(_converse);
+
+        const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+        await mock.openChatBoxFor(_converse, contact_jid);
+        const chatview = _converse.chatboxviews.get(contact_jid);
+        spyOn(chatview, 'minimize').and.callThrough();
+        spyOn(_converse.api, "trigger").and.callThrough();
+        // We need to rebind all events otherwise our spy won't be called
+        chatview.delegateEvents();
+        chatview.el.querySelector('.toggle-chatbox-button').click();
+
+        expect(chatview.minimize).toHaveBeenCalled();
+        expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
+        expect(_converse.api.trigger.calls.count(), 2);
+        expect(u.isVisible(chatview.el)).toBeFalsy();
+        expect(chatview.model.get('minimized')).toBeTruthy();
+        chatview.el.querySelector('.toggle-chatbox-button').click();
+
+        await u.waitUntil(() => _converse.chatboxviews.keys().length);
+        _converse.minimized_chats.el.querySelector("a.restore-chat").click();
+        expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
+        expect(chatview.model.get('minimized')).toBeFalsy();
+        done();
+    }));
+
+
+    it("can be opened in minimized mode initially",
+        mock.initConverse(
+            ['rosterGroupsFetched'], {},
+            async function (done, _converse) {
+
+        await mock.waitForRoster(_converse, 'current');
+        const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        expect(u.isVisible(_converse.minimized_chats.el.firstElementChild)).toBe(false);
+        await _converse.api.chats.create(sender_jid, {'minimized': true});
+        await u.waitUntil(() => _converse.chatboxes.length > 1);
+        const chatBoxView = _converse.chatboxviews.get(sender_jid);
+        expect(u.isVisible(chatBoxView.el)).toBeFalsy();
+        expect(u.isVisible(_converse.minimized_chats.el.firstElementChild)).toBe(true);
+        expect(_converse.minimized_chats.el.firstElementChild.querySelectorAll('converse-minimized-chat').length).toBe(1);
+        expect(_converse.chatboxes.filter('minimized').length).toBe(1);
+        done();
+    }));
+
+
+    it("can be trimmed to conserve space",
+        mock.initConverse(['rosterGroupsFetched'], {},
+        async function (done, _converse) {
+
+        spyOn(_converse.chatboxviews, 'trimChats');
+
+        await mock.waitForRoster(_converse, 'current');
+        await mock.openControlBox(_converse);
+        expect(_converse.chatboxviews.trimChats.calls.count()).toBe(1);
+
+        let jid, chatboxview;
+        // openControlBox was called earlier, so the controlbox is
+        // visible, but no other chat boxes have been created.
+        expect(_converse.chatboxes.length).toEqual(1);
+        expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open
+
+        _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attached.
+        await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length);
+        // Test that they can be maximized again
+        const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat');
+        expect(online_contacts.length).toBe(17);
+        let i;
+        for (i=0; i<online_contacts.length; i++) {
+            const el = online_contacts[i];
+            el.click();
+        }
+        await u.waitUntil(() => _converse.chatboxes.length == 16);
+        expect(_converse.chatboxviews.trimChats.calls.count()).toBe(16);
+
+        _converse.api.chatviews.get().forEach(v => spyOn(v, 'onMinimized').and.callThrough());
+        for (i=0; i<online_contacts.length; i++) {
+            const el = online_contacts[i];
+            jid = el.textContent.trim().replace(/ /g,'.').toLowerCase() + '@montague.lit';
+            chatboxview = _converse.chatboxviews.get(jid);
+            chatboxview.model.set({'minimized': true});
+            expect(chatboxview.onMinimized).toHaveBeenCalled();
+        }
+        await u.waitUntil(() => _converse.chatboxviews.keys().length);
+        var key = _converse.chatboxviews.keys()[1];
+        const chatbox = _converse.chatboxes.get(key);
+        spyOn(chatbox, 'maximize').and.callThrough();
+        _converse.minimized_chats.el.querySelector("a.restore-chat").click();
+
+        expect(chatbox.maximize).toHaveBeenCalled();
+        expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17);
+        done();
+    }));
+});
+
+
+describe("A Minimized ChatBoxView's Unread Message Count", function () {
+
+    it("is displayed when scrolled up chatbox is minimized after receiving unread messages",
+        mock.initConverse(
+            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            async function (done, _converse) {
+
+        await mock.waitForRoster(_converse, 'current', 1);
+        const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        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');
+        const selectUnreadMsgCount = () => _converse.minimized_chats.el.querySelector('#toggle-minimized-chats .unread-message-count');
+        const chatbox = _converse.chatboxes.get(sender_jid);
+        chatbox.save('scrolled', true);
+        _converse.handleMessageStanza(msgFactory());
+        await u.waitUntil(() => chatbox.messages.length);
+        const chatboxview = _converse.chatboxviews.get(sender_jid);
+        chatboxview.minimize();
+
+        const unread_count = selectUnreadMsgCount();
+        expect(u.isVisible(unread_count)).toBeTruthy();
+        expect(unread_count.innerHTML.replace(/<!---->/g, '')).toBe('1');
+        done();
+    }));
+
+    it("is incremented when message is received and windows is not focused",
+        mock.initConverse(
+            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            async function (done, _converse) {
+
+        await mock.waitForRoster(_converse, 'current', 1);
+        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');
+        const selectUnreadMsgCount = () => _converse.minimized_chats.el.querySelector('#toggle-minimized-chats .unread-message-count');
+        view.minimize();
+        _converse.handleMessageStanza(msgFactory());
+        await u.waitUntil(() => view.model.messages.length);
+        const unread_count = selectUnreadMsgCount();
+        expect(u.isVisible(unread_count)).toBeTruthy();
+        expect(unread_count.innerHTML.replace(/<!---->/g, '')).toBe('1');
+        done();
+    }));
+
+    it("will render Openstreetmap-URL from geo-URI",
+        mock.initConverse(
+            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            async function (done, _converse) {
+
+        await mock.waitForRoster(_converse, 'current', 1);
+
+        const message = "geo:37.786971,-122.399677";
+        const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        await mock.openChatBoxFor(_converse, contact_jid);
+        const view = _converse.chatboxviews.get(contact_jid);
+        spyOn(view.model, 'sendMessage').and.callThrough();
+        mock.sendMessage(view, message);
+        await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length, 1000);
+        expect(view.model.sendMessage).toHaveBeenCalled();
+        const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
+        await u.waitUntil(() => msg.innerHTML.replace(/\<!----\>/g, '') ===
+            '<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&amp;'+
+            'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.786971&amp;mlon=-122.399677#map=18/37.786971/-122.399677</a>');
+        done();
+    }));
+});
+
 
 describe("The Minimized Chats Widget", function () {
 
@@ -19,12 +291,12 @@ describe("The Minimized Chats Widget", function () {
         await mock.openChatBoxFor(_converse, contact_jid)
         let chatview = _converse.chatboxviews.get(contact_jid);
         expect(chatview.model.get('minimized')).toBeFalsy();
-        expect(u.isVisible(_converse.minimized_chats.el)).toBe(false);
+        expect(u.isVisible(_converse.minimized_chats.el.firstElementChild)).toBe(false);
         chatview.el.querySelector('.toggle-chatbox-button').click();
         expect(chatview.model.get('minimized')).toBeTruthy();
         expect(u.isVisible(_converse.minimized_chats.el)).toBe(true);
-        expect(_converse.minimized_chats.keys().length).toBe(1);
-        expect(_converse.minimized_chats.keys()[0]).toBe(contact_jid);
+        expect(_converse.chatboxes.filter('minimized').length).toBe(1);
+        expect(_converse.chatboxes.models.filter(c => c.get('minimized')).pop().get('jid')).toBe(contact_jid);
 
         contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         await mock.openChatBoxFor(_converse, contact_jid);
@@ -33,8 +305,8 @@ describe("The Minimized Chats Widget", function () {
         chatview.el.querySelector('.toggle-chatbox-button').click();
         expect(chatview.model.get('minimized')).toBeTruthy();
         expect(u.isVisible(_converse.minimized_chats.el)).toBe(true);
-        expect(_converse.minimized_chats.keys().length).toBe(2);
-        expect(_.includes(_converse.minimized_chats.keys(), contact_jid)).toBeTruthy();
+        expect(_converse.chatboxes.filter('minimized').length).toBe(2);
+        expect(_converse.chatboxes.filter('minimized').map(c => c.get('jid')).includes(contact_jid)).toBeTruthy();
         done();
     }));
 
@@ -50,16 +322,18 @@ describe("The Minimized Chats Widget", function () {
         const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
         await mock.openChatBoxFor(_converse, contact_jid);
         const chatview = _converse.chatboxviews.get(contact_jid);
-        expect(u.isVisible(_converse.minimized_chats.el)).toBeFalsy();
+        expect(u.isVisible(_converse.minimized_chats.el.firstElementChild)).toBe(false);
+
         chatview.model.set({'minimized': true});
         expect(u.isVisible(_converse.minimized_chats.el)).toBeTruthy();
-        expect(_converse.minimized_chats.keys().length).toBe(1);
-        expect(_converse.minimized_chats.keys()[0]).toBe(contact_jid);
+        expect(_converse.chatboxes.filter('minimized').length).toBe(1);
+        expect(_converse.chatboxes.models.filter(c => c.get('minimized')).pop().get('jid')).toBe(contact_jid);
+
         expect(u.isVisible(_converse.minimized_chats.el.querySelector('.minimized-chats-flyout'))).toBeTruthy();
-        expect(_converse.minimized_chats.toggleview.model.get('collapsed')).toBeFalsy();
+        expect(_converse.minimized_chats.minchats.get('collapsed')).toBeFalsy();
         _converse.minimized_chats.el.querySelector('#toggle-minimized-chats').click();
         await u.waitUntil(() => u.isVisible(_converse.minimized_chats.el.querySelector('.minimized-chats-flyout')));
-        expect(_converse.minimized_chats.toggleview.model.get('collapsed')).toBeTruthy();
+        expect(_converse.minimized_chats.minchats.get('collapsed')).toBeTruthy();
         done();
     }));
 
@@ -72,22 +346,22 @@ describe("The Minimized Chats Widget", function () {
         await mock.openControlBox(_converse);
         _converse.minimized_chats.initToggle();
 
-        var i, contact_jid, chatview, msg;
-        _converse.minimized_chats.toggleview.model.set({'collapsed': true});
+        _converse.minimized_chats.minchats.set({'collapsed': true});
 
-        const unread_el = _converse.minimized_chats.toggleview.el.querySelector('.unread-message-count');
-        expect(unread_el === null).toBe(true);
+        const unread_el = _converse.minimized_chats.el.querySelector('.unread-message-count');
+        expect(u.isVisible(unread_el)).toBe(false);
 
+        let i, contact_jid;
         for (i=0; i<3; i++) {
             contact_jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             mock.openChatBoxFor(_converse, contact_jid);
         }
         await u.waitUntil(() => _converse.chatboxes.length == 4);
 
-        chatview = _converse.chatboxviews.get(contact_jid);
+        const chatview = _converse.chatboxviews.get(contact_jid);
         chatview.model.set({'minimized': true});
         for (i=0; i<3; i++) {
-            msg = $msg({
+            const msg = $msg({
                 from: contact_jid,
                 to: _converse.connection.jid,
                 type: 'chat',
@@ -98,8 +372,8 @@ describe("The Minimized Chats Widget", function () {
         }
         await u.waitUntil(() => chatview.model.messages.length === 3, 500);
 
-        expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy();
-        expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((3).toString());
+        expect(u.isVisible(_converse.minimized_chats.el.querySelector('.unread-message-count'))).toBeTruthy();
+        expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe((3).toString());
         // Chat state notifications don't increment the unread messages counter
         // <composing> state
         _converse.handleMessageStanza($msg({
@@ -108,7 +382,7 @@ describe("The Minimized Chats Widget", function () {
             type: 'chat',
             id: u.getUniqueId()
         }).c('composing', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
-        expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
+        expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
 
         // <paused> state
         _converse.handleMessageStanza($msg({
@@ -117,7 +391,7 @@ describe("The Minimized Chats Widget", function () {
             type: 'chat',
             id: u.getUniqueId()
         }).c('paused', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
-        expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
+        expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
 
         // <gone> state
         _converse.handleMessageStanza($msg({
@@ -126,7 +400,7 @@ describe("The Minimized Chats Widget", function () {
             type: 'chat',
             id: u.getUniqueId()
         }).c('gone', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
-        expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
+        expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
 
         // <inactive> state
         _converse.handleMessageStanza($msg({
@@ -135,7 +409,7 @@ describe("The Minimized Chats Widget", function () {
             type: 'chat',
             id: u.getUniqueId()
         }).c('inactive', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
-        expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
+        expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
         done();
     }));
 
@@ -158,8 +432,8 @@ describe("The Minimized Chats Widget", function () {
             }).c('body').t(message).tree();
         view.model.handleMessageStanza(msg);
         await u.waitUntil(() => view.model.messages.length);
-        expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy();
-        expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe('1');
+        expect(u.isVisible(_converse.minimized_chats.el.querySelector('.unread-message-count'))).toBeTruthy();
+        expect(_converse.minimized_chats.el.querySelector('.unread-message-count').textContent).toBe('1');
         done();
     }));
 });

+ 0 - 32
spec/muc.js

@@ -2862,38 +2862,6 @@ describe("Groupchats", function () {
             done();
         }));
 
-        it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
-
-            await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
-            const view = _converse.chatboxviews.get('lounge@montague.lit'),
-                  trimmed_chatboxes = _converse.minimized_chats;
-
-            spyOn(view, 'onMinimized').and.callThrough();
-            spyOn(view, 'onMaximized').and.callThrough();
-            spyOn(_converse.api, "trigger").and.callThrough();
-            view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
-            const button = await u.waitUntil(() => view.el.querySelector('.toggle-chatbox-button'));
-            button.click();
-
-            expect(view.onMinimized).toHaveBeenCalled();
-            expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object));
-            expect(u.isVisible(view.el)).toBeFalsy();
-            expect(view.model.get('minimized')).toBeTruthy();
-            expect(view.onMinimized).toHaveBeenCalled();
-            await u.waitUntil(() => trimmed_chatboxes.get(view.model.get('id')));
-            const trimmedview = trimmed_chatboxes.get(view.model.get('id'));
-            trimmedview.el.querySelector("a.restore-chat").click();
-            expect(view.onMaximized).toHaveBeenCalled();
-            expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
-            expect(view.model.get('minimized')).toBeFalsy();
-            expect(_converse.api.trigger.calls.count(), 3);
-            done();
-
-        }));
-
         it("can be closed again by clicking a DOM element with class 'close-chatbox-button'",
             mock.initConverse(
                 ['rosterGroupsFetched', 'chatBoxesFetched'], {},

+ 47 - 0
src/components/minimized_chat.js

@@ -0,0 +1,47 @@
+import { CustomElement } from './element.js';
+import tpl_trimmed_chat from "templates/trimmed_chat.js";
+import { api, _converse } from "@converse/headless/converse-core";
+
+
+export default class MinimizedChat extends CustomElement {
+
+    static get properties () {
+        return {
+            model: { type: Object },
+            title: { type: String },
+            type: { type: String },
+            num_unread: { type: Number }
+        }
+    }
+
+    render () {
+        const data = {
+            'close': ev => this.close(ev),
+            'num_unread': this.num_unread,
+            'restore': ev => this.restore(ev),
+            'title': this.title,
+            'type': this.type
+        };
+        return tpl_trimmed_chat(data);
+    }
+
+    close (ev) {
+        ev?.preventDefault();
+        const view = _converse.chatboxviews.get(this.model.get('id'));
+        if (view) {
+            // This will call model.destroy(), removing it from the
+            // collection and will also emit 'chatBoxClosed'
+            view.close();
+        } else {
+            this.model.destroy();
+            api.trigger('chatBoxClosed', this);
+        }
+    }
+
+    restore (ev) {
+        ev?.preventDefault();
+        this.model.maximize();
+    }
+}
+
+api.elements.define('converse-minimized-chat', MinimizedChat);

+ 34 - 161
src/converse-minimize.js

@@ -3,16 +3,14 @@
  * @copyright 2020, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
-import "converse-chatview";
-import tpl_chats_panel from "templates/chats_panel.html";
-import tpl_toggle_chats from "templates/toggle_chats.js";
-import tpl_trimmed_chat from "templates/trimmed_chat.js";
+import './components/minimized_chat.js';
+import 'converse-chatview';
+import tpl_chats_panel from 'templates/chats_panel.js';
 import { Model } from '@converse/skeletor/src/model.js';
-import { Overview } from "@converse/skeletor/src/overview";
-import { View } from "@converse/skeletor/src/view";
+import { View } from '@converse/skeletor/src/view';
 import { __ } from '@converse/headless/i18n';
-import { _converse, api, converse } from "@converse/headless/converse-core";
-import { debounce, sum } from 'lodash-es';
+import { _converse, api, converse } from '@converse/headless/converse-core';
+import { debounce } from 'lodash-es';
 import { render } from 'lit-html';
 
 const { dayjs } = converse.env;
@@ -324,183 +322,58 @@ converse.plugins.add('converse-minimize', {
 
         api.promises.add('minimizedChatsInitialized');
 
-        _converse.MinimizedChatBoxView = View.extend({
-            tagName: 'div',
-            events: {
-                'click .close-chatbox-button': 'close',
-                'click .restore-chat': 'restore'
-            },
-
-            initialize () {
-                this.listenTo(this.model, 'change:num_unread', this.render)
-                this.listenTo(this.model, 'change:name', this.render)
-                this.listenTo(this.model, 'change:fullname', this.render)
-                this.listenTo(this.model, 'change:jid', this.render)
-                this.listenTo(this.model, 'destroy', this.remove)
-                /**
-                 * Triggered once a {@link _converse.MinimizedChatBoxView } has been initialized
-                 * @event _converse#minimizedChatViewInitialized
-                 * @type { _converse.MinimizedChatBoxView }
-                 * @example _converse.api.listen.on('minimizedChatViewInitialized', view => { ... });
-                 */
-                api.trigger('minimizedChatViewInitialized', this);
-            },
-
-            render () {
-                const data = Object.assign(this.model.toJSON(), {'title': this.model.getDisplayName()});
-                render(tpl_trimmed_chat(data), this.el);
-                return this.el;
-            },
-
-            close (ev) {
-                if (ev && ev.preventDefault) { ev.preventDefault(); }
-                this.remove();
-                const view = _converse.chatboxviews.get(this.model.get('id'));
-                if (view) {
-                    // This will call model.destroy(), removing it from the
-                    // collection and will also emit 'chatBoxClosed'
-                    view.close();
-                } else {
-                    this.model.destroy();
-                    api.trigger('chatBoxClosed', this);
-                }
-                return this;
-            },
 
-            restore: debounce(function (ev) {
-                if (ev && ev.preventDefault) { ev.preventDefault(); }
-                this.model.off('change:num_unread', null, this);
-                this.remove();
-                this.model.maximize();
-            }, 200, {'leading': true})
+        _converse.MinimizedChatsToggle = Model.extend({
+            defaults: {
+                'collapsed': false
+            }
         });
 
 
-        _converse.MinimizedChats = Overview.extend({
-            tagName: 'div',
-            id: "minimized-chats",
-            className: 'hidden',
-            events: {
-                "click #toggle-minimized-chats": "toggle"
-            },
+        _converse.MinimizedChats = View.extend({
+            tagName: 'span',
 
             async initialize () {
-                this.render();
                 await this.initToggle();
-                const chats = this.model.where({'minimized': true});
-                chats.length && this.addMultipleChats(chats);
-                this.listenTo(this.model, "add", this.onChanged)
-                this.listenTo(this.model, "destroy", this.removeChat)
-                this.listenTo(this.model, "change:minimized", this.onChanged)
-                this.listenTo(this.model, 'change:num_unread', this.updateUnreadMessagesCounter)
+                this.render();
+                this.listenTo(this.minchats, 'change:collapsed', this.render)
+                this.listenTo(this.model, 'add', this.render)
+                this.listenTo(this.model, 'change:fullname', this.render)
+                this.listenTo(this.model, 'change:jid', this.render)
+                this.listenTo(this.model, 'change:minimized', this.render)
+                this.listenTo(this.model, 'change:name', this.render)
+                this.listenTo(this.model, 'change:num_unread', this.render)
+                this.listenTo(this.model, 'remove', this.render)
             },
 
             render () {
+                const chats = this.model.where({'minimized': true});
+                const num_unread = chats.reduce((acc, chat) => (acc + chat.get('num_unread')), 0);
+                const num_minimized = chats.reduce((acc, chat) => (acc + (chat.get('minimized') ? 1 : 0)), 0);
+                const collapsed = this.minchats.get('collapsed');
+                const data = { chats, num_unread, num_minimized, collapsed };
+                data.toggle = ev => this.toggle(ev);
+                render(tpl_chats_panel(data), this.el);
+
                 if (!this.el.parentElement) {
-                    this.el.innerHTML = tpl_chats_panel();
                     _converse.chatboxviews.insertRowColumn(this.el);
                 }
-                if (this.keys().length === 0) {
-                    this.el.classList.add('hidden');
-                } else if (this.keys().length > 0 && !u.isVisible(this.el)) {
-                    this.el.classList.remove('hidden');
-                }
-                return this.el;
             },
 
             async initToggle () {
                 const id = `converse.minchatstoggle-${_converse.bare_jid}`;
-                const model = new _converse.MinimizedChatsToggle({id});
-                model.browserStorage = _converse.createStore(id);
-                await new Promise(resolve => model.fetch({'success': resolve, 'error': resolve}));
-                this.toggleview = new _converse.MinimizedChatsToggleView({model});
+                this.minchats = new _converse.MinimizedChatsToggle({id});
+                this.minchats.browserStorage = _converse.createStore(id);
+                await new Promise(resolve => this.minchats.fetch({'success': resolve, 'error': resolve}));
             },
 
             toggle (ev) {
-                if (ev && ev.preventDefault) { ev.preventDefault(); }
-                this.toggleview.model.save({'collapsed': !this.toggleview.model.get('collapsed')});
-                u.slideToggleElement(this.el.querySelector('.minimized-chats-flyout'), 200);
-            },
-
-            onChanged (item) {
-                if (item.get('id') === 'controlbox')  {
-                    // The ControlBox has it's own minimize toggle
-                    return;
-                }
-                if (item.get('minimized')) {
-                    this.addChat(item);
-                } else if (this.get(item.get('id'))) {
-                    this.removeChat(item);
-                }
-            },
-
-            addChatView (item) {
-                const existing = this.get(item.get('id'));
-                if (existing && existing.el.parentNode) {
-                    return;
-                }
-                const view = new _converse.MinimizedChatBoxView({model: item});
-                this.el.querySelector('.minimized-chats-flyout').insertAdjacentElement('beforeEnd', view.render());
-                this.add(item.get('id'), view);
-            },
-
-            addMultipleChats (items) {
-                items.forEach(item => this.addChatView(item));
-                this.toggleview.model.set({'num_minimized': this.keys().length});
-                this.render();
-            },
-
-            addChat (item) {
-                this.addChatView(item);
-                this.toggleview.model.set({'num_minimized': this.keys().length});
-                this.render();
-            },
-
-            removeChat (item) {
-                this.remove(item.get('id'));
-                this.toggleview.model.set({'num_minimized': this.keys().length});
-                this.render();
-            },
-
-            updateUnreadMessagesCounter () {
-                this.toggleview.model.save({'num_unread': sum(this.model.pluck('num_unread'))});
-                this.render();
+                ev?.preventDefault();
+                this.minchats.save({'collapsed': !this.minchats.get('collapsed')});
             }
         });
 
 
-        _converse.MinimizedChatsToggle = Model.extend({
-            defaults: {
-                'collapsed': false,
-                'num_minimized': 0,
-                'num_unread':  0
-            }
-        });
-
-
-        _converse.MinimizedChatsToggleView = View.extend({
-            _setElement (){
-                this.el = _converse.root.querySelector('#toggle-minimized-chats');
-            },
-
-            initialize () {
-                this.listenTo(this.model, 'change:num_minimized', this.render)
-                this.listenTo(this.model, 'change:num_unread', this.render)
-                this.flyout = this.el.parentElement.querySelector('.minimized-chats-flyout');
-            },
-
-            render () {
-                render(tpl_toggle_chats(Object.assign(this.model.toJSON())), this.el);
-
-                if (this.model.get('collapsed')) {
-                    u.hideElement(this.flyout);
-                } else {
-                    u.showElement(this.flyout);
-                }
-                return this.el;
-            }
-        });
-
         function initMinimizedChats () {
             _converse.minimized_chats?.remove();
             _converse.minimized_chats = new _converse.MinimizedChats({model: _converse.chatboxes});

+ 0 - 2
src/templates/chats_panel.html

@@ -1,2 +0,0 @@
-<a id="toggle-minimized-chats" href="#" class="row no-gutters"></a>
-<div class="flyout minimized-chats-flyout row no-gutters"></div>

+ 18 - 0
src/templates/chats_panel.js

@@ -0,0 +1,18 @@
+import { html } from "lit-html";
+import { __ } from '@converse/headless/i18n';
+
+export default (o) =>
+    html`<div id="minimized-chats" class="${o.chats.length ? '' : 'hidden'}">
+        <a id="toggle-minimized-chats" class="row no-gutters" @click=${o.toggle}>
+            ${o.num_minimized} ${__('Minimized')}
+            <span class="unread-message-count ${!o.num_unread ? 'unread-message-count-hidden' : ''}" href="#">${o.num_unread}</span>
+        </a>
+        <div class="flyout minimized-chats-flyout row no-gutters ${o.collapsed ? 'hidden' : ''}">
+            ${o.chats.map(chat =>
+                html`<converse-minimized-chat
+                        .model=${chat}
+                        title=${chat.getDisplayName()}
+                        type=${chat.get('type')}
+                        num_unread=${chat.get('num_unread')}></converse-minimized-chat>`)}
+        </div>
+    </div>`;

+ 0 - 8
src/templates/toggle_chats.js

@@ -1,8 +0,0 @@
-import { html } from "lit-html";
-import { __ } from '@converse/headless/i18n';
-
-
-export default (o) => html`
-    ${o.num_minimized} ${__('Minimized')}
-    <span class="unread-message-count ${!o.num_unread ? 'unread-message-count-hidden' : ''}" href="#">${o.num_unread}</span>
-`;

+ 2 - 2
src/templates/trimmed_chat.js

@@ -6,10 +6,10 @@ export default (o) => {
     const i18n_tooltip = __('Click to restore this chat');
     return html`
         <div class="chat-head-${o.type} chat-head row no-gutters">
-            <a class="restore-chat w-100 align-self-center" title="${i18n_tooltip}">
+            <a class="restore-chat w-100 align-self-center" title="${i18n_tooltip}" @click=${o.restore}>
                 ${o.num_unread ? html`<span class="message-count badge badge-light">${o.num_unread}</span>` : '' }
                 ${o.title}
             </a>
-            <a class="chatbox-btn close-chatbox-button fa fa-times"></a>
+            <a class="chatbox-btn close-chatbox-button fa fa-times" @click=${o.close}></a>
         </div>`;
 }

+ 1 - 0
webpack.html

@@ -20,6 +20,7 @@
         }
     });
     converse.initialize({
+        // root: new DocumentFragment(),
         show_send_button: true,
         auto_away: 300,
         auto_register_muc_nickname: true,