Prechádzať zdrojové kódy

Revert `flexbox`, `column-reverse` changes

Unfortunately this doesn't work on Firefox and there's no proper
workaround. https://github.com/philipwalton/flexbugs/issues/108

Reverts:

Revert "Bugfix. Properly insert error messages and spinner"
This reverts commit 6a419cc145f939d5fc6a2c70a3f64f2913395770.

Revert "Use flexbox to keep the chat scrolled down"
This reverts commit dd91d3cc55cda7e111647b32f1a75bc13cd8b9fe.
JC Brand 6 rokov pred
rodič
commit
36549bf61d
9 zmenil súbory, kde vykonal 309 pridanie a 271 odobranie
  1. 0 2
      CHANGES.md
  2. 0 2
      sass/_chatbox.scss
  3. 4 2
      sass/_messages.scss
  4. 8 8
      spec/chatbox.js
  5. 83 89
      spec/messages.js
  6. 103 107
      spec/muc.js
  7. 2 2
      spec/omemo.js
  8. 86 45
      src/converse-chatview.js
  9. 23 14
      src/converse-muc-views.js

+ 0 - 2
CHANGES.md

@@ -61,8 +61,6 @@
 - Removed events `statusChanged` and `statusMessageChanged`. Instead, you can
   listen on the `change:status` or `change:status\_message` events on
   `_converse.xmppstatus`.
-- Use flexbox instead of JavaScript to keep chat scrolled down. Due to this
-  change, messages are now inserted into the DOM in reverse order than before.
 
 ### API changes
 

+ 0 - 2
sass/_chatbox.scss

@@ -207,8 +207,6 @@
             margin-bottom: 0.25em;
         }
         .chat-content {
-            display: flex;
-            flex-direction: column-reverse;
             padding: 1em 0;
             height: 100%;
             font-size: var(--message-font-size);

+ 4 - 2
sass/_messages.scss

@@ -77,9 +77,10 @@
         }
 
         &.chat-msg {
-            display: flex;
+            display: inline-flex;
             width: 100%;
             flex-direction: row;
+            overflow: auto; // Ensures that content stays inside
             padding: 0.125rem 1rem;
 
             &.onload {
@@ -151,6 +152,7 @@
                 display: flex;
                 flex-direction: row;
                 justify-content: space-between;
+                width: 100%;
             }
 
             .chat-msg__message {
@@ -267,7 +269,7 @@
             }
             &.chat-msg--action {
                 .chat-msg__content {
-                    flex-wrap: wrap;
+                    flex-wrap: nowrap;
                     flex-direction: row;
                     justify-content: flex-start;
                 }

+ 8 - 8
spec/chatbox.js

@@ -44,7 +44,7 @@
                     }).c('body').t('hello world').tree();
                 await _converse.chatboxes.onMessage(msg);
                 await test_utils.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
-                expect(view.content.firstElementChild.textContent.trim().indexOf('hello world')).not.toBe(-1);
+                expect(view.content.lastElementChild.textContent.trim().indexOf('hello world')).not.toBe(-1);
                 done();
             }));
 
@@ -78,22 +78,22 @@
                 message = '/me is as well';
                 await test_utils.sendMessage(view, message);
                 expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2);
-                await test_utils.waitUntil(() => sizzle('.chat-msg__author:first', view.el).pop().textContent.trim() === '**Romeo Montague');
-                const last_el = sizzle('.chat-msg__text:first', view.el).pop();
+                await test_utils.waitUntil(() => sizzle('.chat-msg__author:last', view.el).pop().textContent.trim() === '**Romeo Montague');
+                const last_el = sizzle('.chat-msg__text:last', view.el).pop();
                 expect(last_el.textContent).toBe('is as well');
                 expect(u.hasClass('chat-msg--followup', last_el)).toBe(false);
                 // Check that /me messages after a normal message don't
                 // get the 'chat-msg--followup' class.
                 message = 'This a normal message';
                 await test_utils.sendMessage(view, message);
-                let message_el = view.el.querySelector('.message:first-child');
+                let message_el = view.el.querySelector('.message:last-child');
                 expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
                 message = '/me wrote a 3rd person message';
                 await test_utils.sendMessage(view, message);
-                message_el = view.el.querySelector('.message:first-child');
+                message_el = view.el.querySelector('.message:last-child');
                 expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3);
-                expect(sizzle('.chat-msg__text:first', view.el).pop().textContent).toBe('wrote a 3rd person message');
-                expect(u.isVisible(sizzle('.chat-msg__author:first', view.el).pop())).toBeTruthy();
+                expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe('wrote a 3rd person message');
+                expect(u.isVisible(sizzle('.chat-msg__author:last', view.el).pop())).toBeTruthy();
                 expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
                 done();
             }));
@@ -267,7 +267,7 @@
                 const jid = el.textContent.replace(/ /g,'.').toLowerCase() + '@montague.lit';
                 spyOn(_converse.api, "trigger");
                 el.click();
-                await test_utils.waitUntil(() => _converse.api.trigger.calls.count(), 1000);
+                await test_utils.waitUntil(() => _converse.api.trigger.calls.count(), 500);
                 expect(_converse.chatboxes.length).toEqual(2);
                 expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxFocused', jasmine.any(Object));
                 done();

+ 83 - 89
spec/messages.js

@@ -260,7 +260,7 @@
             expect(view.model.messages.at(0).get('correcting')).toBeFalsy();
             expect(view.model.messages.at(1).get('correcting')).toBeFalsy();
             expect(view.model.messages.at(2).get('correcting')).toBe(true);
-            await test_utils.waitUntil(() => u.hasClass('correcting', sizzle('.chat-msg:first', view.el).pop()), 500);
+            await test_utils.waitUntil(() => u.hasClass('correcting', sizzle('.chat-msg:last', view.el).pop()), 500);
 
             textarea.selectionEnd = 0; // Happens by pressing up,
                                     // but for some reason not in tests, so we set it manually.
@@ -285,11 +285,11 @@
             expect(textarea.value).toBe('');
             const messages = view.el.querySelectorAll('.chat-msg');
             expect(messages.length).toBe(3);
-            expect(messages[2].querySelector('.chat-msg__text').textContent)
+            expect(messages[0].querySelector('.chat-msg__text').textContent)
                 .toBe('But soft, what light through yonder window breaks?');
             expect(messages[1].querySelector('.chat-msg__text').textContent)
                 .toBe('It is the east, and Juliet is the sun.');
-            expect(messages[0].querySelector('.chat-msg__text').textContent)
+            expect(messages[2].querySelector('.chat-msg__text').textContent)
                 .toBe('Arise, fair sun, and kill the envious moon');
 
             expect(view.model.messages.at(0).get('correcting')).toBeFalsy();
@@ -434,53 +434,53 @@
             view.clearSpinner(); //cleanup
             expect(chat_content.querySelectorAll('.date-separator').length).toEqual(4);
 
-            let day = sizzle('.date-separator:last', chat_content).pop();
+            let day = sizzle('.date-separator:first', chat_content).pop();
             expect(day.getAttribute('data-isodate')).toEqual(dayjs('2017-12-31T00:00:00').toISOString());
 
-            let time = sizzle('time:last', chat_content).pop();
+            let time = sizzle('time:first', chat_content).pop();
             expect(time.textContent).toEqual('Sunday Dec 31st 2017')
 
-            day = sizzle('.date-separator:last', chat_content).pop();
-            expect(day.previousElementSibling.querySelector('.chat-msg__text').textContent).toBe('Older message');
+            day = sizzle('.date-separator:first', chat_content).pop();
+            expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Older message');
 
-            let el = sizzle('.chat-msg:last', chat_content).pop().querySelector('.chat-msg__text')
+            let el = sizzle('.chat-msg:first', chat_content).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(2)', chat_content).pop();
+            time = sizzle('time.separator-text:eq(1)', chat_content).pop();
             expect(time.textContent).toEqual("Monday Jan 1st 2018");
 
-            day = sizzle('.date-separator:eq(2)', chat_content).pop();
+            day = sizzle('.date-separator:eq(1)', chat_content).pop();
             expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-01T00:00:00').toISOString());
-            expect(day.previousElementSibling.querySelector('.chat-msg__text').textContent).toBe('Inbetween message');
+            expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Inbetween message');
 
-            el = sizzle('.chat-msg:eq(5)', chat_content).pop();
+            el = sizzle('.chat-msg:eq(1)', chat_content).pop();
             expect(el.querySelector('.chat-msg__text').textContent).toEqual('Inbetween message');
