Browse Source

Create `converse-muc-chatarea` component

JC Brand 4 years ago
parent
commit
4ca30c4b93

+ 7 - 0
sass/_chatrooms.scss

@@ -140,6 +140,13 @@
                 width: 100%;
                 overflow: hidden;
 
+                converse-muc-chatarea {
+                    width: 100%;
+                    display: flex;
+                    flex-direction: row;
+                    flex-flow: nowrap;
+                }
+
                 .row {
                     flex-direction: row;
                 }

+ 2 - 2
spec/chatbox.js

@@ -37,7 +37,7 @@ describe("Chatboxes", function () {
                     id: u.getUniqueId()
                 }).c('body').t('hello world').tree();
             await _converse.handleMessageStanza(msg);
-            await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
             const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__body';
             await u.waitUntil(() => view.querySelector(msg_txt_sel).textContent.trim() === 'hello world');
             done();
@@ -524,7 +524,7 @@ describe("Chatboxes", function () {
                             id: u.getUniqueId()
                         }).c('body').t('hello world').tree();
                     await _converse.handleMessageStanza(msg);
-                    const msg_el = await u.waitUntil(() => view.content.querySelector('.chat-msg'));
+                    const msg_el = await u.waitUntil(() => view.querySelector('.chat-msg'));
                     await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === '');
                     expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('hello world');
                     done();

+ 8 - 8
spec/emojis.js

@@ -267,7 +267,7 @@ describe("Emojis", function () {
             await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve));
             const view = _converse.api.chatviews.get(sender_jid);
             await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