-            expect(el.previousElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message');
-            el = sizzle('.chat-msg:eq(4)', chat_content).pop();
+            expect(el.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message');
+            el = sizzle('.chat-msg:eq(2)', chat_content).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(1)', chat_content).pop();
+            time = sizzle('time.separator-text:nth(2)', chat_content).pop();
             expect(time.textContent).toEqual("Tuesday Jan 2nd 2018");
 
-            day = sizzle('.date-separator:nth(1)', chat_content).pop();
+            day = sizzle('.date-separator:nth(2)', chat_content).pop();
             expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-02T00:00:00').toISOString());
-            expect(day.previousElementSibling.querySelector('.chat-msg__text').textContent).toBe('An earlier message on the next day');
+            expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('An earlier message on the next day');
 
             el = sizzle('.chat-msg:eq(3)', chat_content).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(2)', chat_content).pop();
+            el = sizzle('.chat-msg:eq(4)', chat_content).pop();
             expect(el.querySelector('.chat-msg__text').textContent).toEqual('message');
-            expect(el.previousElementSibling.querySelector('.chat-msg__text').textContent).toEqual('newer message from the next day');
+            expect(el.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:first', chat_content).pop();
+            day = sizzle('.date-separator:last', chat_content).pop();
             expect(day.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString());
-            expect(day.previousElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message');
+            expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message');
             expect(u.hasClass('chat-msg--followup', el)).toBe(false);
             done();
         }));
@@ -800,12 +800,12 @@
             expect(chat_content.querySelectorAll('time.separator-text').length).toEqual(2); // There are now two time elements
 
             const message_date = new Date();
-            day = sizzle('.date-separator:first', chat_content);
+            day = sizzle('.date-separator:last', chat_content);
             expect(day.length).toEqual(1);
             expect(day[0].getAttribute('class')).toEqual('message date-separator');
             expect(day[0].getAttribute('data-isodate')).toEqual(dayjs(message_date).startOf('day').toISOString());
 
-            time = sizzle('time.separator-text:first', chat_content).pop();
+            time = sizzle('time.separator-text:last', chat_content).pop();
             expect(time.textContent).toEqual(dayjs(message_date).startOf('day').format("dddd MMM Do YYYY"));
 
             // Normal checks for the 2nd message
@@ -815,12 +815,12 @@
             expect(msg_obj.get('fullname')).toBeUndefined();
             expect(msg_obj.get('sender')).toEqual('them');
             expect(msg_obj.get('is_delayed')).toEqual(false);
-            const msg_txt = sizzle('.chat-msg:first .chat-msg__text', chat_content).pop().textContent;
+            const msg_txt = sizzle('.chat-msg:last .chat-msg__text', chat_content).pop().textContent;
             expect(msg_txt).toEqual(message);
 
-            expect(chat_content.querySelector('.chat-msg:first-child .chat-msg__text').textContent).toEqual(message);
-            expect(chat_content.querySelector('.chat-msg:first-child .chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
-            expect(chat_content.querySelector('.chat-msg:first-child .chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
+            expect(chat_content.querySelector('.chat-msg:last-child .chat-msg__text').textContent).toEqual(message);
+            expect(chat_content.querySelector('.chat-msg:last-child .chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
+            expect(chat_content.querySelector('.chat-msg:last-child .chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
             done();
         }));
 
@@ -911,21 +911,21 @@
             message = 'http://www.opkode.com/"onmouseover="alert(1)"whatever';
             await test_utils.sendMessage(view, message);
 
-            msg = sizzle('.chat-content .chat-msg:first .chat-msg__text', view.el).pop();
+            msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
             expect(msg.textContent).toEqual(message);
             expect(msg.innerHTML).toEqual('<a target="_blank" rel="noopener" href="http://www.opkode.com/%22onmouseover=%22alert%281%29%22whatever">http://www.opkode.com/"onmouseover="alert(1)"whatever</a>');
 
             message = "https://en.wikipedia.org/wiki/Ender's_Game";
             await test_utils.sendMessage(view, message);
 
-            msg = sizzle('.chat-content .chat-msg:first .chat-msg__text', view.el).pop();
+            msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
             expect(msg.textContent).toEqual(message);
             expect(msg.innerHTML).toEqual('<a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Ender%27s_Game">'+message+'</a>');
 
             message = "<https://bugs.documentfoundation.org/show_bug.cgi?id=123737>";
             await test_utils.sendMessage(view, message);
 
-            msg = sizzle('.chat-content .chat-msg:first .chat-msg__text', view.el).pop();
+            msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
             expect(msg.textContent).toEqual(message);
             expect(msg.innerHTML).toEqual(
                 `&lt;<a target="_blank" rel="noopener" href="https://bugs.documentfoundation.org/show_bug.cgi?id=123737">https://bugs.documentfoundation.org/show_bug.cgi?id=123737</a>&gt;`);
@@ -933,7 +933,7 @@
             message = '<http://www.opkode.com/"onmouseover="alert(1)"whatever>';
             await test_utils.sendMessage(view, message);
 
-            msg = sizzle('.chat-content .chat-msg:first .chat-msg__text', view.el).pop();
+            msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
             expect(msg.textContent).toEqual(message);
             expect(msg.innerHTML).toEqual(
                 '&lt;<a target="_blank" rel="noopener" href="http://www.opkode.com/%22onmouseover=%22alert%281%29%22whatever">http://www.opkode.com/"onmouseover="alert(1)"whatever</a>&gt;');
@@ -941,7 +941,7 @@
             message = `https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2`
             await test_utils.sendMessage(view, message);
 
-            msg = sizzle('.chat-content .chat-msg:first .chat-msg__text', view.el).pop();
+            msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
             expect(msg.textContent).toEqual(message);
             expect(msg.innerHTML).toEqual(
                 `<a target="_blank" rel="noopener" href="https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=%213m6%211e1%213m4%211sQ7SdHo_bPLPlLlU8GSGWaQ%212e0%217i13312%218i6656%214m5%213m4%211s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08%218m2%213d52.3773668%214d4.5489388%215m1%211e2">https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2</a>`);
@@ -998,7 +998,7 @@
                 </message>`);
             _converse.connection._dataRecv(test_utils.createRequest(stanza));
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
-            expect(chat_content.querySelector('.message:first-child .chat-msg__text').innerHTML).toBe('Hey<br><br>Have you heard the news?');
+            expect(chat_content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('Hey<br><br>Have you heard the news?');
             stanza = u.toStanza(`
                 <message from="${contact_jid}"
                          type="chat"
@@ -1007,7 +1007,7 @@
                 </message>`);
             _converse.connection._dataRecv(test_utils.createRequest(stanza));
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
-            expect(chat_content.querySelector('.message:first-child .chat-msg__text').innerHTML).toBe('Hey<br>Have you heard<br>the news?');
+            expect(chat_content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('Hey<br>Have you heard<br>the news?');
             done();
         }));
 
@@ -1026,7 +1026,7 @@
             test_utils.sendMessage(view, message);
             await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length, 1000)
             expect(view.model.sendMessage).toHaveBeenCalled();
-            let msg = sizzle('.chat-content .chat-msg:first .chat-msg__text').pop();
+            let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
             expect(msg.innerHTML.trim()).toEqual(
                 '<!-- src/templates/image.html -->\n'+
                 '<a href="'+base_url+'/logo/conversejs-filled.svg" target="_blank" rel="noopener"><img class="chat-image img-thumbnail"'+
@@ -1035,7 +1035,7 @@
             test_utils.sendMessage(view, message);
             await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 2, 1000);
             expect(view.model.sendMessage).toHaveBeenCalled();
-            msg = sizzle('.chat-content .chat-msg:first .chat-msg__text').pop();
+            msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
             expect(msg.innerHTML.trim()).toEqual(
                 '<!-- src/templates/image.html -->\n'+
                 '<a href="'+base_url+'/logo/conversejs-filled.svg?param1=val1&amp;param2=val2" target="_blank" rel="noopener"><img'+
@@ -1046,7 +1046,7 @@
             test_utils.sendMessage(view, message);
             await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 4, 1000);
             expect(view.model.sendMessage).toHaveBeenCalled();
-            msg = sizzle('.chat-content .chat-msg:first .chat-msg__text').pop();
+            msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
             expect(msg.textContent.trim()).toEqual('hello world');
             expect(msg.querySelectorAll('img').length).toEqual(2);
 
@@ -1076,10 +1076,10 @@
             expect(chatbox.messages.models.length, 1);
             const msg_object = chatbox.messages.models[0];
 
-            const msg_author = view.el.querySelector('.chat-content .chat-msg:first-child .chat-msg__author');
+            const msg_author = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__author');
             expect(msg_author.textContent.trim()).toBe('Romeo Montague');
 
-            const msg_time = view.el.querySelector('.chat-content .chat-msg:first-child .chat-msg__time');
+            const msg_time = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__time');
             const time = dayjs(msg_object.get('time')).format(_converse.time_format);
             expect(msg_time.textContent).toBe(time);
             done();
@@ -1165,19 +1165,19 @@
 
             expect(chat_content.querySelectorAll('.message').length).toBe(6);
             expect(chat_content.querySelectorAll('.chat-msg').length).toBe(5);
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(5)'))).toBe(false);
-            expect(chat_content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe("A message");
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(4)'))).toBe(true);
-            expect(chat_content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe(
-                "Another message 3 minutes later");
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(3)'))).toBe(false);
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(2)'))).toBe(false);
+            expect(chat_content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message");
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(3)'))).toBe(true);
             expect(chat_content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe(
+                "Another message 3 minutes later");
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(4)'))).toBe(false);
+            expect(chat_content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe(
                 "Another message 14 minutes since we started");
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(2)'))).toBe(true);
-            expect(chat_content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe(
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(5)'))).toBe(true);
+            expect(chat_content.querySelector('.message: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', chat_content.querySelector('.message:nth-child(1)'))).toBe(false);
-            expect(chat_content.querySelector('.message:nth-child(1) .chat-msg__text').textContent).toBe(
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(6)'))).toBe(false);
+            expect(chat_content.querySelector('.message: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
@@ -1197,21 +1197,21 @@
 
             expect(chat_content.querySelectorAll('.message').length).toBe(7);
             expect(chat_content.querySelectorAll('.chat-msg').length).toBe(6);
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(6)'))).toBe(false);
-            expect(chat_content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe("A message");
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(5)'))).toBe(true);
-            expect(chat_content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe(
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(2)'))).toBe(false);
+            expect(chat_content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message");
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(3)'))).toBe(true);
+            expect(chat_content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe(
                 "Another message 3 minutes later");
             expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(4)'))).toBe(true);
             expect(chat_content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe(
                 "A delayed message, sent 5 minutes since we started");
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(3)'))).toBe(true);
-            expect(chat_content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe(
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(5)'))).toBe(true);
+            expect(chat_content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe(
                 "Another message 14 minutes since we started");
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(2)'))).toBe(true);
-            expect(chat_content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe(
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(6)'))).toBe(true);
+            expect(chat_content.querySelector('.message: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', chat_content.querySelector('.message:nth-child(1)'))).toBe(false);
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(7)'))).toBe(false);
 
             _converse.chatboxes.onMessage($msg({'id': 'aeb213', 'to': _converse.bare_jid})
                 .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
@@ -1227,25 +1227,25 @@
 
             expect(chat_content.querySelectorAll('.message').length).toBe(8);
             expect(chat_content.querySelectorAll('.chat-msg').length).toBe(7);
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(7)'))).toBe(false);
-            expect(chat_content.querySelector('.message:nth-child(7) .chat-msg__text').textContent).toBe("A message");
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(6)'))).toBe(true);
-            expect(chat_content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe(
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(2)'))).toBe(false);
+            expect(chat_content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message");
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(3)'))).toBe(true);
+            expect(chat_content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe(
                 "Another message 3 minutes later");
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(5)'))).toBe(false);
-            expect(chat_content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe(
-                "A carbon message 4 minutes later");
             expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(4)'))).toBe(false);
             expect(chat_content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe(
+                "A carbon message 4 minutes later");
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(5)'))).toBe(false);
+            expect(chat_content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe(
                 "A delayed message, sent 5 minutes since we started");
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(3)'))).toBe(true);
-            expect(chat_content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe(
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(6)'))).toBe(true);
+            expect(chat_content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe(
                 "Another message 14 minutes since we started");
-            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(2)'))).toBe(true);
-            expect(chat_content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe(
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(7)'))).toBe(true);
+            expect(chat_content.querySelector('.message: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', chat_content.querySelector('.message:nth-child(1)'))).toBe(false);
-            expect(chat_content.querySelector('.message:nth-child(1) .chat-msg__text').textContent).toBe(
+            expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(8)'))).toBe(false);
+            expect(chat_content.querySelector('.message:nth-child(8) .chat-msg__text').textContent).toBe(
                 "Another message within 10 minutes, but from a different person");
 
             jasmine.clock().uninstall();
@@ -1683,7 +1683,7 @@
                     });
                     view.model.sendMessage(msg_text);
                     await new Promise((resolve, reject) => view.once('messageInserted', resolve));
-                    msg_txt = sizzle('.chat-msg:first .chat-msg__text', chat_content).pop().textContent;
+                    msg_txt = sizzle('.chat-msg:last .chat-msg__text', chat_content).pop().textContent;
                     expect(msg_txt).toEqual(msg_text);
 
                     /* <message xmlns="jabber:client"
@@ -1751,7 +1751,7 @@
                     });
                     view.model.sendMessage(msg_text);
                     await new Promise((resolve, reject) => view.once('messageInserted', resolve));
-                    msg_txt = sizzle('.chat-msg:first .chat-msg__text', chat_content).pop().textContent;
+                    msg_txt = sizzle('.chat-msg:last .chat-msg__text', chat_content).pop().textContent;
                     expect(msg_txt).toEqual(msg_text);
 
                     // A different error message will however render
@@ -1831,20 +1831,14 @@
                         .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
                     promises.push(new Promise((resolve, reject) => view.once('messageInserted', resolve)));
                 }
-
                 await Promise.all(promises);
                 // XXX Fails on Travis
                 // await test_utils.waitUntil(() => view.content.scrollTop, 1000)
                 await test_utils.waitUntil(() => !view.model.get('auto_scrolled'), 500);
                 view.content.scrollTop = 0;
                 // XXX Fails on Travis
-                // await test_utils.waitUntil(() => view.scrolled, 900);
-                view.scrolled = true;
-
-                const text = Array.from(view.el.querySelectorAll('.chat-content .chat-msg__text'))
-                    .map(e => e.textContent)
-                    .join(' ');
-                expect(text).toBe(_.range(20).reverse().map(n => `Message: ${n}`).join(' '));
+                // await test_utils.waitUntil(() => view.model.get('scrolled'), 900);
+                view.model.set('scrolled', true);
 
                 const message = 'This message is received while the chat area is scrolled up';
                 _converse.chatboxes.onMessage($msg({
@@ -1858,10 +1852,10 @@
                 await test_utils.waitUntil(() => view.model.messages.length > 20, 1000);
                 // Now check that the message appears inside the chatbox in the DOM
                 const chat_content = view.el.querySelector('.chat-content');
-                const  msg_txt = sizzle('.chat-content .chat-msg:first .chat-msg__text', view.el).pop().textContent;
+                const  msg_txt = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop().textContent;
                 expect(msg_txt).toEqual(message);
                 await test_utils.waitUntil(() => u.isVisible(view.el.querySelector('.new-msgs-indicator')), 900);
-                expect(view.scrolled).toBe(true);
+                expect(view.model.get('scrolled')).toBe(true);
                 expect(view.content.scrollTop).toBe(0);
                 expect(u.isVisible(view.el.querySelector('.new-msgs-indicator'))).toBeTruthy();
                 // Scroll down again
@@ -1962,9 +1956,9 @@
                     </message>`);
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
-                msg = view.el.querySelector('.chat-msg:first-child .chat-msg__text');
+                msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text');
                 expect(msg.innerHTML).toEqual('<!-- message gets added here via renderMessage -->'); // Emtpy
-                media = view.el.querySelector('.chat-msg:first-child .chat-msg__media');
+                media = view.el.querySelector('.chat-msg:last-child .chat-msg__media');
                 expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual(
                     '<!-- src/templates/audio.html -->'+
                     '<audio controls="" src="https://montague.lit/audio.mp3"></audio>'+
@@ -2012,9 +2006,9 @@
                     </message>`);
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
-                msg = view.el.querySelector('.chat-msg:first-child .chat-msg__text');
+                msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text');
                 expect(msg.innerHTML).toEqual('<!-- message gets added here via renderMessage -->'); // Emtpy
-                media = view.el.querySelector('.chat-msg:first-child .chat-msg__media');
+                media = view.el.querySelector('.chat-msg:last-child .chat-msg__media');
                 expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual(
                     '<!-- src/templates/video.html -->'+
                     '<video controls="" src="https://montague.lit/video.mp4" style="max-height: 50vh"></video>'+
@@ -2355,7 +2349,7 @@
             expect(view.model.messages.last().get('affiliation')).toBe('member');
             expect(view.model.messages.last().get('role')).toBe('participant');
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
-            expect(sizzle('.chat-msg__author:first', view.el).pop().classList.value.trim()).toBe('chat-msg__author participant');
+            expect(sizzle('.chat-msg__author', view.el).pop().classList.value.trim()).toBe('chat-msg__author participant');
 
             presence = $pres({
                     to:'romeo@montague.lit/orchard',
@@ -2561,7 +2555,7 @@
             expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
             expect(view.model.messages.at(0).get('correcting')).toBe(true);
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
-            await test_utils.waitUntil(() => u.hasClass('correcting', sizzle('.chat-msg.groupchat:last', view.el).pop()), 500);
+            await test_utils.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
             expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
             view.onKeyDown({
                 target: textarea,

+ 103 - 107
spec/muc.js

@@ -375,8 +375,8 @@
                 await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2);
 
                 const info_texts = Array.from(view.el.querySelectorAll('.chat-content .chat-info')).map(e => e.textContent);
-                expect(info_texts[1]).toBe('A new groupchat has been created');
-                expect(info_texts[0]).toBe('nicky has entered the groupchat');
+                expect(info_texts[0]).toBe('A new groupchat has been created');
+                expect(info_texts[1]).toBe('nicky has entered the groupchat');
 
                 // An instant room is created by saving the default configuratoin.
                 //
@@ -482,9 +482,9 @@
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
 
                 await test_utils.waitUntil(() => chat_content.querySelectorAll('.chat-info').length === 2);
-                expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
-                    .toBe("This groupchat is not anonymous");
                 expect(sizzle('div.chat-info:first', chat_content).pop().textContent)
+                    .toBe("This groupchat is not anonymous");
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                     .toBe("some1 has entered the groupchat");
                 done();
             }));
@@ -495,7 +495,7 @@
                     null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
 
-                await test_utils.openAndEnterChatRoom(_converse, "coven", 'chat.shakespeare.lit', 'some1');
+                await test_utils.openChatRoom(_converse, "coven", 'chat.shakespeare.lit', 'some1');
                 const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
                 const chat_content = view.el.querySelector('.chat-content');
                 /* We don't show join/leave messages for existing occupants. We
@@ -512,10 +512,7 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                const info_msgs = sizzle('.chat-info', chat_content);
-                expect(info_msgs.length).toBe(2);
-                expect(info_msgs.pop().textContent).toBe('some1 has entered the groupchat');
-                expect(info_msgs.pop().textContent).toBe('oldguy has entered the groupchat');
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(0);
 
                 /* <presence to="romeo@montague.lit/_converse.js-29092160"
                  *           from="coven@chat.shakespeare.lit/some1">
@@ -536,7 +533,7 @@
                     }).up()
                     .c('status', {code: '110'});
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
+                expect(sizzle('div.chat-info:first', chat_content).pop().textContent)
                     .toBe("some1 has entered the groupchat");
 
                 presence = $pres({
@@ -550,8 +547,8 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(3);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent)
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(2);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                     .toBe("newguy has entered the groupchat");
 
                 const msg = $msg({
@@ -576,8 +573,8 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(4);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent)
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(3);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                     .toBe("newgirl has entered the groupchat");
 
                 // Don't show duplicate join messages
@@ -591,7 +588,7 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(4);
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(3);
 
                 /*  <presence
                  *      from='coven@chat.shakespeare.lit/thirdwitch'
@@ -618,8 +615,8 @@
                             'role': 'none'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(5);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe(
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(4);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
                     'newguy has left the groupchat. '+
                     '"Disconnected: Replaced by new connection"');
 
@@ -635,9 +632,8 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(5);
-                let msg_el = sizzle('div.chat-info:first', chat_content).pop();
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(4);
+                let msg_el = sizzle('div.chat-info:last', chat_content).pop();
                 expect(msg_el.textContent).toBe("newguy has left and re-entered the groupchat");
                 expect(msg_el.getAttribute('data-leavejoin')).toBe('newguy');
 
@@ -653,8 +649,8 @@
                             'role': 'none'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(5);
-                msg_el = sizzle('.chat-info:first', chat_content).pop();
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(4);
+                msg_el = sizzle('div.chat-info', chat_content).pop();
                 expect(msg_el.textContent).toBe('newguy has left the groupchat');
                 expect(msg_el.getAttribute('data-leave')).toBe('newguy');
 
@@ -669,8 +665,8 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('.chat-info').length).toBe(6);
-                expect(sizzle('.chat-info:first', chat_content).pop().textContent)
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(5);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                     .toBe("nomorenicks has entered the groupchat");
 
                 presence = $pres({
@@ -684,8 +680,8 @@
                         'role': 'none'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(6);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent)
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(5);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                     .toBe("nomorenicks has entered and left the groupchat");
 
                 presence = $pres({
@@ -699,8 +695,8 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(6);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent)
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(5);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                     .toBe("nomorenicks has entered the groupchat");
 
                 // Test a member joining and leaving
@@ -714,7 +710,7 @@
                         'role': 'participant'
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(7);
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(6);
 
                 /*  <presence
                  *      from='coven@chat.shakespeare.lit/thirdwitch'
@@ -741,8 +737,8 @@
                             'role': 'none'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(7);
-                expect(sizzle('.chat-info:first', chat_content).pop().textContent).toBe(
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(6);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
                     'insider has entered and left the groupchat. '+
                     '"Disconnected: Replaced by new connection"');
 
@@ -763,8 +759,8 @@
                     });
 
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(chat_content.querySelectorAll('.chat-info').length).toBe(7);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe("newgirl has entered and left the groupchat");
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(6);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("newgirl has entered and left the groupchat");
                 expect(view.model.occupants.length).toBe(4);
                 done();
             }));
@@ -790,7 +786,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(2);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe("fabio has entered the groupchat");
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("fabio has entered the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/Dele Olajide">
@@ -800,7 +796,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(3);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe("Dele Olajide has entered the groupchat");
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("Dele Olajide has entered the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/jcbrand">
@@ -811,7 +807,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(4);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe("jcbrand has entered the groupchat");
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("jcbrand has entered the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/Dele Olajide">
@@ -821,7 +817,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(4);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe("Dele Olajide has entered and left the groupchat");
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("Dele Olajide has entered and left the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/Dele Olajide">
@@ -831,7 +827,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(4);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe("Dele Olajide has entered the groupchat");
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("Dele Olajide has entered the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/fuvuv" xml:lang="en">
@@ -843,7 +839,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(5);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe("fuvuv has entered the groupchat");
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("fuvuv has entered the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/fuvuv">
@@ -853,7 +849,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(5);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe("fuvuv has entered and left the groupchat");
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("fuvuv has entered and left the groupchat");
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/fabio">
@@ -864,7 +860,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(5);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe(
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
                     `fabio has entered and left the groupchat. "Disconnected: Replaced by new connection"`);
 
                 presence = u.toStanza(
@@ -877,7 +873,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(5);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe(
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
                     `fabio has entered the groupchat. "Ready for a new day"`);
 
                 // XXX: hack so that we can test leave/enter of occupants
@@ -904,7 +900,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(2);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe(
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
                     `Dele Olajide has left the groupchat`);
 
                 presence = u.toStanza(
@@ -916,7 +912,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(sizzle('div.chat-info', chat_content).length).toBe(2);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe(
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
                     `fabio has left and re-entered the groupchat`);
                 done();
             }));
@@ -942,7 +938,7 @@
                     });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(chat_content.querySelectorAll('div.chat-info').length).toBe(2);
-                expect(sizzle('.chat-info:first', chat_content).pop().textContent).toBe('newguy has entered the groupchat');
+                expect(sizzle('div.chat-info', chat_content).pop().textContent).toBe('newguy has entered the groupchat');
 
                 presence = $pres({
                     to: 'romeo@montague.lit/orchard',
@@ -958,7 +954,7 @@
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(chat_content.querySelectorAll('div.chat-info').length).toBe(2);
-                expect(sizzle('.chat-info:first', chat_content).pop().textContent).toBe('newguy has entered and left the groupchat');
+                expect(sizzle('div.chat-info', chat_content).pop().textContent).toBe('newguy has entered and left the groupchat');
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/fabio">
@@ -970,7 +966,7 @@
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
 
-                expect(sizzle('.chat-info:first', chat_content).pop().textContent).toBe(`fabio has entered the groupchat`);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(`fabio has entered the groupchat`);
 
                 presence = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/Dele Olajide">
@@ -979,8 +975,8 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('.chat-info', chat_content).length).toBe(4);
-                expect(sizzle('.chat-info:first', chat_content).pop().textContent).toBe("Dele Olajide has entered the groupchat");
+                expect(sizzle('div.chat-info', chat_content).length).toBe(4);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("Dele Olajide has entered the groupchat");
                 await test_utils.sendMessage(view, 'hello world');
 
                 presence = u.toStanza(
@@ -991,8 +987,8 @@
                         </x>
                     </presence>`);
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(sizzle('.chat-info', chat_content).length).toBe(5);
-                expect(sizzle('.chat-info:first', chat_content).pop().textContent).toBe(`Dele Olajide has left the groupchat`);
+                expect(sizzle('div.chat-info', chat_content).length).toBe(5);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(`Dele Olajide has left the groupchat`);
                 done();
             }));
 
@@ -1049,11 +1045,11 @@
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
                 const chat_content = view.el.querySelector('.chat-content');
-                const messages = chat_content.querySelectorAll('.chat-info');
+                const messages = chat_content.querySelectorAll('div.chat-info');
                 expect(messages.length).toBe(3);
-                expect(messages[2].textContent).toBe('romeo has entered the groupchat');
+                expect(messages[0].textContent).toBe('romeo has entered the groupchat');
                 expect(messages[1].textContent).toBe('Guus has entered the groupchat');
-                expect(messages[0].textContent).toBe('Guus has left and re-entered the groupchat');
+                expect(messages[2].textContent).toBe('Guus has left and re-entered the groupchat');
                 done();
             }));
 
@@ -1106,8 +1102,8 @@
                 expect(indicator.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString());
                 expect(indicator.querySelector('time').getAttribute('class')).toEqual('separator-text');
                 expect(indicator.querySelector('time').textContent).toEqual(dayjs().startOf('day').format("dddd MMM Do YYYY"));
-                expect(chat_content.querySelectorAll('.chat-info').length).toBe(2);
-                expect(chat_content.querySelector('.chat-info:first-child').textContent).toBe(
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(2);
+                expect(chat_content.querySelector('div.chat-info:last-child').textContent).toBe(
                     "some1 has entered the groupchat"
                 );
 
@@ -1135,8 +1131,8 @@
                 expect(indicator.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString());
 
                 expect(indicator.querySelector('time').textContent).toEqual(dayjs().startOf('day').format("dddd MMM Do YYYY"));
-                expect(chat_content.querySelectorAll('.chat-info').length).toBe(3);
-                expect(sizzle('div.chat-info:first', chat_content).pop().textContent).toBe(
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(3);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
                     'some1 has left the groupchat. '+
                     '"Disconnected: Replaced by new connection"');
 
@@ -1167,12 +1163,12 @@
                 let time = chat_content.querySelectorAll('time.separator-text');
                 expect(time.length).toEqual(4);
 
-                indicator = sizzle('.date-separator:eq(0)', chat_content).pop();
+                indicator = sizzle('.date-separator:eq(3)', chat_content).pop();
                 expect(indicator.getAttribute('class')).toEqual('message date-separator');
                 expect(indicator.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString());
                 expect(indicator.querySelector('time').textContent).toEqual(dayjs().startOf('day').format("dddd MMM Do YYYY"));
-                expect(chat_content.querySelectorAll('.chat-info').length).toBe(4);
-                expect(sizzle('.chat-info:first', chat_content).pop().textContent)
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(4);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                     .toBe("newguy has entered the groupchat");
 
                 jasmine.clock().tick(ONE_DAY_LATER);
@@ -1207,12 +1203,12 @@
                 time = chat_content.querySelectorAll('time.separator-text');
                 expect(time.length).toEqual(6);
 
-                indicator = sizzle('.date-separator:eq(0)', chat_content).pop();
+                indicator = sizzle('.date-separator:eq(5)', chat_content).pop();
                 expect(indicator.getAttribute('class')).toEqual('message date-separator');
                 expect(indicator.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString());
                 expect(indicator.querySelector('time').textContent).toEqual(dayjs().startOf('day').format("dddd MMM Do YYYY"));
-                expect(chat_content.querySelectorAll('.chat-info').length).toBe(5);
-                expect(sizzle('.chat-info:first', chat_content).pop().textContent).toBe(
+                expect(chat_content.querySelectorAll('div.chat-info').length).toBe(5);
+                expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
                     'newguy has left the groupchat. '+
                     '"Disconnected: Replaced by new connection"');
                 jasmine.clock().uninstall();
@@ -1255,8 +1251,8 @@
                 }).c('body').t(message).tree();
                 await view.model.onMessage(msg);
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
-                expect(_.includes(sizzle('.chat-msg__author:first', view.el).pop().textContent, '**Romeo Montague')).toBeTruthy();
-                expect(sizzle('.chat-msg__text:first', view.el).pop().textContent).toBe('is as well');
+                expect(_.includes(sizzle('.chat-msg__author:last', view.el).pop().textContent, '**Romeo Montague')).toBeTruthy();
+                expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe('is as well');
                 done();
             }));
 