-            await u.waitUntil(() => u.hasClass('chat-msg__text--larger', view.content.querySelector('.chat-msg__text')));
+            await u.waitUntil(() => u.hasClass('chat-msg__text--larger', view.querySelector('.chat-msg__text')));
 
             _converse.handleMessageStanza($msg({
                     'from': sender_jid,
@@ -279,7 +279,7 @@ describe("Emojis", function () {
             await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
 
             let sel = '.message:last-child .chat-msg__text';
-            await u.waitUntil(() => u.hasClass('chat-msg__text--larger', view.content.querySelector(sel)));
+            await u.waitUntil(() => u.hasClass('chat-msg__text--larger', view.querySelector(sel)));
 
             // Test that a modified message that no longer contains only
             // emojis now renders normally again.
@@ -293,7 +293,7 @@ describe("Emojis", function () {
             });
             await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
             const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
-            await u.waitUntil(() => view.content.querySelector(last_msg_sel).textContent === '💩 😇');
+            await u.waitUntil(() => view.querySelector(last_msg_sel).textContent === '💩 😇');
 
             expect(textarea.value).toBe('');
             bottom_panel.onKeyDown({
@@ -314,7 +314,7 @@ describe("Emojis", function () {
             await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-msg__text'))
                 .filter(el => el.textContent === edited_text).length);
             expect(view.model.messages.models.length).toBe(3);
-            let message = view.content.querySelector(last_msg_sel);
+            let message = view.querySelector(last_msg_sel);
             expect(u.hasClass('chat-msg__text--larger', message)).toBe(false);
 
             textarea.value = ':smile: Hello world!';
@@ -333,7 +333,7 @@ describe("Emojis", function () {
             });
             await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 5);
 
-            message = view.content.querySelector('.message:last-child .chat-msg__text');
+            message = view.querySelector('.message:last-child .chat-msg__text');
             expect(u.hasClass('chat-msg__text--larger', message)).toBe(true);
             done()
         }));
@@ -355,11 +355,11 @@ describe("Emojis", function () {
             await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve));
             const view = _converse.api.chatviews.get(contact_jid);
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            await u.waitUntil(() => view.content.querySelector('.chat-msg__text').innerHTML.replace(/<!---->/g, '') ===
+            await u.waitUntil(() => view.querySelector('.chat-msg__text').innerHTML.replace(/<!---->/g, '') ===
                 '<img class="emoji" draggable="false" title=":innocent:" alt="😇" src="https://twemoji.maxcdn.com/v/12.1.6//72x72/1f607.png">');
 
             const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
-            let message = view.content.querySelector(last_msg_sel);
+            let message = view.querySelector(last_msg_sel);
             await u.waitUntil(() => u.isVisible(message.querySelector('.emoji')), 1000);
             let imgs = message.querySelectorAll('.emoji');
             expect(imgs.length).toBe(1);
@@ -374,7 +374,7 @@ describe("Emojis", function () {
                 keyCode: 13 // Enter
             });
             await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            message = view.content.querySelector(last_msg_sel);
+            message = view.querySelector(last_msg_sel);
             await u.waitUntil(() => u.isVisible(message.querySelector('.emoji')), 1000);
             imgs = message.querySelectorAll('.emoji');
             expect(imgs.length).toBe(2);

+ 9 - 9
spec/mam.js

@@ -267,8 +267,8 @@ describe("Message Archive Management", function () {
                 </iq>`);
             _converse.connection._dataRecv(mock.createRequest(result));
             await u.waitUntil(() => view.model.messages.length === 5);
-            await u.waitUntil(() => view.content.querySelectorAll('.chat-msg__text').length);
-            await u.waitUntil(() => Array.from(view.content.querySelectorAll('.chat-msg__text'))
+            await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
+            await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-msg__text'))
                 .map(e => e.textContent).join(' ') === "2nd Message 3rd Message 4th Message 5th Message 6th Message", 1000);
             done();
         }));
@@ -455,7 +455,7 @@ describe("Message Archive Management", function () {
                         <stanza-id xmlns="urn:xmpp:sid:0" id="45fbbf2a-1059-479d-9283-c8effaf05621" by="trek-radio@conference.lightwitch.org"/>
                     </message>`);
                 _converse.connection._dataRecv(mock.createRequest(stanza));
-                await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
+                await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
                 expect(view.model.messages.length).toBe(1);
                 expect(view.model.messages.at(0).get('is_archived')).toBe(false);
                 expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621');
@@ -480,7 +480,7 @@ describe("Message Archive Management", function () {
                 expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
                 const result = view.model.getDuplicateMessage.calls.all()[0].returnValue
                 expect(result instanceof _converse.Message).toBe(true);
-                expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
+                expect(view.querySelectorAll('.chat-msg').length).toBe(1);
 
                 await u.waitUntil(() => view.model.updateMessage.calls.count());
                 expect(view.model.messages.length).toBe(1);
@@ -502,7 +502,7 @@ describe("Message Archive Management", function () {
                         <stanza-id xmlns="urn:xmpp:sid:0" id="45fbbf2a-1059-479d-9283-c8effaf05621" by="trek-radio@conference.lightwitch.org"/>
                     </message>`);
                 _converse.connection._dataRecv(mock.createRequest(stanza));
-                await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
+                await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
                 // Not sure whether such a race-condition might pose a problem
                 // in "real-world" situations.
                 stanza = u.toStanza(
@@ -524,7 +524,7 @@ describe("Message Archive Management", function () {
                 expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
                 const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
                 expect(result instanceof _converse.Message).toBe(true);
-                expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
+                expect(view.querySelectorAll('.chat-msg').length).toBe(1);
                 done();
             }));
 
@@ -550,8 +550,8 @@ describe("Message Archive Management", function () {
                         </result>
                     </message>`);
                 _converse.handleMAMResult(view.model, { 'messages': [stanza] });
-                await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
-                expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
+                await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
+                expect(view.querySelectorAll('.chat-msg').length).toBe(1);
 
                 stanza = u.toStanza(
                     `<message xmlns="jabber:client" to="romeo@montague.lit/orchard" from="discuss@conference.conversejs.org">
@@ -574,7 +574,7 @@ describe("Message Archive Management", function () {
                 expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
                 const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
                 expect(result instanceof _converse.Message).toBe(true);
-                expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
+                expect(view.querySelectorAll('.chat-msg').length).toBe(1);
                 done();
             }))
         });

+ 1 - 1
spec/me-messages.js

@@ -22,7 +22,7 @@ describe("A Groupchat Message", function () {
                 'type': 'groupchat'
             }).c('body').t(message).tree();
         await view.model.handleMessageStanza(msg);
-        await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop());
+        await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view).pop());
         await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent.trim() === 'is tired');
         expect(view.querySelector('.chat-msg__author').textContent.includes('**Dyon van de Wege')).toBeTruthy();
 

+ 1 - 1
spec/mentions.js

@@ -381,7 +381,7 @@ describe("A sent groupchat message", function () {
 
             const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
             await u.waitUntil(() =>
-                view.content.querySelector(last_msg_sel).innerHTML.replace(/<!---->/g, '') ===
+                view.querySelector(last_msg_sel).innerHTML.replace(/<!---->/g, '') ===
                     'hello <span class="mention">z3r0</span> <span class="mention">gibson</span> <span class="mention">mr.robot</span>, how are you?'
             );
 

+ 82 - 82
spec/messages.js

@@ -190,53 +190,53 @@ describe("A Chat Message", function () {
         await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 7);
 
         view.clearSpinner(); //cleanup
-        expect(view.content.querySelectorAll('.date-separator').length).toEqual(4);
+        expect(view.querySelectorAll('.date-separator').length).toEqual(4);
 
-        let day = sizzle('.date-separator:first', view.content).pop();
+        let day = sizzle('.date-separator:first', view).pop();
         expect(day.getAttribute('data-isodate')).toEqual(dayjs('2017-12-31T00:00:00').toISOString());
 
-        let time = sizzle('time:first', view.content).pop();
+        let time = sizzle('time:first', view).pop();
         expect(time.textContent).toEqual('Sunday Dec 31st 2017')
 
-        day = sizzle('.date-separator:first', view.content).pop();
+        day = sizzle('.date-separator:first', view).pop();
         expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Older message');
 
-        let el = sizzle('.chat-msg:first', view.content).pop().querySelector('.chat-msg__text')
+        let el = sizzle('.chat-msg:first', view).pop().querySelector('.chat-msg__text')
         expect(u.hasClass('chat-msg--followup', el)).toBe(false);
         expect(el.textContent).toEqual('Older message');
 
-        time = sizzle('time.separator-text:eq(1)', view.content).pop();
+        time = sizzle('time.separator-text:eq(1)', view).pop();
         expect(time.textContent).toEqual("Monday Jan 1st 2018");
 
-        day = sizzle('.date-separator:eq(1)', view.content).pop();
+        day = sizzle('.date-separator:eq(1)', view).pop();
         expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-01T00:00:00').toISOString());
         expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Inbetween message');
 
-        el = sizzle('.chat-msg:eq(1)', view.content).pop();
+        el = sizzle('.chat-msg:eq(1)', view).pop();
         expect(el.querySelector('.chat-msg__text').textContent).toEqual('Inbetween message');
         expect(el.parentElement.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message');
-        el = sizzle('.chat-msg:eq(2)', view.content).pop();
+        el = sizzle('.chat-msg:eq(2)', view).pop();
         expect(el.querySelector('.chat-msg__text').textContent)
             .toEqual('another inbetween message');
         expect(u.hasClass('chat-msg--followup', el)).toBe(true);
 
-        time = sizzle('time.separator-text:nth(2)', view.content).pop();
+        time = sizzle('time.separator-text:nth(2)', view).pop();
         expect(time.textContent).toEqual("Tuesday Jan 2nd 2018");
 
-        day = sizzle('.date-separator:nth(2)', view.content).pop();
+        day = sizzle('.date-separator:nth(2)', view).pop();
         expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-02T00:00:00').toISOString());
         expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('An earlier message on the next day');
 
-        el = sizzle('.chat-msg:eq(3)', view.content).pop();
+        el = sizzle('.chat-msg:eq(3)', view).pop();
         expect(el.querySelector('.chat-msg__text').textContent).toEqual('An earlier message on the next day');
         expect(u.hasClass('chat-msg--followup', el)).toBe(false);
 
-        el = sizzle('.chat-msg:eq(4)', view.content).pop();
+        el = sizzle('.chat-msg:eq(4)', view).pop();
         expect(el.querySelector('.chat-msg__text').textContent).toEqual('message');
         expect(el.parentElement.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('newer message from the next day');
         expect(u.hasClass('chat-msg--followup', el)).toBe(false);
 
-        day = sizzle('.date-separator:last', view.content).pop();
+        day = sizzle('.date-separator:last', view).pop();
         expect(day.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString());
         expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message');
         expect(u.hasClass('chat-msg--followup', el)).toBe(false);
@@ -310,12 +310,12 @@ describe("A Chat Message", function () {
         expect(msg_obj.get('sender')).toEqual('them');
         expect(msg_obj.get('is_delayed')).toEqual(false);
         // Now check that the message appears inside the chatbox in the DOM
-        await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text'));
+        await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text'));
 
-        expect(view.content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext);
-        expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
+        expect(view.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext);
+        expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
         await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet')
-        expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
+        expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
         done();
     }));
 
@@ -578,7 +578,7 @@ describe("A Chat Message", function () {
             </message>`);
         _converse.connection._dataRecv(mock.createRequest(stanza));
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
-        expect(view.content.querySelector('.chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\nHave you heard the news?');
+        expect(view.querySelector('.chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\nHave you heard the news?');
         stanza = u.toStanza(`
             <message from="${contact_jid}"
                      type="chat"
@@ -587,7 +587,7 @@ describe("A Chat Message", function () {
             </message>`);
         _converse.connection._dataRecv(mock.createRequest(stanza));
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
-        const text = view.content.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '');
+        const text = view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '');
         expect(text).toBe('Hey\n\u200B\nHave you heard the news?');
         stanza = u.toStanza(`
             <message from="${contact_jid}"
@@ -597,7 +597,7 @@ describe("A Chat Message", function () {
             </message>`);
         _converse.connection._dataRecv(mock.createRequest(stanza));
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
-        expect(view.content.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\nHave you heard\nthe news?');
+        expect(view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\nHave you heard\nthe news?');
 
         stanza = u.toStanza(`
             <message from="${contact_jid}"
@@ -608,7 +608,7 @@ describe("A Chat Message", function () {
         _converse.connection._dataRecv(mock.createRequest(stanza));
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4);
         await u.waitUntil(() => {
-            const text = view.content.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '');
+            const text = view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '');
             return text === 'Hey\nHave you heard\n\u200B\nthe news?\n<a target="_blank" rel="noopener" href="https://conversejs.org/">https://conversejs.org</a>';
         });
         done();
@@ -655,7 +655,7 @@ describe("A Chat Message", function () {
         message = 'https://imgur.com/oxymPax';
         await mock.sendMessage(view, message);
         await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 5, 1000);
-        expect(view.content.querySelectorAll('.chat-content .chat-image').length).toBe(5);
+        expect(view.querySelectorAll('.chat-content .chat-image').length).toBe(5);
 
         // Check that the Imgur URL gets a .png attached to make it render
         await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-content .chat-image')).pop().src.endsWith('png'), 1000);
@@ -681,7 +681,7 @@ describe("A Chat Message", function () {
         await mock.sendMessage(view, message);
         await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg').length === 2, 1000);
         await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 1, 1000)
-        expect(view.content.querySelectorAll('.chat-content .chat-image').length).toBe(1);
+        expect(view.querySelectorAll('.chat-content .chat-image').length).toBe(1);
 
         done();
     }));
@@ -821,24 +821,24 @@ describe("A Chat Message", function () {
         jasmine.clock().tick(1*ONE_MINUTE_LATER);
         await mock.sendMessage(view, "Another message within 10 minutes, but from a different person");
 
-        expect(view.content.querySelectorAll('.message').length).toBe(6);
-        expect(view.content.querySelectorAll('.chat-msg').length).toBe(5);
+        expect(view.querySelectorAll('.message').length).toBe(6);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(5);
 
         const nth_child = (n) => `converse-chat-message:nth-child(${n}) .chat-msg`;
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(2)))).toBe(false);
-        expect(view.content.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(2)))).toBe(false);
+        expect(view.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
 
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(3)))).toBe(true);
-        expect(view.content.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(3)))).toBe(true);
+        expect(view.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
             "Another message 3 minutes later");
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(4)))).toBe(false);
-        expect(view.content.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(4)))).toBe(false);
+        expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
             "Another message 14 minutes since we started");
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(5)))).toBe(true);
-        expect(view.content.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(true);
+        expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
             "Another message 1 minute and 1 second since the previous one");
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(6)))).toBe(false);
-        expect(view.content.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(false);
+        expect(view.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
             "Another message within 10 minutes, but from a different person");
 
         // Let's add a delayed, inbetween message
@@ -854,29 +854,29 @@ describe("A Chat Message", function () {
               .tree());
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
 
-        expect(view.content.querySelectorAll('.message').length).toBe(7);
-        expect(view.content.querySelectorAll('.chat-msg').length).toBe(6);
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(2)))).toBe(false);
-        expect(view.content.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
+        expect(view.querySelectorAll('.message').length).toBe(7);
+        expect(view.querySelectorAll('.chat-msg').length).toBe(6);
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(2)))).toBe(false);
+        expect(view.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
 
 
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(3)))).toBe(true);
-        expect(view.content.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(3)))).toBe(true);
+        expect(view.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
             "Another message 3 minutes later");
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(4)))).toBe(true);
-        expect(view.content.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(4)))).toBe(true);
+        expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
             "A delayed message, sent 5 minutes since we started");
 
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(5)))).toBe(true);
-        expect(view.content.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(true);
+        expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
             "Another message 14 minutes since we started");
 
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(6)))).toBe(true);
-        expect(view.content.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(true);
+        expect(view.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
             "Another message 1 minute and 1 second since the previous one");
 
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(7)))).toBe(false);
-        expect(view.content.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(7)))).toBe(false);
+        expect(view.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe(
             "Another message within 10 minutes, but from a different person");
 
         _converse.handleMessageStanza(
@@ -891,26 +891,26 @@ describe("A Chat Message", function () {
             .tree());
         await new Promise(resolve => view.model.messages.once('rendered', resolve));
 
-        expect(view.content.querySelectorAll('.chat-msg').length).toBe(7);
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(2)))).toBe(false);
-        expect(view.content.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(3)))).toBe(true);
-        expect(view.content.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
+        expect(view.querySelectorAll('.chat-msg').length).toBe(7);
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(2)))).toBe(false);
+        expect(view.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(3)))).toBe(true);
+        expect(view.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
             "Another message 3 minutes later");
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(4)))).toBe(false);
-        expect(view.content.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(4)))).toBe(false);
+        expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
             "A carbon message 4 minutes later");
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(5)))).toBe(false);
-        expect(view.content.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(false);
+        expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
             "A delayed message, sent 5 minutes since we started");
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(6)))).toBe(true);
-        expect(view.content.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(true);
+        expect(view.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
             "Another message 14 minutes since we started");
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(7)))).toBe(true);
-        expect(view.content.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(7)))).toBe(true);
+        expect(view.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe(
             "Another message 1 minute and 1 second since the previous one");
-        expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(8)))).toBe(false);
-        expect(view.content.querySelector(`${nth_child(8)} .chat-msg__text`).textContent).toBe(
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(8)))).toBe(false);
+        expect(view.querySelector(`${nth_child(8)} .chat-msg__text`).textContent).toBe(
             "Another message within 10 minutes, but from a different person");
 
         jasmine.clock().uninstall();
@@ -1000,11 +1000,11 @@ describe("A Chat Message", function () {
             expect(msg_obj.get('sender')).toEqual('them');
             expect(msg_obj.get('is_delayed')).toEqual(false);
             // Now check that the message appears inside the chatbox in the DOM
-            const mel = await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text'));
+            const mel = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text'));
             expect(mel.textContent).toEqual(message);
-            expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
+            expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
             await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]);
-            expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
+            expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
             done();
         }));
 
@@ -1030,7 +1030,7 @@ describe("A Chat Message", function () {
             expect(view.model.messages.length).toEqual(1);
             const msg_obj = view.model.messages.at(0);
             expect(msg_obj.get('message')).toEqual(message.trim());
-            const mel = await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text'));
+            const mel = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text'));
             expect(mel.textContent).toEqual(message.trim());
             done();
         }));
@@ -1138,9 +1138,9 @@ describe("A Chat Message", function () {
 
                 await u.waitUntil(() => view.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio');
                 // Now check that the message appears inside the chatbox in the DOM
-                expect(view.content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
-                expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
-                expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
+                expect(view.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
+                expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
+                expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
                 done();
             }));
         });
@@ -1174,7 +1174,7 @@ describe("A Chat Message", function () {
                 const view = _converse.api.chatviews.get(sender_jid);
                 const message = await view.model.sendMessage(msg_text);
                 await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
-                let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
+                let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent;
                 expect(msg_txt).toEqual(msg_text);
 
                 // We send another message, for which an error will
@@ -1182,8 +1182,8 @@ describe("A Chat Message", function () {
                 // after the relevant message.
                 msg_text = 'This message will be sent, and also receive an error';
                 const second_message = await view.model.sendMessage(msg_text);
-                await u.waitUntil(() => sizzle('.chat-msg .chat-msg__text', view.content).length === 2, 1000);
-                msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
+                await u.waitUntil(() => sizzle('.chat-msg .chat-msg__text', view).length === 2, 1000);
+                msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent;
                 expect(msg_txt).toEqual(msg_text);
 
                 /* <message xmlns="jabber:client"
@@ -1208,7 +1208,7 @@ describe("A Chat Message", function () {
                     .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
                         .t('Server-to-server connection failed: Connecting failed: connection timeout');
                 _converse.connection._dataRecv(mock.createRequest(stanza));
-                await u.waitUntil(() => view.content.querySelector('.chat-msg__error').textContent.trim() === error_txt);
+                await u.waitUntil(() => view.querySelector('.chat-msg__error').textContent.trim() === error_txt);
 
                 const other_error_txt = 'Server-to-server connection failed: Connecting failed: connection timeout';
                 stanza = $msg({
@@ -1223,7 +1223,7 @@ describe("A Chat Message", function () {
                         .t(other_error_txt);
                 _converse.connection._dataRecv(mock.createRequest(stanza));
                 await u.waitUntil(() =>
-                    view.content.querySelector('converse-chat-message:last-child .chat-msg__error').textContent.trim() === other_error_txt);
+                    view.querySelector('converse-chat-message:last-child .chat-msg__error').textContent.trim() === other_error_txt);
 
                 // We don't render duplicates
                 stanza = $msg({
@@ -1237,11 +1237,11 @@ describe("A Chat Message", function () {
                     .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
                         .t('Server-to-server connection failed: Connecting failed: connection timeout');
                 _converse.connection._dataRecv(mock.createRequest(stanza));
-                expect(view.content.querySelectorAll('.chat-msg__error').length).toEqual(2);
+                expect(view.querySelectorAll('.chat-msg__error').length).toEqual(2);
 
                 msg_text = 'This message will be sent, and also receive an error';
                 const third_message = await view.model.sendMessage(msg_text);
-                await u.waitUntil(() => sizzle('converse-chat-message:last-child .chat-msg__text', view.content).pop()?.textContent === msg_text);
+                await u.waitUntil(() => sizzle('converse-chat-message:last-child .chat-msg__text', view).pop()?.textContent === msg_text);
 
                 // A different error message will however render
                 stanza = $msg({
@@ -1302,7 +1302,7 @@ describe("A Chat Message", function () {
                 const msg_text = 'This message will show!';
                 await view.model.sendMessage(msg_text);
                 await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
-                expect(view.content.querySelectorAll('.chat-error').length).toEqual(0);
+                expect(view.querySelectorAll('.chat-error').length).toEqual(0);
                 done();
             }));
         });
@@ -1317,7 +1317,7 @@ describe("A Chat Message", function () {
             const view = _converse.api.chatviews.get(sender_jid);
             // Create enough messages so that there's a scrollbar.
             const promises = [];
-            view.content.scrollTop = 0;
+            view.querySelector('.chat-content').scrollTop = 0;
             view.model.set('scrolled', true);
 
             for (let i=0; i<20; i++) {
@@ -1336,7 +1336,7 @@ describe("A Chat Message", function () {
             expect(u.isVisible(indicator_el)).toBeTruthy();
 
             expect(view.model.get('scrolled')).toBe(true);
-            expect(view.content.scrollTop).toBe(0);
+            expect(view.querySelector('.chat-content').scrollTop).toBe(0);
             indicator_el.click();
             expect(u.isVisible(indicator_el)).toBeFalsy();
             expect(view.model.get('scrolled')).toBe(false);

+ 22 - 20
spec/muc.js

@@ -927,7 +927,7 @@ describe("Groupchats", function () {
 
             const num_info_msgs = await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-info').length);
             expect(num_info_msgs).toBe(1);
-            expect(sizzle('div.chat-info', view.content).pop().textContent.trim()).toBe("This groupchat is not anonymous");
+            expect(sizzle('div.chat-info', view).pop().textContent.trim()).toBe("This groupchat is not anonymous");
 
             const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent);
             expect(csntext.trim()).toEqual("some1 has entered the groupchat");
@@ -1408,7 +1408,7 @@ describe("Groupchats", function () {
                 }).c('body').t('Some message').tree();
 
             await view.model.handleMessageStanza(msg);
-            await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop());
+            await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view).pop());
 
             let stanza = u.toStanza(
                 `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="conversations@conference.siacs.eu/Guus">
@@ -2089,8 +2089,8 @@ describe("Groupchats", function () {
             }).c('body').t(text);
             await view.model.handleMessageStanza(message.nodeTree);
             await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
-            expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
-            expect(view.content.querySelector('.chat-msg__text').textContent.trim()).toBe(text);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+            expect(view.querySelector('.chat-msg__text').textContent.trim()).toBe(text);
             expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
             done();
         }));
@@ -2111,7 +2111,7 @@ describe("Groupchats", function () {
             await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
 
             expect(_converse.api.trigger).toHaveBeenCalledWith('messageSend', jasmine.any(_converse.Message));
-            expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
 
             // Let's check that if we receive the same message again, it's
             // not shown.
@@ -2127,7 +2127,7 @@ describe("Groupchats", function () {
                     <origin-id xmlns="urn:xmpp:sid:0" id="${view.model.messages.at(0).get('origin_id')}"/>
                 </message>`);
             await view.model.handleMessageStanza(stanza);
-            expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
+            expect(view.querySelectorAll('.chat-msg').length).toBe(1);
             expect(sizzle('.chat-msg__text:last').pop().textContent.trim()).toBe(text);
             expect(view.model.messages.length).toBe(1);
             // We don't emit an event if it's our own message
@@ -2158,7 +2158,8 @@ describe("Groupchats", function () {
             await Promise.all(promises);
             // Give enough time for `markScrolled` to have been called
             setTimeout(async () => {
-                view.content.scrollTop = 0;
+                const content = view.querySelector('.chat-content');
+                content.scrollTop = 0;
                 await view.model.handleMessageStanza(
                     $msg({
                         from: 'lounge@montague.lit/someone',
@@ -2168,9 +2169,9 @@ describe("Groupchats", function () {
                     }).c('body').t(message).tree());
                 await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 21);
                 // Now check that the message appears inside the chatbox in the DOM
-                const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
+                const msg_txt = sizzle('.chat-msg:last .chat-msg__text', content).pop().textContent;
                 expect(msg_txt).toEqual(message);
-                expect(view.content.scrollTop).toBe(0);
+                expect(content.scrollTop).toBe(0);
                 done();
             }, 500);
         }));
@@ -2346,8 +2347,8 @@ describe("Groupchats", function () {
 
             _converse.connection._dataRecv(mock.createRequest(presence));
             expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED);
-            expect(view.content.querySelectorAll('div.chat-info').length).toBe(1);
-            expect(sizzle('div.chat-info', view.content)[0].textContent.trim()).toBe(
+            expect(view.querySelectorAll('div.chat-info').length).toBe(1);
+            expect(sizzle('div.chat-info', view)[0].textContent.trim()).toBe(
                 __(_converse.muc.new_nickname_messages["303"], "newnick")
             );
             occupants = view.querySelector('.occupant-list');
@@ -2964,7 +2965,7 @@ describe("Groupchats", function () {
             bottom_panel.onKeyDown(enter);
 
             await u.waitUntil(() => sizzle('converse-chat-help .chat-info', view).length);
-            const chat_help_el = view.querySelector('converse-chat-help');
+            let chat_help_el = view.querySelector('converse-chat-help');
             let info_messages = sizzle('.chat-info', chat_help_el);
             expect(info_messages.length).toBe(19);
             expect(info_messages.pop().textContent.trim()).toBe('/voice: Allow muted user to post messages');
@@ -2991,11 +2992,12 @@ describe("Groupchats", function () {
             occupant.set('affiliation', 'admin');
 
             view.querySelector('.close-chat-help').click();
-            await u.waitUntil(() => chat_help_el.hidden);
+            expect(view.model.get('show_help_messages')).toBe(false);
+            await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
 
             textarea.value = '/help';
             bottom_panel.onKeyDown(enter);
-            await u.waitUntil(() => !chat_help_el.hidden);
+            chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
             info_messages = sizzle('.chat-info', chat_help_el);
             expect(info_messages.length).toBe(18);
             let commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
@@ -3006,18 +3008,18 @@ describe("Groupchats", function () {
             ]);
             occupant.set('affiliation', 'member');
             view.querySelector('.close-chat-help').click();
-            await u.waitUntil(() => chat_help_el.hidden);
+            await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
 
             textarea.value = '/help';
             bottom_panel.onKeyDown(enter);
-            await u.waitUntil(() => !chat_help_el.hidden);
+            chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
             info_messages = sizzle('.chat-info', chat_help_el);
             expect(info_messages.length).toBe(9);
             commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
             expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/modtools", "/mute", "/nick", "/register", "/voice"]);
 
             view.querySelector('.close-chat-help').click();
-            await u.waitUntil(() => chat_help_el.hidden);
+            await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
             expect(view.model.get('show_help_messages')).toBe(false);
 
             occupant.set('role', 'participant');
@@ -3026,7 +3028,7 @@ describe("Groupchats", function () {
             textarea.value = '/help';
             bottom_panel.onKeyDown(enter);
             await u.waitUntil(() => view.model.get('show_help_messages'));
-            await u.waitUntil(() => !chat_help_el.hidden);
+            chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
             info_messages = sizzle('.chat-info', chat_help_el);
             expect(info_messages.length).toBe(5);
             commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
@@ -3036,11 +3038,11 @@ describe("Groupchats", function () {
             // Note: we're making a shortcut here, this value should never be set manually
             view.model.config.set('changesubject', true);
             view.querySelector('.close-chat-help').click();
-            await u.waitUntil(() => chat_help_el.hidden);
+            await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
 
             textarea.value = '/help';
             bottom_panel.onKeyDown(enter);
-            await u.waitUntil(() => !chat_help_el.hidden, 1000);
+            chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
             info_messages = sizzle('.chat-info', chat_help_el);
             expect(info_messages.length).toBe(7);
             commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));

+ 1 - 1
src/headless/plugins/roster/contacts.js

@@ -73,7 +73,7 @@ const RosterContacts = Collection.extend({
         if (u.isErrorObject(result)) {
             log.error(result);
             // Force a full roster refresh
-            _converse.session.set('roster_cached', false)
+            _converse.session.save('roster_cached', false)
             this.data.save('version', undefined);
         }
 

+ 5 - 1
src/plugins/chatview/bottom_panel.js

@@ -19,10 +19,10 @@ export default class ChatBottomPanel extends ElementView {
         super.connectedCallback();
         this.model = _converse.chatboxes.get(this.getAttribute('jid'));
         this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
-
         await this.model.initialized;
         this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
         this.render();
+        api.listen.on('chatBoxScrolledDown', () => this.hideNewMessagesIndicator());
     }
 
     render () {
@@ -79,6 +79,10 @@ export default class ChatBottomPanel extends ElementView {
         this.renderToolbar();
     }
 
+    hideNewMessagesIndicator () {
+        this.querySelector('.new-msgs-indicator')?.classList.add('hidden');
+    }
+
     onMessageCorrecting (message) {
         if (message.get('correcting')) {
             this.insertIntoTextArea(u.prefixMentions(message), true, true);

+ 16 - 2
src/plugins/chatview/view.js

@@ -1,10 +1,10 @@
 import 'plugins/chatview/heading.js';
 import 'plugins/chatview/bottom_panel.js';
+import { html, render } from 'lit-html';
 import BaseChatView from 'shared/chat/baseview.js';
 import tpl_chat from './templates/chat.js';
 import { __ } from 'i18n';
 import { _converse, api, converse } from '@converse/headless/core';
-import { render } from 'lit-html';
 
 const u = converse.env.utils;
 const { dayjs } = converse.env;
@@ -58,11 +58,25 @@ export default class ChatView extends BaseChatView {
             this.model.toJSON(), { 'markScrolled': ev => this.markScrolled(ev) })
         );
         render(result, this);
-        this.content = this.querySelector('.chat-content');
         this.help_container = this.querySelector('.chat-content__help');
         return this;
     }
 
+    renderHelpMessages () {
+        render(
+            html`
+                <converse-chat-help
+                    .model=${this.model}
+                    .messages=${this.getHelpMessages()}
+                    ?hidden=${!this.model.get('show_help_messages')}
+                    type="info"
+                    chat_type="${this.model.get('type')}"
+                ></converse-chat-help>
+            `,
+            this.help_container
+        );
+    }
+
     getHelpMessages () { // eslint-disable-line class-methods-use-this
         return [
             `<strong>/clear</strong>: ${__('Remove messages')}`,

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

@@ -52,7 +52,6 @@ class HeadlinesView extends BaseChatView {
             })
         );
         render(result, this);
-        this.content = this.querySelector('.chat-content');
         return this;
     }
 

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

@@ -190,10 +190,11 @@ function onMaximized (view) {
  */
 function onMinimized (view) {
     // save the scroll position to restore it on maximize
+    const content = view.querySelector('.chat-content__messages');
     if (view.model.collection && view.model.collection.browserStorage) {
-        view.model.save({ 'scroll': view.content.scrollTop });
+        view.model.save({ 'scroll': content.scrollTop });
     } else {
-        view.model.set({ 'scroll': view.content.scrollTop });
+        view.model.set({ 'scroll': content.scrollTop });
     }
     view.model.setChatState(_converse.INACTIVE);
     /**

+ 129 - 0
src/plugins/muc-views/chatarea.js

@@ -0,0 +1,129 @@
+import debounce from 'lodash-es/debounce';
+import tpl_muc_chatarea from './templates/muc-chatarea.js';
+import { CustomElement } from 'components/element.js';
+import { __ } from 'i18n';
+import { _converse, api, converse } from '@converse/headless/core';
+
+
+const { u } = converse.env;
+
+
+export default class MUCChatArea extends CustomElement {
+
+    static get properties () {
+        return {
+            jid: { type: String },
+            show_help_messages: { type: Boolean },
+            type: { type: String },
+        }
+    }
+
+    connectedCallback () {
+        super.connectedCallback();
+        this.model = _converse.chatboxes.get(this.jid);
+        this.markScrolled = debounce(this._markScrolled, 100);
+        this.listenTo(this.model, 'change:show_help_messages', () => this.requestUpdate());
+    }
+
+    render () {
+        return tpl_muc_chatarea({
+            'help_messages': this.getHelpMessages(),
+            'jid': this.jid,
+            'model': this.model,
+            'occupants': this.model.occupants,
+            'show_help_messages': this.model.get('show_help_messages'),
+            'show_send_button': _converse.show_send_button,
+            'show_sidebar': this.shouldShowSidebar(),
+            'type': this.type,
+        });
+    }
+
+    shouldShowSidebar () {
+        return (
+            !this.model.get('hidden_occupants') &&
+            this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED
+        );
+    }
+
+    getHelpMessages () {
+        const setting = api.settings.get('muc_disable_slash_commands');
+        const disabled_commands = Array.isArray(setting) ? setting : [];
+        return [
+            `<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
+            `<strong>/ban</strong>: ${__('Ban user by changing their affiliation to outcast')}`,
+            `<strong>/clear</strong>: ${__('Clear the chat area')}`,
+            `<strong>/close</strong>: ${__('Close this groupchat')}`,
+            `<strong>/deop</strong>: ${__('Change user role to participant')}`,
+            `<strong>/destroy</strong>: ${__('Remove this groupchat')}`,
+            `<strong>/help</strong>: ${__('Show this menu')}`,
+            `<strong>/kick</strong>: ${__('Kick user from groupchat')}`,
+            `<strong>/me</strong>: ${__('Write in 3rd person')}`,
+            `<strong>/member</strong>: ${__('Grant membership to a user')}`,
+            `<strong>/modtools</strong>: ${__('Opens up the moderator tools GUI')}`,
+            `<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`,
+            `<strong>/nick</strong>: ${__('Change your nickname')}`,
+            `<strong>/op</strong>: ${__('Grant moderator role to user')}`,
+            `<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`,
+            `<strong>/register</strong>: ${__('Register your nickname')}`,
+            `<strong>/revoke</strong>: ${__("Revoke the user's current affiliation")}`,
+            `<strong>/subject</strong>: ${__('Set groupchat subject')}`,
+            `<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`,
+            `<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
+        ]
+            .filter(line => disabled_commands.every(c => !line.startsWith(c + '<', 9)))
+            .filter(line => this.model.getAllowedCommands().some(c => line.startsWith(c + '<', 9)));
+    }
+
+    /**
+     * Called when the chat content is scrolled up or down.
+     * We want to record when the user has scrolled away from
+     * the bottom, so that we don't automatically scroll away
+     * from what the user is reading when new messages are received.
+     *
+     * Don't call this method directly, instead, call `markScrolled`,
+     * which debounces this method by 100ms.
+     * @private
+     */
+    _markScrolled (ev) {
+        let scrolled = true;
+        let scrollTop = null;
+        const msgs_container = this.querySelector('.chat-content__messages');
+        const is_at_bottom =
+            msgs_container.scrollTop + msgs_container.clientHeight >= msgs_container.scrollHeight - 62; // sigh...
+
+        if (is_at_bottom) {
+            scrolled = false;
+            this.onScrolledDown();
+        } else if (msgs_container.scrollTop === 0) {
+            /**
+             * Triggered once the chat's message area has been scrolled to the top
+             * @event _converse#chatBoxScrolledUp
+             * @property { _converse.ChatBoxView | _converse.ChatRoomView } view
+             * @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
+             */
+            api.trigger('chatBoxScrolledUp', this);
+        } else {
+            scrollTop = ev.target.scrollTop;
+        }
+        u.safeSave(this.model, { scrolled, scrollTop });
+    }
+
+    onScrolledDown () {
+        if (!this.model.isHidden()) {
+            this.model.clearUnreadMsgCounter();
+            // Clear location hash if set to one of the messages in our history
+            const hash = window.location.hash;
+            hash && this.model.messages.get(hash.slice(1)) && _converse.router.history.navigate();
+        }
+        /**
+         * Triggered once the chat's message area has been scrolled down to the bottom.
+         * @event _converse#chatBoxScrolledDown
+         * @type {object}
+         * @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
+         * @example _converse.api.listen.on('chatBoxScrolledDown', obj => { ... });
+         */
+        api.trigger('chatBoxScrolledDown', { 'chatbox': this.model });
+    }
+}
+
+api.elements.define('converse-muc-chatarea', MUCChatArea);

+ 8 - 39
src/plugins/muc-views/muc.js

@@ -63,7 +63,6 @@ export default class MUCView extends BaseChatView {
         await this.render();
 
         // Need to be registered after render has been called.
-        this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
         this.listenTo(this.model.messages, 'add', this.onMessageAdded);
         this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification);
         this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
@@ -85,7 +84,8 @@ export default class MUCView extends BaseChatView {
         this.setAttribute('id', this.model.get('box_id'));
         render(
             tpl_muc({
-                sidebar_hidden,
+                'chatview': this,
+                'conn_status': this.model.session.get('connection_status'),
                 'model': this.model,
                 'occupants': this.model.occupants,
                 'show_sidebar':
@@ -93,13 +93,13 @@ export default class MUCView extends BaseChatView {
                     this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED,
                 'markScrolled': ev => this.markScrolled(ev),
                 'muc_show_logs_before_join': api.settings.get('muc_show_logs_before_join'),
-                'show_send_button': _converse.show_send_button
+                'show_send_button': _converse.show_send_button,
+                sidebar_hidden,
             }),
             this
         );
 
         this.notifications = this.querySelector('.chat-content__notifications');
-        this.content = this.querySelector('.chat-content');
         this.help_container = this.querySelector('.chat-content__help');
 
         if (
@@ -114,35 +114,6 @@ export default class MUCView extends BaseChatView {
         !this.model.get('hidden') && this.show();
     }
 
-    getHelpMessages () {
-        const setting = api.settings.get('muc_disable_slash_commands');
-        const disabled_commands = Array.isArray(setting) ? setting : [];
-        return [
-            `<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
-            `<strong>/ban</strong>: ${__('Ban user by changing their affiliation to outcast')}`,
-            `<strong>/clear</strong>: ${__('Clear the chat area')}`,
-            `<strong>/close</strong>: ${__('Close this groupchat')}`,
-            `<strong>/deop</strong>: ${__('Change user role to participant')}`,
-            `<strong>/destroy</strong>: ${__('Remove this groupchat')}`,
-            `<strong>/help</strong>: ${__('Show this menu')}`,
-            `<strong>/kick</strong>: ${__('Kick user from groupchat')}`,
-            `<strong>/me</strong>: ${__('Write in 3rd person')}`,
-            `<strong>/member</strong>: ${__('Grant membership to a user')}`,
-            `<strong>/modtools</strong>: ${__('Opens up the moderator tools GUI')}`,
-            `<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`,
-            `<strong>/nick</strong>: ${__('Change your nickname')}`,
-            `<strong>/op</strong>: ${__('Grant moderator role to user')}`,
-            `<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`,
-            `<strong>/register</strong>: ${__('Register your nickname')}`,
-            `<strong>/revoke</strong>: ${__("Revoke the user's current affiliation")}`,
-            `<strong>/subject</strong>: ${__('Set groupchat subject')}`,
-            `<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`,
-            `<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
-        ]
-            .filter(line => disabled_commands.every(c => !line.startsWith(c + '<', 9)))
-            .filter(line => this.model.getAllowedCommands().some(c => line.startsWith(c + '<', 9)));
-    }
-
     onStartResizeOccupants (ev) {
         this.resizing = true;
         this.addEventListener('mousemove', this.onMouseMove);
@@ -361,7 +332,7 @@ export default class MUCView extends BaseChatView {
     renderNicknameForm () {
         if (api.settings.get('muc_show_logs_before_join')) {
             this.hideSpinner();
-            u.showElement(this.querySelector('.chat-area'));
+            u.showElement(this.querySelector('converse-muc-chatarea'));
         } else {
             const form = this.querySelector('.muc-nickname-form');
             const tpl_result = tpl_muc_nickname_form(this.model.toJSON());
@@ -443,8 +414,7 @@ export default class MUCView extends BaseChatView {
     }
 
     showDestroyedMessage () {
-        u.hideElement(this.querySelector('.chat-area'));
-        u.hideElement(this.querySelector('.occupants'));
+        u.hideElement(this.querySelector('converse-muc-chatarea'));
         sizzle('.spinner', this).forEach(u.removeElement);
 
         const reason = this.model.get('destroyed_reason');
@@ -472,8 +442,7 @@ export default class MUCView extends BaseChatView {
         if (!message) {
             return;
         }
-        u.hideElement(this.querySelector('.chat-area'));
-        u.hideElement(this.querySelector('.occupants'));
+        u.hideElement(this.querySelector('converse-muc-chatarea'));
         sizzle('.spinner', this).forEach(u.removeElement);
 
         const messages = [message];
@@ -542,7 +511,7 @@ export default class MUCView extends BaseChatView {
         } else if (conn_status === converse.ROOMSTATUS.ENTERED) {
             this.hideSpinner();
             this.hideChatRoomContents();
-            u.showElement(this.querySelector('.chat-area'));
+            u.showElement(this.querySelector('converse-muc-chatarea'));
             this.querySelector('.occupants')?.setVisibility();
             this.scrollDown();
             this.maybeFocus();

+ 27 - 0
src/plugins/muc-views/templates/muc-chatarea.js

@@ -0,0 +1,27 @@
+import { html } from "lit-html";
+import { _converse } from '@converse/headless/core';
+
+export default (o) => html`
+    <div class="chat-area">
+        <div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
+            <converse-chat-content
+                class="chat-content__messages"
+                jid="${o.jid}"
+                @scroll=${o.markScrolled}></converse-chat-content>
+
+            ${o.show_help_messages ? html`<div class="chat-content__help">
+                    <converse-chat-help
+                        .model=${o.model}
+                        .messages=${o.help_messages}
+                        ?hidden=${!o.show_help_messages}
+                        type="info"
+                        chat_type="${_converse.CHATROOMS_TYPE}"
+                    ></converse-chat-help></div>` : '' }
+        </div>
+        <converse-muc-bottom-panel jid="${o.jid}" class="bottom-panel"></converse-muc-bottom-panel>
+    </div>
+    <div class="disconnect-container hidden"></div>
+    <converse-muc-sidebar class="occupants col-md-3 col-4 ${o.show_sidebar ? '' : 'hidden' }"
+        .occupants=${o.occupants}
+        .chatroom=${o.model}></converse-muc-sidebar>
+`;

+ 2 - 16
src/plugins/muc-views/templates/muc.js

@@ -1,3 +1,4 @@
+import '../chatarea.js';
 import '../bottom_panel.js';
 import '../heading.js';
 import '../sidebar.js';
@@ -8,22 +9,7 @@ export default (o) => html`
         <converse-dragresize></converse-dragresize>
         <converse-muc-heading jid="${o.model.get('jid')}" class="chat-head chat-head-chatroom row no-gutters"></converse-muc-heading>
         <div class="chat-body chatroom-body row no-gutters">
-            <div class="chat-area col">
-                <div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
-                    <converse-chat-content
-                        class="chat-content__messages"
-                        jid="${o.model.get('jid')}"
-                        @scroll=${o.markScrolled}></converse-chat-content>
-
-                    <div class="chat-content__help"></div>
-                </div>
-                <converse-muc-bottom-panel jid="${o.model.get('jid')}" class="bottom-panel"></converse-muc-bottom-panel>
-            </div>
-            <div class="disconnect-container hidden"></div>
-            <converse-muc-sidebar class="occupants col-md-3 col-4 ${o.sidebar_hidden ? 'hidden' : ''}"
-                .occupants=${o.occupants}
-                .chatroom=${o.model}></converse-muc-sidebar>
-            <div class="nickname-form-container"></div>
+            <converse-muc-chatarea jid="${o.model.get('jid')}"></converse-muc-chatarea>
         </div>
     </div>
 `;

+ 4 - 19
src/shared/chat/baseview.js

@@ -3,7 +3,6 @@ import log from '@converse/headless/log';
 import tpl_spinner from 'templates/spinner.js';
 import { ElementView } from '@converse/skeletor/src/element.js';
 import { _converse, api, converse } from '@converse/headless/core';
-import { html, render } from 'lit-html';
 
 const u = converse.env.utils;
 
@@ -14,21 +13,6 @@ export default class BaseChatView extends ElementView {
         this.debouncedScrollDown = debounce(this.scrollDown, 100);
     }
 
-    renderHelpMessages () {
-        render(
-            html`
-                <converse-chat-help
-                    .model=${this.model}
-                    .messages=${this.getHelpMessages()}
-                    ?hidden=${!this.model.get('show_help_messages')}
-                    type="info"
-                    chat_type="${this.model.get('type')}"
-                ></converse-chat-help>
-            `,
-            this.help_container
-        );
-    }
-
     hideNewMessagesIndicator () {
         const new_msgs_indicator = this.querySelector('.new-msgs-indicator');
         if (new_msgs_indicator !== null) {
@@ -103,19 +87,20 @@ export default class BaseChatView extends ElementView {
     }
 
     addSpinner (append = false) {
+        const content = this.querySelector('.chat-content');
         if (this.querySelector('.spinner') === null) {
             const el = u.getElementFromTemplateResult(tpl_spinner());
             if (append) {
-                this.content.insertAdjacentElement('beforeend', el);
+                content.insertAdjacentElement('beforeend', el);
                 this.scrollDown();
             } else {
-                this.content.insertAdjacentElement('afterbegin', el);
+                content.insertAdjacentElement('afterbegin', el);
             }
         }
     }
 
     clearSpinner () {
-        this.content.querySelectorAll('.spinner').forEach(u.removeElement);
+        this.querySelectorAll('.chat-content .spinner').forEach(u.removeElement);
     }
 
     onStatusMessageChanged (item) {