@@ -1803,7 +1799,7 @@
 
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2);
-                const info_text = sizzle('.chat-content .chat-info:last', view.el).pop().textContent;
+                const info_text = sizzle('.chat-content .chat-info:first', view.el).pop().textContent;
                 expect(info_text).toBe('Your nickname has been automatically set to thirdwitch');
                 done();
             }));
@@ -2021,7 +2017,7 @@
 
                     // Now check that the message appears inside the chatbox in the DOM
                     const chat_content = view.el.querySelector('.chat-content');
-                    const msg_txt = sizzle('.chat-msg:first .chat-msg__text', chat_content).pop().textContent;
+                    const msg_txt = sizzle('.chat-msg:last .chat-msg__text', chat_content).pop().textContent;
                     expect(msg_txt).toEqual(message);
                     expect(view.content.scrollTop).toBe(0);
                     done();
@@ -2159,8 +2155,8 @@
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
                 await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2);
                 const info_messages = view.el.querySelectorAll('.chat-content .chat-info');
-                expect(info_messages[1].textContent).toBe('romeo has entered the groupchat');
-                expect(info_messages[0].textContent).toBe('groupchat logging is now enabled');
+                expect(info_messages[0].textContent).toBe('romeo has entered the groupchat');
+                expect(info_messages[1].textContent).toBe('groupchat logging is now enabled');
                 done();
             }));
 
@@ -2238,7 +2234,7 @@
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2);
 
-                expect(sizzle('.chat-info:first').pop().textContent).toBe(
+                expect(sizzle('div.chat-info:last').pop().textContent).toBe(
                     __(_converse.muc.new_nickname_messages["303"], "newnick")
                 );
                 expect(view.model.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED);
@@ -2581,7 +2577,7 @@
                 _converse.connection._dataRecv(test_utils.createRequest(message));
                 await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 3);
                 const chat_body = view.el.querySelector('.chatroom-body');
-                expect(sizzle('.message:first', chat_body).pop().textContent)
+                expect(sizzle('.message:last', chat_body).pop().textContent)
                     .toBe('This groupchat is now no longer anonymous');
                 done();
             }));
@@ -3181,7 +3177,7 @@
                 });
                 expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
                 expect(view.showErrorMessage).toHaveBeenCalled();
-                expect(view.el.querySelector('.message:first-child').textContent).toBe(
+                expect(view.el.querySelector('.message:last-child').textContent).toBe(
                     "Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.");
 
                 expect(view.model.setAffiliation).not.toHaveBeenCalled();
@@ -3235,7 +3231,7 @@
 
                 textarea.value = '/ban joe22';
                 view.onFormSubmitted(new Event('submit'));
-                expect(view.el.querySelector('.message:first-child').textContent).toBe(
+                expect(view.el.querySelector('.message:last-child').textContent).toBe(
                     "Error: couldn't find a groupchat participant based on your arguments");
                 done();
             }));
@@ -3326,7 +3322,7 @@
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
 
                 await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 4);
-                expect(view.el.querySelectorAll('.chat-info')[0].textContent).toBe("annoying guy has been kicked out");
+                expect(view.el.querySelectorAll('.chat-info')[3].textContent).toBe("annoying guy has been kicked out");
                 expect(view.el.querySelectorAll('.chat-info').length).toBe(4);
                 done();
             }));
@@ -3372,10 +3368,10 @@
                             'role': 'participant'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                const info_msg = view.el.querySelector('.chat-info:first-child');
-                expect(info_msg.textContent).toBe("trustworthyguy has entered the groupchat");
+                var info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
+                expect(info_msgs.pop().textContent).toBe("trustworthyguy has entered the groupchat");
 
-                const textarea = view.el.querySelector('.chat-textarea')
+                var textarea = view.el.querySelector('.chat-textarea')
                 textarea.value = '/op';
                 view.onKeyDown({
                     target: textarea,
@@ -3428,7 +3424,7 @@
                             'role': 'moderator'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                let info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
+                info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
                 expect(info_msgs.pop().textContent).toBe("trustworthyguy is now a moderator");
                 // Call now with the correct amount of arguments.
                 // XXX: Calling onFormSubmitted directly, trying
@@ -3502,7 +3498,7 @@
                  * </x>
                  * </presence>
                  */
-                let presence = $pres({
+                var presence = $pres({
                         'from': 'lounge@montague.lit/annoyingGuy',
                         'id':'27C55F89-1C6A-459A-9EB5-77690145D624',
                         'to': 'romeo@montague.lit/desktop'
@@ -3514,8 +3510,8 @@
                             'role': 'participant'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                const info_msg = sizzle('.chat-info:first', view.el).pop();
-                expect(info_msg.textContent).toBe("annoyingGuy has entered the groupchat");
+                var info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
+                expect(info_msgs.pop().textContent).toBe("annoyingGuy has entered the groupchat");
 
                 const textarea = view.el.querySelector('.chat-textarea')
                 textarea.value = '/mute';
@@ -3569,7 +3565,7 @@
                             'role': 'visitor'
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                let info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
+                info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
                 expect(info_msgs.pop().textContent).toBe("annoyingGuy has been muted");
 
                 // Call now with the correct of arguments.
@@ -4707,7 +4703,7 @@
                         });
                     _converse.connection._dataRecv(test_utils.createRequest(presence));
                     expect(chat_content.querySelectorAll('div.chat-info').length).toBe(2);
-                    expect(sizzle('.chat-info:first', chat_content).pop().textContent)
+                    expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                         .toBe("newguy has entered the groupchat");
 
                     presence = $pres({
@@ -4722,7 +4718,7 @@
                         });
                     _converse.connection._dataRecv(test_utils.createRequest(presence));
                     expect(chat_content.querySelectorAll('div.chat-info').length).toBe(3);
-                    expect(sizzle('.chat-info:first', chat_content).pop().textContent)
+                    expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                         .toBe("nomorenicks has entered the groupchat");
 
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
@@ -4741,9 +4737,9 @@
                     // Check that the notification appears inside the chatbox in the DOM
                     let events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
-                    expect(events[2].textContent).toEqual('some1 has entered the groupchat');
+                    expect(events[0].textContent).toEqual('some1 has entered the groupchat');
                     expect(events[1].textContent).toEqual('newguy has entered the groupchat');
-                    expect(events[0].textContent).toEqual('nomorenicks has entered the groupchat');
+                    expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
 
                     let notifications = view.el.querySelectorAll('.chat-state-notification');
                     expect(notifications.length).toBe(1);
@@ -4763,9 +4759,9 @@
 
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
-                    expect(events[2].textContent).toEqual('some1 has entered the groupchat');
+                    expect(events[0].textContent).toEqual('some1 has entered the groupchat');
                     expect(events[1].textContent).toEqual('newguy has entered the groupchat');
-                    expect(events[0].textContent).toEqual('nomorenicks has entered the groupchat');
+                    expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
 
                     notifications = view.el.querySelectorAll('.chat-state-notification');
                     expect(notifications.length).toBe(1);
@@ -4782,15 +4778,15 @@
                     await view.model.onMessage(msg);
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
-                    expect(events[2].textContent).toEqual('some1 has entered the groupchat');
+                    expect(events[0].textContent).toEqual('some1 has entered the groupchat');
                     expect(events[1].textContent).toEqual('newguy has entered the groupchat');
-                    expect(events[0].textContent).toEqual('nomorenicks has entered the groupchat');
+                    expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
 
                     await test_utils.waitUntil(() => (view.el.querySelectorAll('.chat-state-notification').length === 2));
                     notifications = view.el.querySelectorAll('.chat-state-notification');
                     expect(notifications.length).toBe(2);
-                    expect(notifications[1].textContent).toEqual('nomorenicks is typing');
-                    expect(notifications[0].textContent).toEqual('newguy is typing');
+                    expect(notifications[0].textContent).toEqual('nomorenicks is typing');
+                    expect(notifications[1].textContent).toEqual('newguy is typing');
 
                     // Check that new messages appear under the chat state notifications
                     msg = $msg({
@@ -4811,9 +4807,9 @@
                     timeout_functions[0]();
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
-                    expect(events[2].textContent).toEqual('some1 has entered the groupchat');
+                    expect(events[0].textContent).toEqual('some1 has entered the groupchat');
                     expect(events[1].textContent).toEqual('newguy has entered the groupchat');
-                    expect(events[0].textContent).toEqual('nomorenicks has entered the groupchat');
+                    expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
 
                     notifications = view.el.querySelectorAll('.chat-state-notification');
                     expect(notifications.length).toBe(1);
@@ -4822,9 +4818,9 @@
                     timeout_functions[1]();
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
-                    expect(events[2].textContent).toEqual('some1 has entered the groupchat');
+                    expect(events[0].textContent).toEqual('some1 has entered the groupchat');
                     expect(events[1].textContent).toEqual('newguy has entered the groupchat');
-                    expect(events[0].textContent).toEqual('nomorenicks has entered the groupchat');
+                    expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
 
                     notifications = view.el.querySelectorAll('.chat-state-notification');
                     expect(notifications.length).toBe(0);
@@ -4877,7 +4873,7 @@
                         });
                     _converse.connection._dataRecv(test_utils.createRequest(presence));
                     expect(chat_content.querySelectorAll('div.chat-info').length).toBe(2);
-                    expect(sizzle('.chat-info:first', chat_content).pop().textContent)
+                    expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                         .toBe("newguy has entered the groupchat");
 
                     presence = $pres({
@@ -4892,7 +4888,7 @@
                         });
                     _converse.connection._dataRecv(test_utils.createRequest(presence));
                     expect(chat_content.querySelectorAll('div.chat-info').length).toBe(3);
-                    expect(sizzle('.chat-info:first', chat_content).pop().textContent)
+                    expect(sizzle('div.chat-info:last', chat_content).pop().textContent)
                         .toBe("nomorenicks has entered the groupchat");
 
                     // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
@@ -4909,9 +4905,9 @@
                     // Check that the notification appears inside the chatbox in the DOM
                     var events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
-                    expect(events[2].textContent).toEqual('some1 has entered the groupchat');
+                    expect(events[0].textContent).toEqual('some1 has entered the groupchat');
                     expect(events[1].textContent).toEqual('newguy has entered the groupchat');
-                    expect(events[0].textContent).toEqual('nomorenicks has entered the groupchat');
+                    expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
 
                     await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-state-notification').length);
                     let notifications = view.el.querySelectorAll('.chat-state-notification');
@@ -4929,9 +4925,9 @@
 
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
-                    expect(events[2].textContent).toEqual('some1 has entered the groupchat');
+                    expect(events[0].textContent).toEqual('some1 has entered the groupchat');
                     expect(events[1].textContent).toEqual('newguy has entered the groupchat');
-                    expect(events[0].textContent).toEqual('nomorenicks has entered the groupchat');
+                    expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
 
                     notifications = view.el.querySelectorAll('.chat-state-notification');
                     expect(notifications.length).toBe(1);
@@ -4947,9 +4943,9 @@
                     await view.model.onMessage(msg);
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
-                    expect(events[2].textContent).toEqual('some1 has entered the groupchat');
+                    expect(events[0].textContent).toEqual('some1 has entered the groupchat');
                     expect(events[1].textContent).toEqual('newguy has entered the groupchat');
-                    expect(events[0].textContent).toEqual('nomorenicks has entered the groupchat');
+                    expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
 
                     await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-state-notification').length ===  2);
                     notifications = view.el.querySelectorAll('.chat-state-notification');
@@ -4970,9 +4966,9 @@
                     await view.model.onMessage(msg);
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
-                    expect(events[2].textContent).toEqual('some1 has entered the groupchat');
+                    expect(events[0].textContent).toEqual('some1 has entered the groupchat');
                     expect(events[1].textContent).toEqual('newguy has entered the groupchat');
-                    expect(events[0].textContent).toEqual('nomorenicks has entered the groupchat');
+                    expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
 
                     await test_utils.waitUntil(() => {
                         return _.map(

+ 2 - 2
spec/omemo.js

@@ -208,7 +208,7 @@
             _converse.connection._dataRecv(test_utils.createRequest(stanza));
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
             expect(view.model.messages.length).toBe(2);
-            expect(view.el.querySelectorAll('.chat-msg__body')[0].textContent.trim())
+            expect(view.el.querySelectorAll('.chat-msg__body')[1].textContent.trim())
                 .toBe('This is an encrypted message from the contact');
 
             // #1193 Check for a received message without <body> tag
@@ -228,7 +228,7 @@
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
             await test_utils.waitUntil(() => view.model.messages.length > 1);
             expect(view.model.messages.length).toBe(3);
-            expect(view.el.querySelectorAll('.chat-msg__body')[0].textContent.trim())
+            expect(view.el.querySelectorAll('.chat-msg__body')[2].textContent.trim())
                 .toBe('Another received encrypted message without fallback');
             done();
         }));

+ 86 - 45
src/converse-chatview.js

@@ -345,6 +345,7 @@ converse.plugins.add('converse-chatview', {
             initialize () {
                 this.initDebounced();
                 this.model.messages.on('add', this.onMessageAdded, this);
+                this.model.messages.on('rendered', this.scrollDown, this);
                 this.model.messages.on('reset', () => {
                     this.content.innerHTML = '';
                     this.removeAll();
@@ -366,6 +367,7 @@ converse.plugins.add('converse-chatview', {
             },
 
             initDebounced () {
+                this.scrollDown = _.debounce(this._scrollDown, 100);
                 this.markScrolled = _.debounce(this._markScrolled, 100);
                 this.show = _.debounce(this._show, 500, {'leading': true});
             },
@@ -544,7 +546,8 @@ converse.plugins.add('converse-chatview', {
                 await this.model.messages.fetched;
                 await Promise.all(this.model.messages.map(m => this.onMessageAdded(m)));
                 this.insertIntoDOM();
-                this.content.addEventListener('scroll', () => this.markScrolled());
+                this.scrollDown();
+                this.content.addEventListener('scroll', this.markScrolled.bind(this));
             },
 
             insertIntoDOM () {
@@ -565,23 +568,26 @@ converse.plugins.add('converse-chatview', {
                         'message': message,
                         'isodate': isodate,
                     }));
-                this.insertDayIndicator(this.content.firstElementChild);
+                this.insertDayIndicator(this.content.lastElementChild);
+                this.scrollDown();
                 return isodate;
             },
 
             showErrorMessage (message) {
                 this.content.insertAdjacentHTML(
-                    'afterBegin',
+                    'beforeend',
                     tpl_error_message({'message': message, 'isodate': (new Date()).toISOString() })
                 );
+                this.scrollDown();
             },
 
             addSpinner (append=false) {
                 if (_.isNull(this.el.querySelector('.spinner'))) {
                     if (append) {
-                        this.content.insertAdjacentHTML('afterBegin', tpl_spinner());
+                        this.content.insertAdjacentHTML('beforeend', tpl_spinner());
+                        this.scrollDown();
                     } else {
-                        this.content.insertAdjacentHTML('beforeEnd', tpl_spinner());
+                        this.content.insertAdjacentHTML('afterbegin', tpl_spinner());
                     }
                 }
             },
@@ -602,16 +608,16 @@ converse.plugins.add('converse-chatview', {
              *      which specifies its creation date.
              */
             insertDayIndicator (next_msg_el) {
-                const earlier_msg_el = u.getNextElement(next_msg_el, ".message:not(.chat-state-notification)"),
-                      earlier_msg_date = _.isNull(earlier_msg_el) ? null : earlier_msg_el.getAttribute('data-isodate'),
+                const prev_msg_el = u.getPreviousElement(next_msg_el, ".message:not(.chat-state-notification)"),
+                      prev_msg_date = _.isNull(prev_msg_el) ? null : prev_msg_el.getAttribute('data-isodate'),
                       next_msg_date = next_msg_el.getAttribute('data-isodate');
 
-                if (_.isNull(earlier_msg_date) && _.isNull(next_msg_date)) {
+                if (_.isNull(prev_msg_date) && _.isNull(next_msg_date)) {
                     return;
                 }
-                if (_.isNull(earlier_msg_date) || dayjs(next_msg_date).isAfter(earlier_msg_date, 'day')) {
+                if (_.isNull(prev_msg_date) || dayjs(next_msg_date).isAfter(prev_msg_date, 'day')) {
                     const day_date = dayjs(next_msg_date).startOf('day');
-                    next_msg_el.insertAdjacentHTML('afterEnd',
+                    next_msg_el.insertAdjacentHTML('beforeBegin',
                         tpl_new_day({
                             'isodate': day_date.toISOString(),
                             'datestring': day_date.format("dddd MMM Do YYYY")
@@ -629,15 +635,14 @@ converse.plugins.add('converse-chatview', {
              * @returns { Date }
              */
             getLastMessageDate (cutoff) {
-                const most_recent_msg = u.getFirstChildElement(this.content, '.message:not(.chat-state-notification)');
-                const most_recent_date = most_recent_msg ? most_recent_msg.getAttribute('data-isodate') : null;
-                if (_.isNull(most_recent_date)) {
+                const first_msg = u.getFirstChildElement(this.content, '.message:not(.chat-state-notification)');
+                const oldest_date = first_msg ? first_msg.getAttribute('data-isodate') : null;
+                if (!_.isNull(oldest_date) && dayjs(oldest_date).isAfter(cutoff)) {
                     return null;
                 }
-
-                const oldest_message = u.getLastChildElement(this.content, '.message:not(.chat-state-notification)');
-                const oldest_date = oldest_message ? oldest_message.getAttribute('data-isodate') : null;
-                if (!_.isNull(oldest_date) && dayjs(oldest_date).isAfter(cutoff)) {
+                const last_msg = u.getLastChildElement(this.content, '.message:not(.chat-state-notification)');
+                const most_recent_date = last_msg ? last_msg.getAttribute('data-isodate') : null;
+                if (_.isNull(most_recent_date)) {
                     return null;
                 }
                 if (dayjs(most_recent_date).isBefore(cutoff)) {
@@ -664,6 +669,29 @@ converse.plugins.add('converse-chatview', {
                 }
             },
 
+            setScrollPosition (message_el) {
+                /* Given a newly inserted message, determine whether we
+                 * should keep the scrollbar in place (so as to not scroll
+                 * up when using infinite scroll).
+                 */
+                if (this.model.get('scrolled')) {
+                    const next_msg_el = u.getNextElement(message_el, ".chat-msg");
+                    if (next_msg_el) {
+                        // The currently received message is not new, there
+                        // are newer messages after it. So let's see if we
+                        // should maintain our current scroll position.
+                        if (this.content.scrollTop === 0 || this.model.get('top_visible_message')) {
+                            const top_visible_message = this.model.get('top_visible_message') || next_msg_el;
+
+                            this.model.set('top_visible_message', top_visible_message);
+                            this.content.scrollTop = top_visible_message.offsetTop - 30;
+                        }
+                    }
+                } else {
+                    this.scrollDown();
+                }
+            },
+
             showHelpMessages (msgs, type, spinner) {
                 msgs.forEach(msg => {
                     this.content.insertAdjacentHTML(
@@ -680,6 +708,7 @@ converse.plugins.add('converse-chatview', {
                 } else if (spinner === false) {
                     this.clearSpinner();
                 }
+                return this.scrollDown();
             },
 
             shouldShowOnTextMessage () {
@@ -697,24 +726,24 @@ converse.plugins.add('converse-chatview', {
                 if (view.model.get('type') === 'error') {
                     const previous_msg_el = this.content.querySelector(`[data-msgid="${view.model.get('msgid')}"]`);
                     if (previous_msg_el) {
-                        previous_msg_el.insertAdjacentElement('beforeBegin', view.el);
+                        previous_msg_el.insertAdjacentElement('afterend', view.el);
                         return this.trigger('messageInserted', view.el);
                     }
                 }
-                const current_msg_date = dayjs(view.model.get('time')).toDate() || new Date();
-                const previous_msg_date = this.getLastMessageDate(current_msg_date);
+                const current_msg_date = dayjs(view.model.get('time')).toDate() || new Date(),
+                      previous_msg_date = this.getLastMessageDate(current_msg_date);
 
                 if (_.isNull(previous_msg_date)) {
-                    this.content.insertAdjacentElement('beforeEnd', view.el);
+                    this.content.insertAdjacentElement('afterbegin', view.el);
                 } else {
-                    const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date.toISOString()}"]:first`, this.content).pop();
+                    const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date.toISOString()}"]:last`, this.content).pop();
                     if (view.model.get('type') === 'error' &&
                             u.hasClass('chat-error', previous_msg_el) &&
                             previous_msg_el.textContent === view.model.get('message')) {
                         // We don't show a duplicate error message
                         return;
                     }
-                    previous_msg_el.insertAdjacentElement('beforeBegin', view.el);
+                    previous_msg_el.insertAdjacentElement('afterend', view.el);
                     this.markFollowups(view.el);
                 }
                 return this.trigger('messageInserted', view.el);
@@ -736,27 +765,26 @@ converse.plugins.add('converse-chatview', {
              * @param { HTMLElement } el - The message element
              */
             markFollowups (el) {
-                const from = el.getAttribute('data-from');
-                const earlier_msg_el = el.nextElementSibling;
-                const date = dayjs(el.getAttribute('data-isodate'));
-
-                if (earlier_msg_el &&
-                        !u.hasClass('chat-msg--action', el) && !u.hasClass('chat-msg--action', earlier_msg_el) &&
-                        earlier_msg_el.getAttribute('data-from') === from &&
-                        date.isBefore(dayjs(earlier_msg_el.getAttribute('data-isodate')).add(10, 'minutes')) &&
-                        el.getAttribute('data-encrypted') === earlier_msg_el.getAttribute('data-encrypted')) {
+                const from = el.getAttribute('data-from'),
+                      previous_el = el.previousElementSibling,
+                      date = dayjs(el.getAttribute('data-isodate')),
+                      next_el = el.nextElementSibling;
+
+                if (!u.hasClass('chat-msg--action', el) && !u.hasClass('chat-msg--action', previous_el) &&
+                        previous_el.getAttribute('data-from') === from &&
+                        date.isBefore(dayjs(previous_el.getAttribute('data-isodate')).add(10, 'minutes')) &&
+                        el.getAttribute('data-encrypted') === previous_el.getAttribute('data-encrypted')) {
                     u.addClass('chat-msg--followup', el);
                 }
+                if (!next_el) { return; }
 
-                const later_msg_el = el.previousElementSibling;
-                if (!later_msg_el) { return; }
                 if (!u.hasClass('chat-msg--action', el) &&
-                        later_msg_el.getAttribute('data-from') === from &&
-                        dayjs(later_msg_el.getAttribute('data-isodate')).isBefore(date.add(10, 'minutes')) &&
-                        el.getAttribute('data-encrypted') === later_msg_el.getAttribute('data-encrypted')) {
-                    u.addClass('chat-msg--followup', later_msg_el);
+                        next_el.getAttribute('data-from') === from &&
+                        dayjs(next_el.getAttribute('data-isodate')).isBefore(date.add(10, 'minutes')) &&
+                        el.getAttribute('data-encrypted') === next_el.getAttribute('data-encrypted')) {
+                    u.addClass('chat-msg--followup', next_el);
                 } else {
-                    u.removeClass('chat-msg--followup', later_msg_el);
+                    u.removeClass('chat-msg--followup', next_el);
                 }
             },
 
@@ -781,6 +809,7 @@ converse.plugins.add('converse-chatview', {
 
                 this.insertMessage(view);
                 this.insertDayIndicator(view.el);
+                this.setScrollPosition(view.el);
 
                 if (u.isNewMessage(message)) {
                     if (message.get('sender') === 'me') {
@@ -788,8 +817,8 @@ converse.plugins.add('converse-chatview', {
                         // gets scrolled down. We always want to scroll down
                         // when the user writes a message as opposed to when a
                         // message is received.
-                        this.scrolled = false;
-                    } else if (this.scrolled && !u.isOnlyChatStateNotification(message)) {
+                        this.model.set('scrolled', false);
+                    } else if (this.model.get('scrolled', true) && !u.isOnlyChatStateNotification(message)) {
                         this.showNewMessagesIndicator();
                     }
                 }
@@ -1230,6 +1259,7 @@ converse.plugins.add('converse-chatview', {
                                 'message': text,
                                 'isodate': (new Date()).toISOString(),
                             }));
+                        this.scrollDown();
                     }
                 }
             },
@@ -1300,6 +1330,7 @@ converse.plugins.add('converse-chatview', {
                 if (_converse.auto_focus) {
                     this.focus();
                 }
+                this.focus();
             },
 
             _show () {
@@ -1341,16 +1372,26 @@ converse.plugins.add('converse-chatview', {
                     scrolled = false;
                     this.onScrolledDown();
                 }
-                this.scrolled = scrolled;
+                u.safeSave(this.model, {
+                    'scrolled': scrolled,
+                    'top_visible_message': null
+                });
             },
 
             viewUnreadMessages () {
-                this.scrolled = false;
+                this.model.save({
+                    'scrolled': false,
+                    'top_visible_message': null
+                });
                 this.scrollDown();
             },
 
-            scrollDown () {
-                if (this.content && u.isVisible(this.content) && !this.scrolled) {
+            _scrollDown () {
+                /* Inner method that gets debounced */
+                if (_.isUndefined(this.content)) {
+                    return;
+                }
+                if (u.isVisible(this.content) && !this.model.get('scrolled')) {
                     this.content.scrollTop = this.content.scrollHeight;
                 }
             },

+ 23 - 14
src/converse-muc-views.js

@@ -470,6 +470,7 @@ converse.plugins.add('converse-muc-views', {
                 this.initDebounced();
 
                 this.model.messages.on('add', this.onMessageAdded, this);
+                this.model.messages.on('rendered', this.scrollDown, this);
                 this.model.messages.on('reset', () => {
                     this.content.innerHTML = '';
                     this.removeAll();
@@ -679,6 +680,7 @@ converse.plugins.add('converse-muc-views', {
                     this.model.clearUnreadMsgCounter();
                     this.model.save();
                 }
+                this.scrollDown();
                 this.renderEmojiPicker();
             },
 
@@ -706,6 +708,7 @@ converse.plugins.add('converse-muc-views', {
                 } else if (conn_status === converse.ROOMSTATUS.ENTERED) {
                     this.hideSpinner();
                     this.setChatState(_converse.ACTIVE);
+                    this.scrollDown();
                     if (_converse.auto_focus) {
                         this.focus();
                     }
@@ -763,6 +766,7 @@ converse.plugins.add('converse-muc-views', {
                     ev.stopPropagation();
                 }
                 this.model.save({'hidden_occupants': true});
+                this.scrollDown();
             },
 
             toggleOccupants (ev) {
@@ -774,6 +778,7 @@ converse.plugins.add('converse-muc-views', {
                     ev.stopPropagation();
                 }
                 this.model.save({'hidden_occupants': !this.model.get('hidden_occupants')});
+                this.scrollDown();
             },
 
             onOccupantClicked (ev) {
@@ -1321,7 +1326,7 @@ converse.plugins.add('converse-muc-views', {
                         return;
                     }
                     if (!dayjs(el.getAttribute('data-isodate')).isSame(new Date(), "day")) {
-                        el = el.nextElementSibling;
+                        el = el.previousElementSibling;
                         continue;
                     }
                     if (data.join === nick ||
@@ -1330,7 +1335,7 @@ converse.plugins.add('converse-muc-views', {
                             data.joinleave === nick) {
                         return el;
                     }
-                    el = el.nextElementSibling;
+                    el = el.previousElementSibling;
                 }
             },
 
@@ -1341,7 +1346,7 @@ converse.plugins.add('converse-muc-views', {
                 }
                 const nick = occupant.get('nick'),
                       stat = _converse.muc_show_join_leave_status ? occupant.get('status') : null,
-                      prev_info_el = this.getPreviousJoinOrLeaveNotification(this.content.firstElementChild, nick),
+                      prev_info_el = this.getPreviousJoinOrLeaveNotification(this.content.lastElementChild, nick),
                       data = _.get(prev_info_el, 'dataset', {});
 
                 if (data.leave === nick) {
@@ -1359,8 +1364,8 @@ converse.plugins.add('converse-muc-views', {
                         'message': message
                     };
                     this.content.removeChild(prev_info_el);
-                    this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
-                    const el = this.content.firstElementChild;
+                    this.content.insertAdjacentHTML('beforeend', tpl_info(data));
+                    const el = this.content.lastElementChild;
                     setTimeout(() => u.addClass('fade-out', el), 5000);
                     setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5500);
                 } else {
@@ -1379,12 +1384,13 @@ converse.plugins.add('converse-muc-views', {
                     };
                     if (prev_info_el) {
                         this.content.removeChild(prev_info_el);
-                        this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
+                        this.content.insertAdjacentHTML('beforeend', tpl_info(data));
                     } else {
-                        this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
-                        this.insertDayIndicator(this.content.firstElementChild);
+                        this.content.insertAdjacentHTML('beforeend', tpl_info(data));
+                        this.insertDayIndicator(this.content.lastElementChild);
                     }
                 }
+                this.scrollDown();
             },
 
             showLeaveNotification (occupant) {
@@ -1395,7 +1401,7 @@ converse.plugins.add('converse-muc-views', {
                 }
                 const nick = occupant.get('nick'),
                       stat = _converse.muc_show_join_leave_status ? occupant.get('status') : null,
-                      prev_info_el = this.getPreviousJoinOrLeaveNotification(this.content.firstElementChild, nick),
+                      prev_info_el = this.getPreviousJoinOrLeaveNotification(this.content.lastElementChild, nick),
                       dataset = _.get(prev_info_el, 'dataset', {});
 
                 if (dataset.join === nick) {
@@ -1413,8 +1419,8 @@ converse.plugins.add('converse-muc-views', {
                         'message': message
                     };
                     this.content.removeChild(prev_info_el);
-                    this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
-                    const el = this.content.firstElementChild;
+                    this.content.insertAdjacentHTML('beforeend', tpl_info(data));
+                    const el = this.content.lastElementChild;
                     setTimeout(() => u.addClass('fade-out', el), 5000);
                     setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5500);
                 } else {
@@ -1433,12 +1439,13 @@ converse.plugins.add('converse-muc-views', {
                     }
                     if (prev_info_el) {
                         this.content.removeChild(prev_info_el);
-                        this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
+                        this.content.insertAdjacentHTML('beforeend', tpl_info(data));
                     } else {
-                        this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
-                        this.insertDayIndicator(this.content.firstElementChild);
+                        this.content.insertAdjacentHTML('beforeend', tpl_info(data));
+                        this.insertDayIndicator(this.content.lastElementChild);
                     }
                 }
+                this.scrollDown();
             },
 
             renderAfterTransition () {
@@ -1453,6 +1460,7 @@ converse.plugins.add('converse-muc-views', {
                 } else {
                     u.showElement(this.el.querySelector('.chat-area'));
                     u.showElement(this.el.querySelector('.occupants'));
+                    this.scrollDown();
                 }
             },
 
@@ -1502,6 +1510,7 @@ converse.plugins.add('converse-muc-views', {
                             'render_message': true
                         }));
                 }
+                this.scrollDown();
             }
         });