Bladeren bron

Rearrange: move non-nested tests to top of `describe`

JC Brand 6 jaren geleden
bovenliggende
commit
e10d9b074e
1 gewijzigde bestanden met toevoegingen van 1223 en 1218 verwijderingen
  1. 1223 1218
      spec/messages.js

+ 1223 - 1218
spec/messages.js

@@ -269,1410 +269,1415 @@
         }));
         }));
 
 
 
 
-        describe("when received from someone else", function () {
 
 
-            it("will open a chatbox and be displayed inside it",
-                mock.initConverseWithPromises(
-                    null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+        it("can be received out of order, and will still be displayed in the right order",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {},
+                function (done, _converse) {
 
 
-                test_utils.createContacts(_converse, 'current');
-                test_utils.openControlBox();
-                test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300)
-                .then(function () {
-                    spyOn(_converse, 'emit');
-                    const message = 'This is a received message';
-                    const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
 
 
-                    // We don't already have an open chatbox for this user
-                    expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
+            test_utils.createContacts(_converse, 'current');
+            test_utils.openControlBox();
 
 
-                    _converse.chatboxes.onMessage(
-                        $msg({
-                            'from': sender_jid,
-                            'to': _converse.connection.jid,
-                            'type': 'chat',
-                            'id': (new Date()).getTime()
-                        }).c('body').t(message).up()
-                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
-                    );
+            test_utils.waitUntil(function () {
+                    return $(_converse.rosterview.el).find('.roster-group').length;
+                }, 300)
+            .then(function () {
+                var message, msg;
+                spyOn(_converse, 'log');
+                spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
+                _converse.filter_by_resource = true;
+                var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
 
 
-                    expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
+                /*  <message id='aeb213' to='juliet@capulet.lit/chamber'>
+                *    <forwarded xmlns='urn:xmpp:forward:0'>
+                *      <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
+                *      <message xmlns='jabber:client'
+                *          to='juliet@capulet.lit/balcony'
+                *          from='romeo@montague.lit/orchard'
+                *          type='chat'>
+                *          <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
+                *      </message>
+                *    </forwarded>
+                *  </message>
+                */
+                msg = $msg({'id': 'aeb213', 'to': _converse.bare_jid})
+                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T13:08:25Z'}).up()
+                        .c('message', {
+                            'xmlns': 'jabber:client',
+                            'to': _converse.bare_jid,
+                            'from': sender_jid,
+                            'type': 'chat'})
+                        .c('body').t("message")
+                        .tree();
+                _converse.chatboxes.onMessage(msg);
 
 
-                    // Check that the chatbox and its view now exist
-                    const chatbox = _converse.chatboxes.get(sender_jid);
-                    const chatboxview = _converse.chatboxviews.get(sender_jid);
-                    expect(chatbox).toBeDefined();
-                    expect(chatboxview).toBeDefined();
-                    // Check that the message was received and check the message parameters
-                    expect(chatbox.messages.length).toEqual(1);
-                    const msg_obj = chatbox.messages.models[0];
-                    expect(msg_obj.get('message')).toEqual(message);
-                    expect(msg_obj.get('fullname')).toEqual(mock.cur_names[0]);
-                    expect(msg_obj.get('sender')).toEqual('them');
-                    expect(msg_obj.get('is_delayed')).toEqual(false);
-                    // Now check that the message appears inside the chatbox in the DOM
-                    const chat_content = chatboxview.el.querySelector('.chat-content');
-                    expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
-                    expect(chat_content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
-                    return test_utils.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0])
-                    .then(function () {
-                        expect(chat_content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Max Frankfurter');
-                        done();
-                    });
-                });
-            }));
+                msg = $msg({'id': 'aeb214', 'to': _converse.bare_jid})
+                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'}).up()
+                        .c('message', {
+                            'xmlns': 'jabber:client',
+                            'to': _converse.bare_jid,
+                            'from': sender_jid,
+                            'type': 'chat'})
+                        .c('body').t("Older message")
+                        .tree();
+                _converse.chatboxes.onMessage(msg);
 
 
-            it("can be replaced with a correction",
-                mock.initConverseWithPromises(
-                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                    function (done, _converse) {
+                msg = $msg({'id': 'aeb215', 'to': _converse.bare_jid})
+                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}).up()
+                        .c('message', {
+                            'xmlns': 'jabber:client',
+                            'to': _converse.bare_jid,
+                            'from': sender_jid,
+                            'type': 'chat'})
+                        .c('body').t("Inbetween message").up()
+                        .tree();
+                _converse.chatboxes.onMessage(msg);
 
 
-                test_utils.createContacts(_converse, 'current', 1);
-                _converse.emit('rosterContactsFetched');
-                test_utils.openControlBox();
-                const message = 'This is a received message';
-                const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-                test_utils.openChatBoxFor(_converse, sender_jid)
-                .then(() => {
-                    const msg_id = u.getUniqueId();
-                    _converse.chatboxes.onMessage($msg({
+                msg = $msg({'id': 'aeb216', 'to': _converse.bare_jid})
+                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}).up()
+                        .c('message', {
+                            'xmlns': 'jabber:client',
+                            'to': _converse.bare_jid,
                             'from': sender_jid,
                             'from': sender_jid,
-                            'to': _converse.connection.jid,
-                            'type': 'chat',
-                            'id': msg_id,
-                        }).c('body').t('But soft, what light through yonder airlock breaks?').tree());
+                            'type': 'chat'})
+                        .c('body').t("another inbetween message")
+                        .tree();
+                _converse.chatboxes.onMessage(msg);
 
 
-                    var chatboxview = _converse.chatboxviews.get(sender_jid);
-                    expect(chatboxview.el.querySelectorAll('.chat-msg').length).toBe(1);
-                    expect(chatboxview.el.querySelector('.chat-msg__text').textContent)
-                        .toBe('But soft, what light through yonder airlock breaks?');
+                msg = $msg({'id': 'aeb217', 'to': _converse.bare_jid})
+                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'}).up()
+                        .c('message', {
+                            'xmlns': 'jabber:client',
+                            'to': _converse.bare_jid,
+                            'from': sender_jid,
+                            'type': 'chat'})
+                        .c('body').t("An earlier message on the next day")
+                        .tree();
+                _converse.chatboxes.onMessage(msg);
 
 
-                    _converse.chatboxes.onMessage($msg({
+                msg = $msg({'id': 'aeb218', 'to': _converse.bare_jid})
+                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'}).up()
+                        .c('message', {
+                            'xmlns': 'jabber:client',
+                            'to': _converse.bare_jid,
                             'from': sender_jid,
                             'from': sender_jid,
-                            'to': _converse.connection.jid,
-                            'type': 'chat',
-                            'id': u.getUniqueId(),
-                        }).c('body').t('But soft, what light through yonder chimney breaks?').up()
-                        .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
+                            'type': 'chat'})
+                        .c('body').t("newer message from the next day")
+                        .tree();
+                _converse.chatboxes.onMessage(msg);
 
 
-                    test_utils.waitUntil(() => chatboxview.el.querySelector('.chat-msg__text').textContent ===
-                        'But soft, what light through yonder chimney breaks?').then(() => {
+                // Insert <composing> message, to also check that
+                // text messages are inserted correctly with
+                // temporary chat events in the chat contents.
+                msg = $msg({
+                        'id': 'aeb219',
+                        'to': _converse.bare_jid,
+                        'xmlns': 'jabber:client',
+                        'from': sender_jid,
+                        'type': 'chat'})
+                    .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
+                    .tree();
+                _converse.chatboxes.onMessage(msg);
 
 
-                        expect(chatboxview.el.querySelectorAll('.chat-msg').length).toBe(1);
-                        expect(chatboxview.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
+                msg = $msg({
+                        'id': 'aeb220',
+                        'to': _converse.bare_jid,
+                        'xmlns': 'jabber:client',
+                        'from': sender_jid,
+                        'type': 'chat'})
+                    .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
+                    .c('body').t("latest message")
+                    .tree();
+                _converse.chatboxes.onMessage(msg);
 
 
-                        _converse.chatboxes.onMessage($msg({
-                                'from': sender_jid,
-                                'to': _converse.connection.jid,
-                                'type': 'chat',
-                                'id': u.getUniqueId(),
-                            }).c('body').t('But soft, what light through yonder window breaks?').up()
-                            .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
+                var chatboxview = _converse.chatboxviews.get(sender_jid);
+                var $chat_content = $(chatboxview.el).find('.chat-content');
+                chatboxview.clearSpinner(); //cleanup
 
 
-                        return test_utils.waitUntil(() => chatboxview.el.querySelector('.chat-msg__text').textContent ===
-                            'But soft, what light through yonder window breaks?');
-                    }).then(() => {
-                        expect(chatboxview.el.querySelectorAll('.chat-msg').length).toBe(1);
-                        expect(chatboxview.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
-                        chatboxview.el.querySelector('.chat-msg__content .fa-edit').click();
-                        const modal = chatboxview.model.messages.at(0).message_versions_modal;
-                        return test_utils.waitUntil(() => u.isVisible(modal.el), 1000);
-                    }).then(() => {
-                        const modal = chatboxview.model.messages.at(0).message_versions_modal;
-                        const older_msgs = modal.el.querySelectorAll('.older-msg');
-                        expect(older_msgs.length).toBe(2);
-                        expect(older_msgs[0].textContent).toBe('But soft, what light through yonder airlock breaks?');
-                        expect(older_msgs[1].textContent).toBe('But soft, what light through yonder chimney breaks?');
-                        done();
-                    });
-                });
-            }));
+                expect($chat_content[0].querySelectorAll('.date-separator').length).toEqual(4);
 
 
-            describe("when a chatbox is opened for someone who is not in the roster", function () {
+                var $day = $chat_content.find('.date-separator:first');
+                expect($day.data('isodate')).toEqual(moment('2017-12-31T00:00:00').format());
 
 
-                it("the VCard for that user is fetched and the chatbox updated with the results",
-                    mock.initConverseWithPromises(
-                        null, ['rosterGroupsFetched'], {},
-                        function (done, _converse) {
+                var $time = $chat_content.find('time:first');
+                expect($time.text()).toEqual('Sunday Dec 31st 2017')
 
 
-                    _converse.allow_non_roster_messaging = true;
-                    spyOn(_converse, 'emit').and.callThrough();
+                $day = $chat_content.find('.date-separator:first');
+                expect($day[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Older message');
 
 
-                    var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    var vcard_fetched = false;
-                    spyOn(_converse.api.vcard, "get").and.callFake(function () {
-                        vcard_fetched = true;
-                        return Promise.resolve({
-                            'fullname': mock.cur_names[0],
-                            'vcard_updated': moment().format(),
-                            'jid': sender_jid
-                        });
-                    });
-                    var message = 'This is a received message from someone not on the roster';
-                    var msg = $msg({
-                            from: sender_jid,
-                            to: _converse.connection.jid,
-                            type: 'chat',
-                            id: (new Date()).getTime()
-                        }).c('body').t(message).up()
-                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
+                var $el = $chat_content.find('.chat-msg:first').find('.chat-msg__text')
+                expect($el.hasClass('chat-msg--followup')).toBe(false);
+                expect($el.text()).toEqual('Older message');
 
 
-                    // We don't already have an open chatbox for this user
-                    expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
+                $time = $chat_content.find('time.separator-text:eq(1)');
+                expect($time.text()).toEqual("Monday Jan 1st 2018");
 
 
-                    _converse.chatboxes.onMessage(msg);
-                    expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
+                $day = $chat_content.find('.date-separator:eq(1)');
+                expect($day.data('isodate')).toEqual(moment('2018-01-01T00:00:00').format());
+                expect($day[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Inbetween message');
 
 
-                    // Check that the chatbox and its view now exist
-                    var chatbox = _converse.chatboxes.get(sender_jid);
-                    var chatboxview = _converse.chatboxviews.get(sender_jid);
-                    expect(chatbox).toBeDefined();
-                    expect(chatboxview).toBeDefined();
+                $el = $chat_content.find('.chat-msg:eq(1)');
+                expect($el.find('.chat-msg__text').text()).toEqual('Inbetween message');
+                expect($el[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message');
+                $el = $chat_content.find('.chat-msg:eq(2)');
+                expect($el.find('.chat-msg__text').text()).toEqual('another inbetween message');
+                expect($el.hasClass('chat-msg--followup')).toBe(true);
 
 
-                    var author_el = chatboxview.el.querySelector('.chat-msg__author');
-                    expect(chatbox.get('fullname') === sender_jid);
-                    expect( _.includes(author_el.textContent.trim(), 'max.frankfurter@localhost')).toBeTruthy();
+                $time = $chat_content.find('time.separator-text:nth(2)');
+                expect($time.text()).toEqual("Tuesday Jan 2nd 2018");
 
 
-                    test_utils.waitUntil(function () { return vcard_fetched; }, 100)
-                    .then(function () {
-                        expect(_converse.api.vcard.get).toHaveBeenCalled();
-                        return test_utils.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0])
-                    }).then(function () {
-                        var author_el = chatboxview.el.querySelector('.chat-msg__author');
-                        expect( _.includes(author_el.textContent.trim(), 'Max Frankfurter')).toBeTruthy();
-                        done();
-                    });
-                }));
-            });
+                $day = $chat_content.find('.date-separator:nth(2)');
+                expect($day.data('isodate')).toEqual(moment('2018-01-02T00:00:00').format());
+                expect($day[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('An earlier message on the next day');
 
 
-            describe("who is not on the roster", function () {
+                $el = $chat_content.find('.chat-msg:eq(3)');
+                expect($el.find('.chat-msg__text').text()).toEqual('An earlier message on the next day');
+                expect($el.hasClass('chat-msg--followup')).toBe(false);
 
 
-                it("will open a chatbox and be displayed inside it if allow_non_roster_messaging is true",
-                    mock.initConverseWithPromises(
-                        null, ['rosterGroupsFetched'], {},
-                        function (done, _converse) {
+                $el = $chat_content.find('.chat-msg:eq(4)');
+                expect($el.find('.chat-msg__text').text()).toEqual('message');
+                expect($el[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('newer message from the next day');
+                expect($el.hasClass('chat-msg--followup')).toBe(false);
 
 
-                    _converse.allow_non_roster_messaging = false;
+                $day = $chat_content.find('.date-separator:last');
+                expect($day.data('isodate')).toEqual(moment().startOf('day').format());
+                expect($day[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message');
+                expect($el.hasClass('chat-msg--followup')).toBe(false);
+                done();
+            });
+        }));
 
 
-                    spyOn(_converse, 'emit');
-                    var message = 'This is a received message from someone not on the roster';
-                    var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    var msg = $msg({
-                            from: sender_jid,
-                            to: _converse.connection.jid,
-                            type: 'chat',
-                            id: (new Date()).getTime()
-                        }).c('body').t(message).up()
-                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
+        it("is ignored if it's a malformed headline message",
+        mock.initConverseWithPromises(
+            null, ['rosterGroupsFetched'], {},
+            function (done, _converse) {
 
 
-                    // We don't already have an open chatbox for this user
-                    expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
+            test_utils.createContacts(_converse, 'current');
+            test_utils.openControlBox();
 
 
-                    var chatbox = _converse.chatboxes.get(sender_jid);
-                    expect(chatbox).not.toBeDefined();
+            /* Ideally we wouldn't have to filter out headline
+            * messages, but Prosody gives them the wrong 'type' :(
+            */
+            sinon.spy(_converse, 'log');
+            sinon.spy(_converse.chatboxes, 'getChatBox');
+            sinon.spy(u, 'isHeadlineMessage');
+            const msg = $msg({
+                    from: 'localhost',
+                    to: _converse.bare_jid,
+                    type: 'chat',
+                    id: (new Date()).getTime()
+                }).c('body').t("This headline message will not be shown").tree();
+            _converse.chatboxes.onMessage(msg);
+            expect(_converse.log.calledWith(
+                "onMessage: Ignoring incoming headline message sent with type 'chat' from JID: localhost",
+                Strophe.LogLevel.INFO
+            )).toBeTruthy();
+            expect(u.isHeadlineMessage.called).toBeTruthy();
+            expect(u.isHeadlineMessage.returned(true)).toBeTruthy();
+            expect(_converse.chatboxes.getChatBox.called).toBeFalsy();
+            // Remove sinon spies
+            _converse.log.restore();
+            _converse.chatboxes.getChatBox.restore();
+            u.isHeadlineMessage.restore();
+            done();
+        }));
 
 
-                    // onMessage is a handler for received XMPP messages
-                    _converse.chatboxes.onMessage(msg);
-                    expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
 
 
-                    // onMessage is a handler for received XMPP messages
-                    _converse.allow_non_roster_messaging =true;
-                    _converse.chatboxes.onMessage(msg);
-                    expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
+        it("can be a carbon message, as defined in XEP-0280",
+        mock.initConverseWithPromises(
+            null, ['rosterGroupsFetched'], {},
+            function (done, _converse) {
 
 
-                    // Check that the chatbox and its view now exist
-                    chatbox = _converse.chatboxes.get(sender_jid);
-                    var chatboxview = _converse.chatboxviews.get(sender_jid);
-                    expect(chatbox).toBeDefined();
-                    expect(chatboxview).toBeDefined();
-                    // Check that the message was received and check the message parameters
-                    expect(chatbox.messages.length).toEqual(1);
-                    var msg_obj = chatbox.messages.models[0];
-                    expect(msg_obj.get('message')).toEqual(message);
-                    expect(msg_obj.get('fullname')).toEqual(undefined);
-                    expect(msg_obj.get('sender')).toEqual('them');
-                    expect(msg_obj.get('is_delayed')).toEqual(false);
-                    // Now check that the message appears inside the chatbox in the DOM
-                    var chat_content = chatboxview.el.querySelector('.chat-content');
-                    expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
-                    expect(chat_content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
-                    expect(chat_content.querySelector('span.chat-msg__author').textContent.trim()).toBe('max.frankfurter@localhost');
-                    done();
-                }));
-            });
+            test_utils.createContacts(_converse, 'current');
+            test_utils.openControlBox();
 
 
-            describe("and for which then an error message is received from the server", function () {
+            // Send a message from a different resource
+            spyOn(_converse, 'log');
+            var msgtext = 'This is a carbon message';
+            var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
+            var msg = $msg({
+                    'from': sender_jid,
+                    'id': (new Date()).getTime(),
+                    'to': _converse.connection.jid,
+                    'type': 'chat',
+                    'xmlns': 'jabber:client'
+                }).c('received', {'xmlns': 'urn:xmpp:carbons:2'})
+                .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                .c('message', {
+                        'xmlns': 'jabber:client',
+                        'from': sender_jid,
+                        'to': _converse.bare_jid+'/another-resource',
+                        'type': 'chat'
+                }).c('body').t(msgtext).tree();
+            _converse.chatboxes.onMessage(msg);
 
 
-                it("will have the error message displayed after itself",
-                    mock.initConverseWithPromises(
-                        null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                        function (done, _converse) {
+            // Check that the chatbox and its view now exist
+            var chatbox = _converse.chatboxes.get(sender_jid);
+            var chatboxview = _converse.chatboxviews.get(sender_jid);
+            expect(chatbox).toBeDefined();
+            expect(chatboxview).toBeDefined();
+            // Check that the message was received and check the message parameters
+            expect(chatbox.messages.length).toEqual(1);
+            var msg_obj = chatbox.messages.models[0];
+            expect(msg_obj.get('message')).toEqual(msgtext);
+            expect(msg_obj.get('fullname')).toEqual(mock.cur_names[1]);
+            expect(msg_obj.get('sender')).toEqual('them');
+            expect(msg_obj.get('is_delayed')).toEqual(false);
+            // Now check that the message appears inside the chatbox in the DOM
+            const chat_content = chatboxview.el.querySelector('.chat-content');
+            expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext);
+            expect(chat_content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
+            return test_utils.waitUntil(() => chatbox.vcard.get('fullname') === 'Candice van der Knijff')
+            .then(function () {
+                expect(chat_content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Candice van der Knijff');
+                done();
+            });
+        }));
 
 
-                    test_utils.createContacts(_converse, 'current');
-                    _converse.emit('rosterContactsFetched');
-                    test_utils.openControlBox();
+        it("can be a carbon message that this user sent from a different client, as defined in XEP-0280",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {},
+                function (done, _converse) {
 
 
-                    // TODO: what could still be done for error
-                    // messages... if the <error> element has type
-                    // "cancel", then we know the messages wasn't sent,
-                    // and can give the user a nicer indication of
-                    // that.
+            test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp']).then(function () {
+                test_utils.createContacts(_converse, 'current');
+                test_utils.openControlBox();
 
 
-                    /* <message from="scotty@enterprise.com/_converse.js-84843526"
-                     *          to="kirk@enterprise.com.com"
-                     *          type="chat"
-                     *          id="82bc02ce-9651-4336-baf0-fa04762ed8d2"
-                     *          xmlns="jabber:client">
-                     *      <body>yo</body>
-                     *      <active xmlns="http://jabber.org/protocol/chatstates"/>
-                     *  </message>
-                     */
-                    var sender_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    var fullname = _converse.xmppstatus.get('fullname');
-                    fullname = _.isEmpty(fullname)? _converse.bare_jid: fullname;
-                    _converse.api.chats.open(sender_jid)
-                    .then(() => {
-                        var msg_text = 'This message will not be sent, due to an error';
-                        var view = _converse.chatboxviews.get(sender_jid);
-                        var message = view.model.messages.create({
-                            'msgid': '82bc02ce-9651-4336-baf0-fa04762ed8d2',
-                            'fullname': fullname,
-                            'sender': 'me',
-                            'time': moment().format(),
-                            'message': msg_text
-                        });
-                        view.model.sendMessage(message);
-                        var $chat_content = $(view.el).find('.chat-content');
-                        var msg_txt = $chat_content.find('.chat-msg:last').find('.chat-msg__text').text();
-                        expect(msg_txt).toEqual(msg_text);
+                // Send a message from a different resource
+                spyOn(_converse, 'log');
+                const msgtext = 'This is a sent carbon message';
+                const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
+                const msg = $msg({
+                        'from': _converse.bare_jid,
+                        'id': (new Date()).getTime(),
+                        'to': _converse.connection.jid,
+                        'type': 'chat',
+                        'xmlns': 'jabber:client'
+                    }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
+                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                    .c('message', {
+                            'xmlns': 'jabber:client',
+                            'from': _converse.bare_jid+'/another-resource',
+                            'to': recipient_jid,
+                            'type': 'chat'
+                    }).c('body').t(msgtext).tree();
+                _converse.chatboxes.onMessage(msg);
 
 
-                        // We send another message, for which an error will
-                        // not be received, to test that errors appear
-                        // after the relevant message.
-                        msg_text = 'This message will be sent, and also receive an error';
-                        message = view.model.messages.create({
-                            'msgid': '6fcdeee3-000f-4ce8-a17e-9ce28f0ae104',
-                            'fullname': fullname,
-                            'sender': 'me',
-                            'time': moment().format(),
-                            'message': msg_text
-                        });
-                        view.model.sendMessage(message);
-                        msg_txt = $chat_content.find('.chat-msg:last').find('.chat-msg__text').text();
-                        expect(msg_txt).toEqual(msg_text);
-
-                        /* <message xmlns="jabber:client"
-                         *          to="scotty@enterprise.com/_converse.js-84843526"
-                         *          type="error"
-                         *          id="82bc02ce-9651-4336-baf0-fa04762ed8d2"
-                         *          from="kirk@enterprise.com.com">
-                         *     <error type="cancel">
-                         *         <remote-server-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
-                         *         <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Server-to-server connection failed: Connecting failed: connection timeout</text>
-                         *     </error>
-                         * </message>
-                         */
-                        var error_txt = 'Server-to-server connection failed: Connecting failed: connection timeout';
-                        var stanza = $msg({
-                                'to': _converse.connection.jid,
-                                'type':'error',
-                                'id':'82bc02ce-9651-4336-baf0-fa04762ed8d2',
-                                'from': sender_jid
-                            })
-                            .c('error', {'type': 'cancel'})
-                            .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
-                            .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
-                                .t('Server-to-server connection failed: Connecting failed: connection timeout');
-                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                        expect($chat_content.find('.chat-error').text()).toEqual(error_txt);
+                // Check that the chatbox and its view now exist
+                const chatbox = _converse.chatboxes.get(recipient_jid);
+                const chatboxview = _converse.chatboxviews.get(recipient_jid);
+                expect(chatbox).toBeDefined();
+                expect(chatboxview).toBeDefined();
+                // Check that the message was received and check the message parameters
+                expect(chatbox.messages.length).toEqual(1);
+                const  msg_obj = chatbox.messages.models[0];
+                expect(msg_obj.get('message')).toEqual(msgtext);
+                expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname'));
+                expect(msg_obj.get('sender')).toEqual('me');
+                expect(msg_obj.get('is_delayed')).toEqual(false);
+                // Now check that the message appears inside the chatbox in the DOM
+                const msg_txt = chatboxview.el.querySelector('.chat-content .chat-msg .chat-msg__text').textContent;
+                expect(msg_txt).toEqual(msgtext);
+                done();
+            });
+        }));
 
 
-                        stanza = $msg({
-                                'to': _converse.connection.jid,
-                                'type':'error',
-                                'id':'some-other-unused-id',
-                                'from': sender_jid
-                            })
-                            .c('error', {'type': 'cancel'})
-                            .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
-                            .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
-                                .t('Server-to-server connection failed: Connecting failed: connection timeout');
-                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                        expect($chat_content.find('.chat-error').length).toEqual(2);
+        it("will be discarded if it's a malicious message meant to look like a carbon copy",
+        mock.initConverseWithPromises(
+            null, ['rosterGroupsFetched'], {},
+            function (done, _converse) {
 
 
-                        // If the last message is already an error message,
-                        // then we don't render it another time.
-                        stanza = $msg({
-                                'to': _converse.connection.jid,
-                                'type':'error',
-                                'id':'another-unused-id',
-                                'from': sender_jid
-                            })
-                            .c('error', {'type': 'cancel'})
-                            .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
-                            .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
-                                .t('Server-to-server connection failed: Connecting failed: connection timeout');
-                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                        expect($chat_content.find('.chat-error').length).toEqual(2);
+            test_utils.createContacts(_converse, 'current');
+            test_utils.openControlBox();
+            /* <message from="mallory@evil.example" to="b@xmpp.example">
+            *    <received xmlns='urn:xmpp:carbons:2'>
+            *      <forwarded xmlns='urn:xmpp:forward:0'>
+            *          <message from="alice@xmpp.example" to="bob@xmpp.example/client1">
+            *              <body>Please come to Creepy Valley tonight, alone!</body>
+            *          </message>
+            *      </forwarded>
+            *    </received>
+            * </message>
+            */
+            spyOn(_converse, 'log');
+            var msgtext = 'Please come to Creepy Valley tonight, alone!';
+            var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
+            var impersonated_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
+            var msg = $msg({
+                    'from': sender_jid,
+                    'id': (new Date()).getTime(),
+                    'to': _converse.connection.jid,
+                    'type': 'chat',
+                    'xmlns': 'jabber:client'
+                }).c('received', {'xmlns': 'urn:xmpp:carbons:2'})
+                .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                .c('message', {
+                        'xmlns': 'jabber:client',
+                        'from': impersonated_jid,
+                        'to': _converse.connection.jid,
+                        'type': 'chat'
+                }).c('body').t(msgtext).tree();
+            _converse.chatboxes.onMessage(msg);
 
 
-                        // A different error message will however render
-                        stanza = $msg({
-                                'to': _converse.connection.jid,
-                                'type':'error',
-                                'id':'another-id',
-                                'from': sender_jid
-                            })
-                            .c('error', {'type': 'cancel'})
-                            .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
-                            .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
-                                .t('Something else went wrong as well');
-                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                        expect($chat_content.find('.chat-error').length).toEqual(3);
-                        done();
-                    });
-                }));
-            });
+            // Check that chatbox for impersonated user is not created.
+            var chatbox = _converse.chatboxes.get(impersonated_jid);
+            expect(chatbox).not.toBeDefined();
 
 
-            it("will cause the chat area to be scrolled down only if it was at the bottom originally",
-                mock.initConverseWithPromises(
-                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                    function (done, _converse) {
+            // Check that the chatbox for the malicous user is not created
+            chatbox = _converse.chatboxes.get(sender_jid);
+            expect(chatbox).not.toBeDefined();
+            done();
+        }));
 
 
-                test_utils.createContacts(_converse, 'current');
-                _converse.emit('rosterContactsFetched');
-                test_utils.openControlBox();
+        it("received for a minimized chat box will increment a counter on its header",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                function (done, _converse) {
 
 
-                let chatboxview;
-                const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-                const message = 'This message is received while the chat area is scrolled up';
-                test_utils.openChatBoxFor(_converse, sender_jid)
-                .then(() => {
-                    chatboxview = _converse.chatboxviews.get(sender_jid);
-                    spyOn(chatboxview, 'onScrolledDown').and.callThrough();
+            if (_converse.view_mode === 'fullscreen') {
+                return done();
+            }
+            test_utils.createContacts(_converse, 'current');
+            _converse.emit('rosterContactsFetched');
+            const contact_name = mock.cur_names[0];
+            const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@localhost';
+            test_utils.openControlBox();
+            spyOn(_converse, 'emit').and.callThrough();
 
 
-                    // Create enough messages so that there's a scrollbar.
-                    for (var i=0; i<20; i++) {
-                        _converse.chatboxes.onMessage($msg({
-                                from: sender_jid,
-                                to: _converse.connection.jid,
-                                type: 'chat',
-                                id: (new Date()).getTime()
-                            }).c('body').t('Message: '+i).up()
-                            .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
-                    }
-                    return test_utils.waitUntil(() => chatboxview.content.scrollTop, 1000)
-                .then(() => test_utils.waitUntil(() => !chatboxview.model.get('auto_scrolled'), 500))
-                }).then(() => {
-                    chatboxview.content.scrollTop = 0;
-                    return test_utils.waitUntil(() => chatboxview.model.get('scrolled'), 900);
-                }).then(() => {
-                    _converse.chatboxes.onMessage($msg({
-                            from: sender_jid,
-                            to: _converse.connection.jid,
-                            type: 'chat',
-                            id: (new Date()).getTime()
-                        }).c('body').t(message).up()
-                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+            test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
+            .then(() => test_utils.openChatBoxFor(_converse, contact_jid))
+            .then(() => {
+                var chatview = _converse.chatboxviews.get(contact_jid);
+                expect(u.isVisible(chatview.el)).toBeTruthy();
+                expect(chatview.model.get('minimized')).toBeFalsy();
+                chatview.el.querySelector('.toggle-chatbox-button').click();
+                expect(chatview.model.get('minimized')).toBeTruthy();
+                var message = 'This message is sent to a minimized chatbox';
+                var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                var msg = $msg({
+                    from: sender_jid,
+                    to: _converse.connection.jid,
+                    type: 'chat',
+                    id: (new Date()).getTime()
+                }).c('body').t(message).up()
+                .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
+                _converse.chatboxes.onMessage(msg);
+                expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
+                var trimmed_chatboxes = _converse.minimized_chats;
+                var trimmedview = trimmed_chatboxes.get(contact_jid);
+                var $count = $(trimmedview.el).find('.message-count');
+                expect(u.isVisible(chatview.el)).toBeFalsy();
+                expect(trimmedview.model.get('minimized')).toBeTruthy();
+                expect(u.isVisible($count[0])).toBeTruthy();
+                expect($count.html()).toBe('1');
+                _converse.chatboxes.onMessage(
+                    $msg({
+                        from: mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
+                        to: _converse.connection.jid,
+                        type: 'chat',
+                        id: (new Date()).getTime()
+                    }).c('body').t('This message is also sent to a minimized chatbox').up()
+                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
+                );
+                expect(u.isVisible(chatview.el)).toBeFalsy();
+                expect(trimmedview.model.get('minimized')).toBeTruthy();
+                $count = $(trimmedview.el).find('.message-count');
+                expect(u.isVisible($count[0])).toBeTruthy();
+                expect($count.html()).toBe('2');
+                trimmedview.el.querySelector('.restore-chat').click();
+                expect(trimmed_chatboxes.keys().length).toBe(0);
+                done();
+            });
+        }));
 
 
-                    // Now check that the message appears inside the chatbox in the DOM
-                    const $chat_content = $(chatboxview.el).find('.chat-content');
-                    const  msg_txt = $chat_content.find('.chat-msg:last').find('.chat-msg__text').text();
-                    expect(msg_txt).toEqual(message);
-                    return test_utils.waitUntil(() => u.isVisible(chatboxview.el.querySelector('.new-msgs-indicator')), 900);
-                }).then(() => {
-                    expect(chatboxview.model.get('scrolled')).toBe(true);
-                    expect(chatboxview.content.scrollTop).toBe(0);
-                    expect(u.isVisible(chatboxview.el.querySelector('.new-msgs-indicator'))).toBeTruthy();
-                    // Scroll down again
-                    chatboxview.content.scrollTop = chatboxview.content.scrollHeight;
-                    return test_utils.waitUntil(() => !u.isVisible(chatboxview.el.querySelector('.new-msgs-indicator')), 900);
-                }).then(done);
-            }));
+        it("will indicate when it has a time difference of more than a day between it and its predecessor",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                function (done, _converse) {
 
 
-            it("is ignored if it's intended for a different resource and filter_by_resource is set to true",
-                mock.initConverseWithPromises(
-                    null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+            test_utils.createContacts(_converse, 'current');
+            _converse.emit('rosterContactsFetched');
+            test_utils.openControlBox();
+            spyOn(_converse, 'emit');
+            const contact_name = mock.cur_names[1];
+            const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@localhost';
+            test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
+            .then(() => test_utils.openChatBoxFor(_converse, contact_jid))
+            .then(() => {
+                test_utils.clearChatBoxMessages(_converse, contact_jid);
+                var one_day_ago = moment();
+                one_day_ago.subtract('days', 1);
+                var message = 'This is a day old message';
+                var chatbox = _converse.chatboxes.get(contact_jid);
+                var chatboxview = _converse.chatboxviews.get(contact_jid);
+                var msg_obj;
+                var msg_txt;
+                var sender_txt;
 
 
-                test_utils.createContacts(_converse, 'current');
-                test_utils.openControlBox();
+                var msg = $msg({
+                    from: contact_jid,
+                    to: _converse.connection.jid,
+                    type: 'chat',
+                    id: one_day_ago.unix()
+                }).c('body').t(message).up()
+                .c('delay', { xmlns:'urn:xmpp:delay', from: 'localhost', stamp: one_day_ago.format() })
+                .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
+                _converse.chatboxes.onMessage(msg);
+                expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
+                expect(chatbox.messages.length).toEqual(1);
+                msg_obj = chatbox.messages.models[0];
+                expect(msg_obj.get('message')).toEqual(message);
+                expect(msg_obj.get('fullname')).toEqual(contact_name);
+                expect(msg_obj.get('sender')).toEqual('them');
+                expect(msg_obj.get('is_delayed')).toEqual(true);
 
 
-                test_utils.waitUntil(function () {
-                        return $(_converse.rosterview.el).find('.roster-group').length;
-                    }, 300)
+                return test_utils.waitUntil(() => chatbox.vcard.get('fullname') === 'Candice van der Knijff')
                 .then(function () {
                 .then(function () {
-                    // Send a message from a different resource
-                    var message, sender_jid, msg;
-                    spyOn(_converse, 'log');
-                    spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
-                    _converse.filter_by_resource = true;
-                    sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    msg = $msg({
-                            from: sender_jid,
-                            to: _converse.bare_jid+"/some-other-resource",
-                            type: 'chat',
-                            id: (new Date()).getTime()
-                        }).c('body').t("This message will not be shown").up()
-                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-                    _converse.chatboxes.onMessage(msg);
+                    const chat_content = chatboxview.el.querySelector('.chat-content');
+                    expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
+                    expect(chat_content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
+                    expect(chat_content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Candice van der Knijff');
 
 
-                    expect(_converse.log).toHaveBeenCalledWith(
-                            "onMessage: Ignoring incoming message intended for a different resource: dummy@localhost/some-other-resource",
-                            Strophe.LogLevel.INFO);
-                    expect(_converse.chatboxes.getChatBox).not.toHaveBeenCalled();
-                    _converse.filter_by_resource = false;
+                    expect(chat_content.querySelectorAll('.date-separator').length).toEqual(1);
+                    const day = chat_content.querySelector('.date-separator');
+                    expect(day.getAttribute('class')).toEqual('message date-separator');
+                    expect(day.getAttribute('data-isodate')).toEqual(moment(one_day_ago.startOf('day')).format());
 
 
-                    message = "This message sent to a different resource will be shown";
+                    const $chat_content = $(chat_content);
+                    var $time = $chat_content.find('time.separator-text');
+                    expect($time.text()).toEqual(moment(one_day_ago.startOf('day')).format("dddd MMM Do YYYY"));
+
+                    message = 'This is a current message';
                     msg = $msg({
                     msg = $msg({
-                            from: sender_jid,
-                            to: _converse.bare_jid+"/some-other-resource",
-                            type: 'chat',
-                            id: '134234623462346'
-                        }).c('body').t(message).up()
-                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
+                        from: contact_jid,
+                        to: _converse.connection.jid,
+                        type: 'chat',
+                        id: new Date().getTime()
+                    }).c('body').t(message).up()
+                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
                     _converse.chatboxes.onMessage(msg);
                     _converse.chatboxes.onMessage(msg);
+                    expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
+                    // Check that there is a <time> element, with the required
+                    // props.
+                    expect($chat_content[0].querySelectorAll('time.separator-text').length).toEqual(2); // There are now two time elements
 
 
-                    expect(_converse.chatboxes.getChatBox).toHaveBeenCalled();
-                    var chatboxview = _converse.chatboxviews.get(sender_jid);
-                    var $chat_content = $(chatboxview.el).find('.chat-content:last');
-                    var msg_txt = $chat_content.find('.chat-msg').find('.chat-msg__text').text();
+                    var message_date = new Date();
+                    const $day = $chat_content.find('.date-separator:last');
+                    expect($day.length).toEqual(1);
+                    expect($day.attr('class')).toEqual('message date-separator');
+                    expect($day.data('isodate')).toEqual(moment(message_date).startOf('day').format());
+
+                    $time = $chat_content.find('time.separator-text:last');
+                    expect($time.text()).toEqual(moment(message_date).startOf('day').format("dddd MMM Do YYYY"));
+
+                    // Normal checks for the 2nd message
+                    expect(chatbox.messages.length).toEqual(2);
+                    msg_obj = chatbox.messages.models[1];
+                    expect(msg_obj.get('message')).toEqual(message);
+                    expect(msg_obj.get('fullname')).toEqual(contact_name);
+                    expect(msg_obj.get('sender')).toEqual('them');
+                    expect(msg_obj.get('is_delayed')).toEqual(false);
+                    msg_txt = $chat_content.find('.chat-msg').last().find('.chat-msg__text').text();
                     expect(msg_txt).toEqual(message);
                     expect(msg_txt).toEqual(message);
+
+                    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('Candice van der Knijff');
                     done();
                     done();
                 });
                 });
-            }));
-        });
+            });
+        }));
 
 
-        it("can be received out of order, and will still be displayed in the right order",
+        it("can be sent from a chatbox, and will appear inside it",
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    function (done, _converse) {
+
+            test_utils.createContacts(_converse, 'current');
+            _converse.emit('rosterContactsFetched');
+            test_utils.openControlBox();
+
+            spyOn(_converse, 'emit');
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            test_utils.openChatBoxFor(_converse, contact_jid)
+            .then(() => {
+                expect(_converse.emit).toHaveBeenCalledWith('chatBoxFocused', jasmine.any(Object));
+                const view = _converse.chatboxviews.get(contact_jid);
+                const message = 'This message is sent from this chatbox';
+                spyOn(view.model, 'sendMessage').and.callThrough();
+                test_utils.sendMessage(view, message);
+                expect(view.model.sendMessage).toHaveBeenCalled();
+                expect(view.model.messages.length, 2);
+                expect(_converse.emit.calls.mostRecent().args, ['messageSend', message]);
+                expect($(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text').text()).toEqual(message);
+                done();
+            });
+        }));
+
+        it("is sanitized to prevent Javascript injection attacks",
             mock.initConverseWithPromises(
             mock.initConverseWithPromises(
-                null, ['rosterGroupsFetched'], {},
+                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                 function (done, _converse) {
                 function (done, _converse) {
 
 
-
             test_utils.createContacts(_converse, 'current');
             test_utils.createContacts(_converse, 'current');
+            _converse.emit('rosterContactsFetched');
             test_utils.openControlBox();
             test_utils.openControlBox();
 
 
-            test_utils.waitUntil(function () {
-                    return $(_converse.rosterview.el).find('.roster-group').length;
-                }, 300)
-            .then(function () {
-                var message, msg;
-                spyOn(_converse, 'log');
-                spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
-                _converse.filter_by_resource = true;
-                var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-
-                /*  <message id='aeb213' to='juliet@capulet.lit/chamber'>
-                *    <forwarded xmlns='urn:xmpp:forward:0'>
-                *      <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
-                *      <message xmlns='jabber:client'
-                *          to='juliet@capulet.lit/balcony'
-                *          from='romeo@montague.lit/orchard'
-                *          type='chat'>
-                *          <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
-                *      </message>
-                *    </forwarded>
-                *  </message>
-                */
-                msg = $msg({'id': 'aeb213', 'to': _converse.bare_jid})
-                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T13:08:25Z'}).up()
-                        .c('message', {
-                            'xmlns': 'jabber:client',
-                            'to': _converse.bare_jid,
-                            'from': sender_jid,
-                            'type': 'chat'})
-                        .c('body').t("message")
-                        .tree();
-                _converse.chatboxes.onMessage(msg);
-
-                msg = $msg({'id': 'aeb214', 'to': _converse.bare_jid})
-                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'}).up()
-                        .c('message', {
-                            'xmlns': 'jabber:client',
-                            'to': _converse.bare_jid,
-                            'from': sender_jid,
-                            'type': 'chat'})
-                        .c('body').t("Older message")
-                        .tree();
-                _converse.chatboxes.onMessage(msg);
-
-                msg = $msg({'id': 'aeb215', 'to': _converse.bare_jid})
-                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}).up()
-                        .c('message', {
-                            'xmlns': 'jabber:client',
-                            'to': _converse.bare_jid,
-                            'from': sender_jid,
-                            'type': 'chat'})
-                        .c('body').t("Inbetween message").up()
-                        .tree();
-                _converse.chatboxes.onMessage(msg);
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            test_utils.openChatBoxFor(_converse, contact_jid)
+            .then(() => {
+                const view = _converse.chatboxviews.get(contact_jid);
+                const message = '<p>This message contains <em>some</em> <b>markup</b></p>';
+                spyOn(view.model, 'sendMessage').and.callThrough();
+                test_utils.sendMessage(view, message);
+                expect(view.model.sendMessage).toHaveBeenCalled();
+                const msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
+                expect(msg.text()).toEqual(message);
+                expect(msg.html()).toEqual('&lt;p&gt;This message contains &lt;em&gt;some&lt;/em&gt; &lt;b&gt;markup&lt;/b&gt;&lt;/p&gt;');
+                done();
+            });
+        }));
 
 
-                msg = $msg({'id': 'aeb216', 'to': _converse.bare_jid})
-                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}).up()
-                        .c('message', {
-                            'xmlns': 'jabber:client',
-                            'to': _converse.bare_jid,
-                            'from': sender_jid,
-                            'type': 'chat'})
-                        .c('body').t("another inbetween message")
-                        .tree();
-                _converse.chatboxes.onMessage(msg);
+        it("can contain hyperlinks, which will be clickable",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                function (done, _converse) {
 
 
-                msg = $msg({'id': 'aeb217', 'to': _converse.bare_jid})
-                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'}).up()
-                        .c('message', {
-                            'xmlns': 'jabber:client',
-                            'to': _converse.bare_jid,
-                            'from': sender_jid,
-                            'type': 'chat'})
-                        .c('body').t("An earlier message on the next day")
-                        .tree();
-                _converse.chatboxes.onMessage(msg);
+            test_utils.createContacts(_converse, 'current');
+            _converse.emit('rosterContactsFetched');
+            test_utils.openControlBox();
 
 
-                msg = $msg({'id': 'aeb218', 'to': _converse.bare_jid})
-                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'}).up()
-                        .c('message', {
-                            'xmlns': 'jabber:client',
-                            'to': _converse.bare_jid,
-                            'from': sender_jid,
-                            'type': 'chat'})
-                        .c('body').t("newer message from the next day")
-                        .tree();
-                _converse.chatboxes.onMessage(msg);
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            test_utils.openChatBoxFor(_converse, contact_jid)
+            .then(() => {
+                const view = _converse.chatboxviews.get(contact_jid);
+                const message = 'This message contains a hyperlink: www.opkode.com';
+                spyOn(view.model, 'sendMessage').and.callThrough();
+                test_utils.sendMessage(view, message);
+                expect(view.model.sendMessage).toHaveBeenCalled();
+                const msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
+                expect(msg.text()).toEqual(message);
+                expect(msg.html()).toEqual('This message contains a hyperlink: <a target="_blank" rel="noopener" href="http://www.opkode.com">www.opkode.com</a>');
+                done();
+            });
+        }));
 
 
-                // Insert <composing> message, to also check that
-                // text messages are inserted correctly with
-                // temporary chat events in the chat contents.
-                msg = $msg({
-                        'id': 'aeb219',
-                        'to': _converse.bare_jid,
-                        'xmlns': 'jabber:client',
-                        'from': sender_jid,
-                        'type': 'chat'})
-                    .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
-                    .tree();
-                _converse.chatboxes.onMessage(msg);
+        it("will have properly escaped URLs",
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    function (done, _converse) {
 
 
-                msg = $msg({
-                        'id': 'aeb220',
-                        'to': _converse.bare_jid,
-                        'xmlns': 'jabber:client',
-                        'from': sender_jid,
-                        'type': 'chat'})
-                    .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
-                    .c('body').t("latest message")
-                    .tree();
-                _converse.chatboxes.onMessage(msg);
+            test_utils.createContacts(_converse, 'current');
+            _converse.emit('rosterContactsFetched');
+            test_utils.openControlBox();
 
 
-                var chatboxview = _converse.chatboxviews.get(sender_jid);
-                var $chat_content = $(chatboxview.el).find('.chat-content');
-                chatboxview.clearSpinner(); //cleanup
+            let message, msg;
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            test_utils.openChatBoxFor(_converse, contact_jid)
+            .then(() => {
+                var view = _converse.chatboxviews.get(contact_jid);
+                spyOn(view.model, 'sendMessage').and.callThrough();
+                message = "http://www.opkode.com/'onmouseover='alert(1)'whatever";
+                test_utils.sendMessage(view, message);
+                expect(view.model.sendMessage).toHaveBeenCalled();
+                msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
+                expect(msg.text()).toEqual(message);
+                expect(msg.html()).toEqual('<a target="_blank" rel="noopener" href="http://www.opkode.com/%27onmouseover=%27alert%281%29%27whatever">http://www.opkode.com/\'onmouseover=\'alert(1)\'whatever</a>');
+                message = 'http://www.opkode.com/"onmouseover="alert(1)"whatever';
+                test_utils.sendMessage(view, message);
 
 
-                expect($chat_content[0].querySelectorAll('.date-separator').length).toEqual(4);
+                expect(view.model.sendMessage).toHaveBeenCalled();
+                msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
+                expect(msg.text()).toEqual(message);
+                expect(msg.html()).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>');
 
 
-                var $day = $chat_content.find('.date-separator:first');
-                expect($day.data('isodate')).toEqual(moment('2017-12-31T00:00:00').format());
+                message = "https://en.wikipedia.org/wiki/Ender's_Game";
+                test_utils.sendMessage(view, message);
 
 
-                var $time = $chat_content.find('time:first');
-                expect($time.text()).toEqual('Sunday Dec 31st 2017')
+                expect(view.model.sendMessage).toHaveBeenCalled();
+                msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
+                expect(msg.text()).toEqual(message);
+                expect(msg.html()).toEqual('<a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Ender%27s_Game">'+message+'</a>');
 
 
-                $day = $chat_content.find('.date-separator:first');
-                expect($day[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Older message');
+                message = "https://en.wikipedia.org/wiki/Ender's_Game";
+                test_utils.sendMessage(view, message);
 
 
-                var $el = $chat_content.find('.chat-msg:first').find('.chat-msg__text')
-                expect($el.hasClass('chat-msg--followup')).toBe(false);
-                expect($el.text()).toEqual('Older message');
+                expect(view.model.sendMessage).toHaveBeenCalled();
+                msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
+                expect(msg.text()).toEqual(message);
+                expect(msg.html()).toEqual('<a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Ender%27s_Game">'+message+'</a>');
+                done();
+            });
+        }));
 
 
-                $time = $chat_content.find('time.separator-text:eq(1)');
-                expect($time.text()).toEqual("Monday Jan 1st 2018");
+        it("will render newlines",
+                mock.initConverseWithPromises(null, ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
 
 
-                $day = $chat_content.find('.date-separator:eq(1)');
-                expect($day.data('isodate')).toEqual(moment('2018-01-01T00:00:00').format());
-                expect($day[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Inbetween message');
+            test_utils.createContacts(_converse, 'current');
+            _converse.emit('rosterContactsFetched');
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            test_utils.openChatBoxFor(_converse, contact_jid);
 
 
-                $el = $chat_content.find('.chat-msg:eq(1)');
-                expect($el.find('.chat-msg__text').text()).toEqual('Inbetween message');
-                expect($el[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message');
-                $el = $chat_content.find('.chat-msg:eq(2)');
-                expect($el.find('.chat-msg__text').text()).toEqual('another inbetween message');
-                expect($el.hasClass('chat-msg--followup')).toBe(true);
+            let stanza = Strophe.xmlHtmlNode(
+                "<message from='"+contact_jid+"'"+
+                "         type='chat'"+
+                "         to='dummy@localhost/resource'>"+
+                "    <body>Hey\nHave you heard the news?</body>"+
+                "</message>").firstChild;
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
 
-                $time = $chat_content.find('time.separator-text:nth(2)');
-                expect($time.text()).toEqual("Tuesday Jan 2nd 2018");
+            const view = _converse.chatboxviews.get(contact_jid);
+            const chat_content = view.el.querySelector('.chat-content');
+            expect(chat_content.querySelector('.chat-msg__text').innerHTML).toBe('Hey<br>Have you heard the news?');
 
 
-                $day = $chat_content.find('.date-separator:nth(2)');
-                expect($day.data('isodate')).toEqual(moment('2018-01-02T00:00:00').format());
-                expect($day[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('An earlier message on the next day');
+            stanza = Strophe.xmlHtmlNode(
+                "<message from='"+contact_jid+"'"+
+                "         type='chat'"+
+                "         to='dummy@localhost/resource'>"+
+                "    <body>Hey\n\n\nHave you heard the news?</body>"+
+                "</message>").firstChild;
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+            expect(chat_content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('Hey<br><br>Have you heard the news?');
 
 
-                $el = $chat_content.find('.chat-msg:eq(3)');
-                expect($el.find('.chat-msg__text').text()).toEqual('An earlier message on the next day');
-                expect($el.hasClass('chat-msg--followup')).toBe(false);
+            stanza = Strophe.xmlHtmlNode(
+                "<message from='"+contact_jid+"'"+
+                "         type='chat'"+
+                "         to='dummy@localhost/resource'>"+
+                "    <body>Hey\nHave you heard\nthe news?</body>"+
+                "</message>").firstChild;
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+            expect(chat_content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('Hey<br>Have you heard<br>the news?');
+            done();
+        }));
 
 
-                $el = $chat_content.find('.chat-msg:eq(4)');
-                expect($el.find('.chat-msg__text').text()).toEqual('message');
-                expect($el[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('newer message from the next day');
-                expect($el.hasClass('chat-msg--followup')).toBe(false);
+        it("will render images from their URLs",
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    function (done, _converse) {
 
 
-                $day = $chat_content.find('.date-separator:last');
-                expect($day.data('isodate')).toEqual(moment().startOf('day').format());
-                expect($day[0].nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message');
-                expect($el.hasClass('chat-msg--followup')).toBe(false);
+            test_utils.createContacts(_converse, 'current', 1);
+            _converse.emit('rosterContactsFetched');
+            const base_url = document.URL.split(window.location.pathname)[0];
+            let message = base_url+"/logo/conversejs-filled.svg";
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            let view;
+            test_utils.openChatBoxFor(_converse, contact_jid)
+            .then(() => {
+                view = _converse.chatboxviews.get(contact_jid);
+                spyOn(view.model, 'sendMessage').and.callThrough();
+                test_utils.sendMessage(view, message);
+                return test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length, 1000)
+            }).then(() => {
+                expect(view.model.sendMessage).toHaveBeenCalled();
+                const msg = $(view.el).find('.chat-content .chat-msg').last().find('.chat-msg__text');
+                expect(msg.html().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"'+
+                    ' src="' + message + '"></a>');
+                message += "?param1=val1&param2=val2";
+                test_utils.sendMessage(view, message);
+                return test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 2, 1000);
+            }).then(() => {
+                expect(view.model.sendMessage).toHaveBeenCalled();
+                const msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
+                expect(msg.html().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'+
+                    ' class="chat-image img-thumbnail" src="'+message.replace(/&/g, '&amp;')+'"></a>')
+
+                // Test now with two images in one message
+                message += ' hello world '+base_url+"/logo/conversejs-filled.svg";
+                test_utils.sendMessage(view, message);
+                return test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 4, 1000);
+            }).then(function () {
+                expect(view.model.sendMessage).toHaveBeenCalled();
+                const msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
+                expect(msg[0].textContent.trim()).toEqual('hello world');
+                expect(msg[0].querySelectorAll('img').length).toEqual(2);
                 done();
                 done();
             });
             });
         }));
         }));
 
 
-        it("is ignored if it's a malformed headline message",
-        mock.initConverseWithPromises(
-            null, ['rosterGroupsFetched'], {},
-            function (done, _converse) {
+        it("will render the message time as configured",
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched', 'chatBoxesFetched'],
+                    {}, function (done, _converse) {
 
 
             test_utils.createContacts(_converse, 'current');
             test_utils.createContacts(_converse, 'current');
-            test_utils.openControlBox();
-
-            /* Ideally we wouldn't have to filter out headline
-            * messages, but Prosody gives them the wrong 'type' :(
-            */
-            sinon.spy(_converse, 'log');
-            sinon.spy(_converse.chatboxes, 'getChatBox');
-            sinon.spy(u, 'isHeadlineMessage');
-            const msg = $msg({
-                    from: 'localhost',
-                    to: _converse.bare_jid,
-                    type: 'chat',
-                    id: (new Date()).getTime()
-                }).c('body').t("This headline message will not be shown").tree();
-            _converse.chatboxes.onMessage(msg);
-            expect(_converse.log.calledWith(
-                "onMessage: Ignoring incoming headline message sent with type 'chat' from JID: localhost",
-                Strophe.LogLevel.INFO
-            )).toBeTruthy();
-            expect(u.isHeadlineMessage.called).toBeTruthy();
-            expect(u.isHeadlineMessage.returned(true)).toBeTruthy();
-            expect(_converse.chatboxes.getChatBox.called).toBeFalsy();
-            // Remove sinon spies
-            _converse.log.restore();
-            _converse.chatboxes.getChatBox.restore();
-            u.isHeadlineMessage.restore();
-            done();
-        }));
-
+            _converse.emit('rosterContactsFetched');
 
 
-        it("can be a carbon message, as defined in XEP-0280",
-        mock.initConverseWithPromises(
-            null, ['rosterGroupsFetched'], {},
-            function (done, _converse) {
+            _converse.time_format = 'hh:mm';
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            test_utils.openChatBoxFor(_converse, contact_jid)
+            .then(() => {
+                const view = _converse.chatboxviews.get(contact_jid);
+                const message = 'This message is sent from this chatbox';
+                test_utils.sendMessage(view, message);
 
 
-            test_utils.createContacts(_converse, 'current');
-            test_utils.openControlBox();
+                const chatbox = _converse.chatboxes.get(contact_jid);
+                expect(chatbox.messages.models.length, 1);
+                const msg_object = chatbox.messages.models[0];
 
 
-            // Send a message from a different resource
-            spyOn(_converse, 'log');
-            var msgtext = 'This is a carbon message';
-            var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
-            var msg = $msg({
-                    'from': sender_jid,
-                    'id': (new Date()).getTime(),
-                    'to': _converse.connection.jid,
-                    'type': 'chat',
-                    'xmlns': 'jabber:client'
-                }).c('received', {'xmlns': 'urn:xmpp:carbons:2'})
-                .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                .c('message', {
-                        'xmlns': 'jabber:client',
-                        'from': sender_jid,
-                        'to': _converse.bare_jid+'/another-resource',
-                        'type': 'chat'
-                }).c('body').t(msgtext).tree();
-            _converse.chatboxes.onMessage(msg);
+                const msg_author = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__author');
+                expect(msg_author.textContent.trim()).toBe('Max Mustermann');
 
 
-            // Check that the chatbox and its view now exist
-            var chatbox = _converse.chatboxes.get(sender_jid);
-            var chatboxview = _converse.chatboxviews.get(sender_jid);
-            expect(chatbox).toBeDefined();
-            expect(chatboxview).toBeDefined();
-            // Check that the message was received and check the message parameters
-            expect(chatbox.messages.length).toEqual(1);
-            var msg_obj = chatbox.messages.models[0];
-            expect(msg_obj.get('message')).toEqual(msgtext);
-            expect(msg_obj.get('fullname')).toEqual(mock.cur_names[1]);
-            expect(msg_obj.get('sender')).toEqual('them');
-            expect(msg_obj.get('is_delayed')).toEqual(false);
-            // Now check that the message appears inside the chatbox in the DOM
-            const chat_content = chatboxview.el.querySelector('.chat-content');
-            expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext);
-            expect(chat_content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
-            return test_utils.waitUntil(() => chatbox.vcard.get('fullname') === 'Candice van der Knijff')
-            .then(function () {
-                expect(chat_content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Candice van der Knijff');
+                const msg_time = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__time');
+                const time = moment(msg_object.get('time')).format(_converse.time_format);
+                expect(msg_time.textContent).toBe(time);
                 done();
                 done();
             });
             });
         }));
         }));
 
 
-        it("can be a carbon message that this user sent from a different client, as defined in XEP-0280",
+        it("will be correctly identified and rendered as a followup message",
             mock.initConverseWithPromises(
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched'], {},
                 null, ['rosterGroupsFetched'], {},
                 function (done, _converse) {
                 function (done, _converse) {
 
 
-            test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp']).then(function () {
-                test_utils.createContacts(_converse, 'current');
-                test_utils.openControlBox();
+            test_utils.createContacts(_converse, 'current');
+            test_utils.openControlBox();
 
 
-                // Send a message from a different resource
+            test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300)
+            .then(function () {
+                const base_time = new Date();
+                const ONE_MINUTE_LATER = 60000;
+
+                jasmine.clock().install();
+                jasmine.clock().mockDate(base_time);
+
+                var message, msg;
                 spyOn(_converse, 'log');
                 spyOn(_converse, 'log');
-                const msgtext = 'This is a sent carbon message';
-                const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
-                const msg = $msg({
-                        'from': _converse.bare_jid,
-                        'id': (new Date()).getTime(),
+                spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
+                _converse.filter_by_resource = true;
+                var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+
+                _converse.chatboxes.onMessage($msg({
+                        'from': sender_jid,
                         'to': _converse.connection.jid,
                         'to': _converse.connection.jid,
                         'type': 'chat',
                         'type': 'chat',
-                        'xmlns': 'jabber:client'
-                    }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'})
-                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                    .c('message', {
-                            'xmlns': 'jabber:client',
-                            'from': _converse.bare_jid+'/another-resource',
-                            'to': recipient_jid,
-                            'type': 'chat'
-                    }).c('body').t(msgtext).tree();
-                _converse.chatboxes.onMessage(msg);
+                        'id': (new Date()).getTime()
+                    }).c('body').t('A message').up()
+                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
 
 
-                // Check that the chatbox and its view now exist
-                const chatbox = _converse.chatboxes.get(recipient_jid);
-                const chatboxview = _converse.chatboxviews.get(recipient_jid);
-                expect(chatbox).toBeDefined();
-                expect(chatboxview).toBeDefined();
-                // Check that the message was received and check the message parameters
-                expect(chatbox.messages.length).toEqual(1);
-                const  msg_obj = chatbox.messages.models[0];
-                expect(msg_obj.get('message')).toEqual(msgtext);
-                expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname'));
-                expect(msg_obj.get('sender')).toEqual('me');
-                expect(msg_obj.get('is_delayed')).toEqual(false);
-                // Now check that the message appears inside the chatbox in the DOM
-                const msg_txt = chatboxview.el.querySelector('.chat-content .chat-msg .chat-msg__text').textContent;
-                expect(msg_txt).toEqual(msgtext);
-                done();
-            });
-        }));
+                jasmine.clock().tick(3*ONE_MINUTE_LATER);
 
 
-        it("will be discarded if it's a malicious message meant to look like a carbon copy",
-        mock.initConverseWithPromises(
-            null, ['rosterGroupsFetched'], {},
-            function (done, _converse) {
+                _converse.chatboxes.onMessage($msg({
+                        'from': sender_jid,
+                        'to': _converse.connection.jid,
+                        'type': 'chat',
+                        'id': (new Date()).getTime()
+                    }).c('body').t("Another message 3 minutes later").up()
+                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
 
 
-            test_utils.createContacts(_converse, 'current');
-            test_utils.openControlBox();
-            /* <message from="mallory@evil.example" to="b@xmpp.example">
-            *    <received xmlns='urn:xmpp:carbons:2'>
-            *      <forwarded xmlns='urn:xmpp:forward:0'>
-            *          <message from="alice@xmpp.example" to="bob@xmpp.example/client1">
-            *              <body>Please come to Creepy Valley tonight, alone!</body>
-            *          </message>
-            *      </forwarded>
-            *    </received>
-            * </message>
-            */
-            spyOn(_converse, 'log');
-            var msgtext = 'Please come to Creepy Valley tonight, alone!';
-            var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
-            var impersonated_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
-            var msg = $msg({
-                    'from': sender_jid,
-                    'id': (new Date()).getTime(),
-                    'to': _converse.connection.jid,
-                    'type': 'chat',
-                    'xmlns': 'jabber:client'
-                }).c('received', {'xmlns': 'urn:xmpp:carbons:2'})
-                .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                .c('message', {
+                jasmine.clock().tick(11*ONE_MINUTE_LATER);
+
+                _converse.chatboxes.onMessage($msg({
+                        'from': sender_jid,
+                        'to': _converse.connection.jid,
+                        'type': 'chat',
+                        'id': (new Date()).getTime()
+                    }).c('body').t("Another message 14 minutes since we started").up()
+                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+
+                jasmine.clock().tick(1000);
+
+                // Insert <composing> message, to also check that
+                // text messages are inserted correctly with
+                // temporary chat events in the chat contents.
+                _converse.chatboxes.onMessage($msg({
+                        'id': 'aeb219',
+                        'to': _converse.bare_jid,
                         'xmlns': 'jabber:client',
                         'xmlns': 'jabber:client',
-                        'from': impersonated_jid,
+                        'from': sender_jid,
+                        'type': 'chat'})
+                    .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
+                    .tree());
+
+                jasmine.clock().tick(1*ONE_MINUTE_LATER);
+
+                _converse.chatboxes.onMessage($msg({
+                        'from': sender_jid,
                         'to': _converse.connection.jid,
                         'to': _converse.connection.jid,
-                        'type': 'chat'
-                }).c('body').t(msgtext).tree();
-            _converse.chatboxes.onMessage(msg);
+                        'type': 'chat',
+                        'id': (new Date()).getTime()
+                    }).c('body').t("Another message 1 minute and 1 second since the previous one").up()
+                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
 
 
-            // Check that chatbox for impersonated user is not created.
-            var chatbox = _converse.chatboxes.get(impersonated_jid);
-            expect(chatbox).not.toBeDefined();
+                jasmine.clock().tick(1*ONE_MINUTE_LATER);
 
 
-            // Check that the chatbox for the malicous user is not created
-            chatbox = _converse.chatboxes.get(sender_jid);
-            expect(chatbox).not.toBeDefined();
-            done();
-        }));
+                var view = _converse.chatboxviews.get(sender_jid);
+                test_utils.sendMessage(view, "Another message within 10 minutes, but from a different person");
 
 
-        it("received for a minimized chat box will increment a counter on its header",
-            mock.initConverseWithPromises(
-                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                function (done, _converse) {
+                var chat_content = view.el.querySelector('.chat-content');
+                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(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(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(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");
 
 
-            if (_converse.view_mode === 'fullscreen') {
-                return done();
-            }
-            test_utils.createContacts(_converse, 'current');
-            _converse.emit('rosterContactsFetched');
-            const contact_name = mock.cur_names[0];
-            const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@localhost';
-            test_utils.openControlBox();
-            spyOn(_converse, 'emit').and.callThrough();
+                // Let's add a delayed, inbetween message
+                _converse.chatboxes.onMessage($msg({'id': 'aeb218', 'to': _converse.bare_jid})
+                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                        .c('delay', {'xmlns': 'urn:xmpp:delay',
+                                     'stamp': moment(base_time).add(5, 'minutes').format()
+                                    }).up()
+                        .c('message', {
+                            'xmlns': 'jabber:client',
+                            'to': _converse.bare_jid,
+                            'from': sender_jid,
+                            'type': 'chat'})
+                        .c('body').t("A delayed message, sent 5 minutes since we started")
+                        .tree());
 
 
-            test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
-            .then(() => test_utils.openChatBoxFor(_converse, contact_jid))
-            .then(() => {
-                var chatview = _converse.chatboxviews.get(contact_jid);
-                expect(u.isVisible(chatview.el)).toBeTruthy();
-                expect(chatview.model.get('minimized')).toBeFalsy();
-                chatview.el.querySelector('.toggle-chatbox-button').click();
-                expect(chatview.model.get('minimized')).toBeTruthy();
-                var message = 'This message is sent to a minimized chatbox';
-                var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-                var msg = $msg({
-                    from: sender_jid,
-                    to: _converse.connection.jid,
-                    type: 'chat',
-                    id: (new Date()).getTime()
-                }).c('body').t(message).up()
-                .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-                _converse.chatboxes.onMessage(msg);
-                expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
-                var trimmed_chatboxes = _converse.minimized_chats;
-                var trimmedview = trimmed_chatboxes.get(contact_jid);
-                var $count = $(trimmedview.el).find('.message-count');
-                expect(u.isVisible(chatview.el)).toBeFalsy();
-                expect(trimmedview.model.get('minimized')).toBeTruthy();
-                expect(u.isVisible($count[0])).toBeTruthy();
-                expect($count.html()).toBe('1');
-                _converse.chatboxes.onMessage(
-                    $msg({
-                        from: mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
-                        to: _converse.connection.jid,
-                        type: 'chat',
-                        id: (new Date()).getTime()
-                    }).c('body').t('This message is also sent to a minimized chatbox').up()
-                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
-                );
-                expect(u.isVisible(chatview.el)).toBeFalsy();
-                expect(trimmedview.model.get('minimized')).toBeTruthy();
-                $count = $(trimmedview.el).find('.message-count');
-                expect(u.isVisible($count[0])).toBeTruthy();
-                expect($count.html()).toBe('2');
-                trimmedview.el.querySelector('.restore-chat').click();
-                expect(trimmed_chatboxes.keys().length).toBe(0);
+                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(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(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(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(7)'))).toBe(false);
+
+                _converse.chatboxes.onMessage($msg({'id': 'aeb213', 'to': _converse.bare_jid})
+                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':moment(base_time).add(4, 'minutes').format()}).up()
+                        .c('message', {
+                            'xmlns': 'jabber:client',
+                            'to': sender_jid,
+                            'from': _converse.bare_jid+"/some-other-resource",
+                            'type': 'chat'})
+                        .c('body').t("A carbon message 4 minutes later")
+                        .tree());
+                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(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(
+                    "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(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(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(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();
                 done();
                 done();
             });
             });
         }));
         }));
 
 
-        it("will indicate when it has a time difference of more than a day between it and its predecessor",
-            mock.initConverseWithPromises(
-                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                function (done, _converse) {
-
-            test_utils.createContacts(_converse, 'current');
-            _converse.emit('rosterContactsFetched');
-            test_utils.openControlBox();
-            spyOn(_converse, 'emit');
-            const contact_name = mock.cur_names[1];
-            const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@localhost';
-            test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
-            .then(() => test_utils.openChatBoxFor(_converse, contact_jid))
-            .then(() => {
-                test_utils.clearChatBoxMessages(_converse, contact_jid);
-                var one_day_ago = moment();
-                one_day_ago.subtract('days', 1);
-                var message = 'This is a day old message';
-                var chatbox = _converse.chatboxes.get(contact_jid);
-                var chatboxview = _converse.chatboxviews.get(contact_jid);
-                var msg_obj;
-                var msg_txt;
-                var sender_txt;
+        describe("when received from someone else", function () {
 
 
-                var msg = $msg({
-                    from: contact_jid,
-                    to: _converse.connection.jid,
-                    type: 'chat',
-                    id: one_day_ago.unix()
-                }).c('body').t(message).up()
-                .c('delay', { xmlns:'urn:xmpp:delay', from: 'localhost', stamp: one_day_ago.format() })
-                .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-                _converse.chatboxes.onMessage(msg);
-                expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
-                expect(chatbox.messages.length).toEqual(1);
-                msg_obj = chatbox.messages.models[0];
-                expect(msg_obj.get('message')).toEqual(message);
-                expect(msg_obj.get('fullname')).toEqual(contact_name);
-                expect(msg_obj.get('sender')).toEqual('them');
-                expect(msg_obj.get('is_delayed')).toEqual(true);
+            it("will open a chatbox and be displayed inside it",
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched'], {},
+                    function (done, _converse) {
 
 
-                return test_utils.waitUntil(() => chatbox.vcard.get('fullname') === 'Candice van der Knijff')
+                test_utils.createContacts(_converse, 'current');
+                test_utils.openControlBox();
+                test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300)
                 .then(function () {
                 .then(function () {
-                    const chat_content = chatboxview.el.querySelector('.chat-content');
-                    expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
-                    expect(chat_content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
-                    expect(chat_content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Candice van der Knijff');
-
-                    expect(chat_content.querySelectorAll('.date-separator').length).toEqual(1);
-                    const day = chat_content.querySelector('.date-separator');
-                    expect(day.getAttribute('class')).toEqual('message date-separator');
-                    expect(day.getAttribute('data-isodate')).toEqual(moment(one_day_ago.startOf('day')).format());
-
-                    const $chat_content = $(chat_content);
-                    var $time = $chat_content.find('time.separator-text');
-                    expect($time.text()).toEqual(moment(one_day_ago.startOf('day')).format("dddd MMM Do YYYY"));
+                    spyOn(_converse, 'emit');
+                    const message = 'This is a received message';
+                    const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
 
 
-                    message = 'This is a current message';
-                    msg = $msg({
-                        from: contact_jid,
-                        to: _converse.connection.jid,
-                        type: 'chat',
-                        id: new Date().getTime()
-                    }).c('body').t(message).up()
-                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-                    _converse.chatboxes.onMessage(msg);
-                    expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
-                    // Check that there is a <time> element, with the required
-                    // props.
-                    expect($chat_content[0].querySelectorAll('time.separator-text').length).toEqual(2); // There are now two time elements
+                    // We don't already have an open chatbox for this user
+                    expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
 
 
-                    var message_date = new Date();
-                    const $day = $chat_content.find('.date-separator:last');
-                    expect($day.length).toEqual(1);
-                    expect($day.attr('class')).toEqual('message date-separator');
-                    expect($day.data('isodate')).toEqual(moment(message_date).startOf('day').format());
+                    _converse.chatboxes.onMessage(
+                        $msg({
+                            'from': sender_jid,
+                            'to': _converse.connection.jid,
+                            'type': 'chat',
+                            'id': (new Date()).getTime()
+                        }).c('body').t(message).up()
+                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
+                    );
 
 
-                    $time = $chat_content.find('time.separator-text:last');
-                    expect($time.text()).toEqual(moment(message_date).startOf('day').format("dddd MMM Do YYYY"));
+                    expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
 
 
-                    // Normal checks for the 2nd message
-                    expect(chatbox.messages.length).toEqual(2);
-                    msg_obj = chatbox.messages.models[1];
+                    // Check that the chatbox and its view now exist
+                    const chatbox = _converse.chatboxes.get(sender_jid);
+                    const chatboxview = _converse.chatboxviews.get(sender_jid);
+                    expect(chatbox).toBeDefined();
+                    expect(chatboxview).toBeDefined();
+                    // Check that the message was received and check the message parameters
+                    expect(chatbox.messages.length).toEqual(1);
+                    const msg_obj = chatbox.messages.models[0];
                     expect(msg_obj.get('message')).toEqual(message);
                     expect(msg_obj.get('message')).toEqual(message);
-                    expect(msg_obj.get('fullname')).toEqual(contact_name);
+                    expect(msg_obj.get('fullname')).toEqual(mock.cur_names[0]);
                     expect(msg_obj.get('sender')).toEqual('them');
                     expect(msg_obj.get('sender')).toEqual('them');
                     expect(msg_obj.get('is_delayed')).toEqual(false);
                     expect(msg_obj.get('is_delayed')).toEqual(false);
-                    msg_txt = $chat_content.find('.chat-msg').last().find('.chat-msg__text').text();
-                    expect(msg_txt).toEqual(message);
-
-                    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('Candice van der Knijff');
-                    done();
+                    // Now check that the message appears inside the chatbox in the DOM
+                    const chat_content = chatboxview.el.querySelector('.chat-content');
+                    expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
+                    expect(chat_content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
+                    return test_utils.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0])
+                    .then(function () {
+                        expect(chat_content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Max Frankfurter');
+                        done();
+                    });
                 });
                 });
-            });
-        }));
+            }));
 
 
-        it("can be sent from a chatbox, and will appear inside it",
+            it("can be replaced with a correction",
                 mock.initConverseWithPromises(
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     function (done, _converse) {
                     function (done, _converse) {
 
 
-            test_utils.createContacts(_converse, 'current');
-            _converse.emit('rosterContactsFetched');
-            test_utils.openControlBox();
-
-            spyOn(_converse, 'emit');
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-            test_utils.openChatBoxFor(_converse, contact_jid)
-            .then(() => {
-                expect(_converse.emit).toHaveBeenCalledWith('chatBoxFocused', jasmine.any(Object));
-                const view = _converse.chatboxviews.get(contact_jid);
-                const message = 'This message is sent from this chatbox';
-                spyOn(view.model, 'sendMessage').and.callThrough();
-                test_utils.sendMessage(view, message);
-                expect(view.model.sendMessage).toHaveBeenCalled();
-                expect(view.model.messages.length, 2);
-                expect(_converse.emit.calls.mostRecent().args, ['messageSend', message]);
-                expect($(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text').text()).toEqual(message);
-                done();
-            });
-        }));
-
-        it("is sanitized to prevent Javascript injection attacks",
-            mock.initConverseWithPromises(
-                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                function (done, _converse) {
-
-            test_utils.createContacts(_converse, 'current');
-            _converse.emit('rosterContactsFetched');
-            test_utils.openControlBox();
-
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-            test_utils.openChatBoxFor(_converse, contact_jid)
-            .then(() => {
-                const view = _converse.chatboxviews.get(contact_jid);
-                const message = '<p>This message contains <em>some</em> <b>markup</b></p>';
-                spyOn(view.model, 'sendMessage').and.callThrough();
-                test_utils.sendMessage(view, message);
-                expect(view.model.sendMessage).toHaveBeenCalled();
-                const msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
-                expect(msg.text()).toEqual(message);
-                expect(msg.html()).toEqual('&lt;p&gt;This message contains &lt;em&gt;some&lt;/em&gt; &lt;b&gt;markup&lt;/b&gt;&lt;/p&gt;');
-                done();
-            });
-        }));
+                test_utils.createContacts(_converse, 'current', 1);
+                _converse.emit('rosterContactsFetched');
+                test_utils.openControlBox();
+                const message = 'This is a received message';
+                const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                test_utils.openChatBoxFor(_converse, sender_jid)
+                .then(() => {
+                    const msg_id = u.getUniqueId();
+                    _converse.chatboxes.onMessage($msg({
+                            'from': sender_jid,
+                            'to': _converse.connection.jid,
+                            'type': 'chat',
+                            'id': msg_id,
+                        }).c('body').t('But soft, what light through yonder airlock breaks?').tree());
 
 
-        it("can contain hyperlinks, which will be clickable",
-            mock.initConverseWithPromises(
-                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                function (done, _converse) {
+                    var chatboxview = _converse.chatboxviews.get(sender_jid);
+                    expect(chatboxview.el.querySelectorAll('.chat-msg').length).toBe(1);
+                    expect(chatboxview.el.querySelector('.chat-msg__text').textContent)
+                        .toBe('But soft, what light through yonder airlock breaks?');
 
 
-            test_utils.createContacts(_converse, 'current');
-            _converse.emit('rosterContactsFetched');
-            test_utils.openControlBox();
+                    _converse.chatboxes.onMessage($msg({
+                            'from': sender_jid,
+                            'to': _converse.connection.jid,
+                            'type': 'chat',
+                            'id': u.getUniqueId(),
+                        }).c('body').t('But soft, what light through yonder chimney breaks?').up()
+                        .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
 
 
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-            test_utils.openChatBoxFor(_converse, contact_jid)
-            .then(() => {
-                const view = _converse.chatboxviews.get(contact_jid);
-                const message = 'This message contains a hyperlink: www.opkode.com';
-                spyOn(view.model, 'sendMessage').and.callThrough();
-                test_utils.sendMessage(view, message);
-                expect(view.model.sendMessage).toHaveBeenCalled();
-                const msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
-                expect(msg.text()).toEqual(message);
-                expect(msg.html()).toEqual('This message contains a hyperlink: <a target="_blank" rel="noopener" href="http://www.opkode.com">www.opkode.com</a>');
-                done();
-            });
-        }));
+                    test_utils.waitUntil(() => chatboxview.el.querySelector('.chat-msg__text').textContent ===
+                        'But soft, what light through yonder chimney breaks?').then(() => {
 
 
-        it("will have properly escaped URLs",
-                mock.initConverseWithPromises(
-                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                    function (done, _converse) {
+                        expect(chatboxview.el.querySelectorAll('.chat-msg').length).toBe(1);
+                        expect(chatboxview.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
 
 
-            test_utils.createContacts(_converse, 'current');
-            _converse.emit('rosterContactsFetched');
-            test_utils.openControlBox();
+                        _converse.chatboxes.onMessage($msg({
+                                'from': sender_jid,
+                                'to': _converse.connection.jid,
+                                'type': 'chat',
+                                'id': u.getUniqueId(),
+                            }).c('body').t('But soft, what light through yonder window breaks?').up()
+                            .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
 
 
-            let message, msg;
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-            test_utils.openChatBoxFor(_converse, contact_jid)
-            .then(() => {
-                var view = _converse.chatboxviews.get(contact_jid);
-                spyOn(view.model, 'sendMessage').and.callThrough();
-                message = "http://www.opkode.com/'onmouseover='alert(1)'whatever";
-                test_utils.sendMessage(view, message);
-                expect(view.model.sendMessage).toHaveBeenCalled();
-                msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
-                expect(msg.text()).toEqual(message);
-                expect(msg.html()).toEqual('<a target="_blank" rel="noopener" href="http://www.opkode.com/%27onmouseover=%27alert%281%29%27whatever">http://www.opkode.com/\'onmouseover=\'alert(1)\'whatever</a>');
-                message = 'http://www.opkode.com/"onmouseover="alert(1)"whatever';
-                test_utils.sendMessage(view, message);
+                        return test_utils.waitUntil(() => chatboxview.el.querySelector('.chat-msg__text').textContent ===
+                            'But soft, what light through yonder window breaks?');
+                    }).then(() => {
+                        expect(chatboxview.el.querySelectorAll('.chat-msg').length).toBe(1);
+                        expect(chatboxview.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
+                        chatboxview.el.querySelector('.chat-msg__content .fa-edit').click();
+                        const modal = chatboxview.model.messages.at(0).message_versions_modal;
+                        return test_utils.waitUntil(() => u.isVisible(modal.el), 1000);
+                    }).then(() => {
+                        const modal = chatboxview.model.messages.at(0).message_versions_modal;
+                        const older_msgs = modal.el.querySelectorAll('.older-msg');
+                        expect(older_msgs.length).toBe(2);
+                        expect(older_msgs[0].textContent).toBe('But soft, what light through yonder airlock breaks?');
+                        expect(older_msgs[1].textContent).toBe('But soft, what light through yonder chimney breaks?');
+                        done();
+                    });
+                });
+            }));
 
 
-                expect(view.model.sendMessage).toHaveBeenCalled();
-                msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
-                expect(msg.text()).toEqual(message);
-                expect(msg.html()).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";
-                test_utils.sendMessage(view, message);
+            describe("when a chatbox is opened for someone who is not in the roster", function () {
 
 
-                expect(view.model.sendMessage).toHaveBeenCalled();
-                msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
-                expect(msg.text()).toEqual(message);
-                expect(msg.html()).toEqual('<a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Ender%27s_Game">'+message+'</a>');
+                it("the VCard for that user is fetched and the chatbox updated with the results",
+                    mock.initConverseWithPromises(
+                        null, ['rosterGroupsFetched'], {},
+                        function (done, _converse) {
 
 
-                message = "https://en.wikipedia.org/wiki/Ender's_Game";
-                test_utils.sendMessage(view, message);
+                    _converse.allow_non_roster_messaging = true;
+                    spyOn(_converse, 'emit').and.callThrough();
 
 
-                expect(view.model.sendMessage).toHaveBeenCalled();
-                msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
-                expect(msg.text()).toEqual(message);
-                expect(msg.html()).toEqual('<a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Ender%27s_Game">'+message+'</a>');
-                done();
-            });
-        }));
+                    var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    var vcard_fetched = false;
+                    spyOn(_converse.api.vcard, "get").and.callFake(function () {
+                        vcard_fetched = true;
+                        return Promise.resolve({
+                            'fullname': mock.cur_names[0],
+                            'vcard_updated': moment().format(),
+                            'jid': sender_jid
+                        });
+                    });
+                    var message = 'This is a received message from someone not on the roster';
+                    var msg = $msg({
+                            from: sender_jid,
+                            to: _converse.connection.jid,
+                            type: 'chat',
+                            id: (new Date()).getTime()
+                        }).c('body').t(message).up()
+                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
 
 
-        it("will render newlines",
-                mock.initConverseWithPromises(null, ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
+                    // We don't already have an open chatbox for this user
+                    expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
 
 
-            test_utils.createContacts(_converse, 'current');
-            _converse.emit('rosterContactsFetched');
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-            test_utils.openChatBoxFor(_converse, contact_jid);
+                    _converse.chatboxes.onMessage(msg);
+                    expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
 
 
-            let stanza = Strophe.xmlHtmlNode(
-                "<message from='"+contact_jid+"'"+
-                "         type='chat'"+
-                "         to='dummy@localhost/resource'>"+
-                "    <body>Hey\nHave you heard the news?</body>"+
-                "</message>").firstChild;
-            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                    // Check that the chatbox and its view now exist
+                    var chatbox = _converse.chatboxes.get(sender_jid);
+                    var chatboxview = _converse.chatboxviews.get(sender_jid);
+                    expect(chatbox).toBeDefined();
+                    expect(chatboxview).toBeDefined();
 
 
-            const view = _converse.chatboxviews.get(contact_jid);
-            const chat_content = view.el.querySelector('.chat-content');
-            expect(chat_content.querySelector('.chat-msg__text').innerHTML).toBe('Hey<br>Have you heard the news?');
+                    var author_el = chatboxview.el.querySelector('.chat-msg__author');
+                    expect(chatbox.get('fullname') === sender_jid);
+                    expect( _.includes(author_el.textContent.trim(), 'max.frankfurter@localhost')).toBeTruthy();
 
 
-            stanza = Strophe.xmlHtmlNode(
-                "<message from='"+contact_jid+"'"+
-                "         type='chat'"+
-                "         to='dummy@localhost/resource'>"+
-                "    <body>Hey\n\n\nHave you heard the news?</body>"+
-                "</message>").firstChild;
-            _converse.connection._dataRecv(test_utils.createRequest(stanza));
-            expect(chat_content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('Hey<br><br>Have you heard the news?');
+                    test_utils.waitUntil(function () { return vcard_fetched; }, 100)
+                    .then(function () {
+                        expect(_converse.api.vcard.get).toHaveBeenCalled();
+                        return test_utils.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0])
+                    }).then(function () {
+                        var author_el = chatboxview.el.querySelector('.chat-msg__author');
+                        expect( _.includes(author_el.textContent.trim(), 'Max Frankfurter')).toBeTruthy();
+                        done();
+                    });
+                }));
+            });
 
 
-            stanza = Strophe.xmlHtmlNode(
-                "<message from='"+contact_jid+"'"+
-                "         type='chat'"+
-                "         to='dummy@localhost/resource'>"+
-                "    <body>Hey\nHave you heard\nthe news?</body>"+
-                "</message>").firstChild;
-            _converse.connection._dataRecv(test_utils.createRequest(stanza));
-            expect(chat_content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('Hey<br>Have you heard<br>the news?');
-            done();
-        }));
 
 
-        it("will render images from their URLs",
-                mock.initConverseWithPromises(
-                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                    function (done, _converse) {
+            describe("who is not on the roster", function () {
 
 
-            test_utils.createContacts(_converse, 'current', 1);
-            _converse.emit('rosterContactsFetched');
-            const base_url = document.URL.split(window.location.pathname)[0];
-            let message = base_url+"/logo/conversejs-filled.svg";
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-            let view;
-            test_utils.openChatBoxFor(_converse, contact_jid)
-            .then(() => {
-                view = _converse.chatboxviews.get(contact_jid);
-                spyOn(view.model, 'sendMessage').and.callThrough();
-                test_utils.sendMessage(view, message);
-                return test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length, 1000)
-            }).then(() => {
-                expect(view.model.sendMessage).toHaveBeenCalled();
-                const msg = $(view.el).find('.chat-content .chat-msg').last().find('.chat-msg__text');
-                expect(msg.html().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"'+
-                    ' src="' + message + '"></a>');
-                message += "?param1=val1&param2=val2";
-                test_utils.sendMessage(view, message);
-                return test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 2, 1000);
-            }).then(() => {
-                expect(view.model.sendMessage).toHaveBeenCalled();
-                const msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
-                expect(msg.html().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'+
-                    ' class="chat-image img-thumbnail" src="'+message.replace(/&/g, '&amp;')+'"></a>')
+                it("will open a chatbox and be displayed inside it if allow_non_roster_messaging is true",
+                    mock.initConverseWithPromises(
+                        null, ['rosterGroupsFetched'], {},
+                        function (done, _converse) {
 
 
-                // Test now with two images in one message
-                message += ' hello world '+base_url+"/logo/conversejs-filled.svg";
-                test_utils.sendMessage(view, message);
-                return test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 4, 1000);
-            }).then(function () {
-                expect(view.model.sendMessage).toHaveBeenCalled();
-                const msg = $(view.el).find('.chat-content').find('.chat-msg').last().find('.chat-msg__text');
-                expect(msg[0].textContent.trim()).toEqual('hello world');
-                expect(msg[0].querySelectorAll('img').length).toEqual(2);
-                done();
-            });
-        }));
+                    _converse.allow_non_roster_messaging = false;
 
 
-        it("will render the message time as configured",
-                mock.initConverseWithPromises(
-                    null, ['rosterGroupsFetched', 'chatBoxesFetched'],
-                    {}, function (done, _converse) {
+                    spyOn(_converse, 'emit');
+                    var message = 'This is a received message from someone not on the roster';
+                    var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    var msg = $msg({
+                            from: sender_jid,
+                            to: _converse.connection.jid,
+                            type: 'chat',
+                            id: (new Date()).getTime()
+                        }).c('body').t(message).up()
+                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
 
 
-            test_utils.createContacts(_converse, 'current');
-            _converse.emit('rosterContactsFetched');
+                    // We don't already have an open chatbox for this user
+                    expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
 
 
-            _converse.time_format = 'hh:mm';
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-            test_utils.openChatBoxFor(_converse, contact_jid)
-            .then(() => {
-                const view = _converse.chatboxviews.get(contact_jid);
-                const message = 'This message is sent from this chatbox';
-                test_utils.sendMessage(view, message);
+                    var chatbox = _converse.chatboxes.get(sender_jid);
+                    expect(chatbox).not.toBeDefined();
 
 
-                const chatbox = _converse.chatboxes.get(contact_jid);
-                expect(chatbox.messages.models.length, 1);
-                const msg_object = chatbox.messages.models[0];
+                    // onMessage is a handler for received XMPP messages
+                    _converse.chatboxes.onMessage(msg);
+                    expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
 
 
-                const msg_author = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__author');
-                expect(msg_author.textContent.trim()).toBe('Max Mustermann');
+                    // onMessage is a handler for received XMPP messages
+                    _converse.allow_non_roster_messaging =true;
+                    _converse.chatboxes.onMessage(msg);
+                    expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
 
 
-                const msg_time = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__time');
-                const time = moment(msg_object.get('time')).format(_converse.time_format);
-                expect(msg_time.textContent).toBe(time);
-                done();
+                    // Check that the chatbox and its view now exist
+                    chatbox = _converse.chatboxes.get(sender_jid);
+                    var chatboxview = _converse.chatboxviews.get(sender_jid);
+                    expect(chatbox).toBeDefined();
+                    expect(chatboxview).toBeDefined();
+                    // Check that the message was received and check the message parameters
+                    expect(chatbox.messages.length).toEqual(1);
+                    var msg_obj = chatbox.messages.models[0];
+                    expect(msg_obj.get('message')).toEqual(message);
+                    expect(msg_obj.get('fullname')).toEqual(undefined);
+                    expect(msg_obj.get('sender')).toEqual('them');
+                    expect(msg_obj.get('is_delayed')).toEqual(false);
+                    // Now check that the message appears inside the chatbox in the DOM
+                    var chat_content = chatboxview.el.querySelector('.chat-content');
+                    expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
+                    expect(chat_content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
+                    expect(chat_content.querySelector('span.chat-msg__author').textContent.trim()).toBe('max.frankfurter@localhost');
+                    done();
+                }));
             });
             });
-        }));
 
 
-        it("will be correctly identified and rendered as a followup message",
-            mock.initConverseWithPromises(
-                null, ['rosterGroupsFetched'], {},
-                function (done, _converse) {
 
 
-            test_utils.createContacts(_converse, 'current');
-            test_utils.openControlBox();
+            describe("and for which then an error message is received from the server", function () {
 
 
-            test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300)
-            .then(function () {
-                const base_time = new Date();
-                const ONE_MINUTE_LATER = 60000;
+                it("will have the error message displayed after itself",
+                    mock.initConverseWithPromises(
+                        null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                        function (done, _converse) {
 
 
-                jasmine.clock().install();
-                jasmine.clock().mockDate(base_time);
+                    test_utils.createContacts(_converse, 'current');
+                    _converse.emit('rosterContactsFetched');
+                    test_utils.openControlBox();
 
 
-                var message, msg;
-                spyOn(_converse, 'log');
-                spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
-                _converse.filter_by_resource = true;
-                var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    // TODO: what could still be done for error
+                    // messages... if the <error> element has type
+                    // "cancel", then we know the messages wasn't sent,
+                    // and can give the user a nicer indication of
+                    // that.
 
 
-                _converse.chatboxes.onMessage($msg({
-                        'from': sender_jid,
-                        'to': _converse.connection.jid,
-                        'type': 'chat',
-                        'id': (new Date()).getTime()
-                    }).c('body').t('A message').up()
-                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+                    /* <message from="scotty@enterprise.com/_converse.js-84843526"
+                     *          to="kirk@enterprise.com.com"
+                     *          type="chat"
+                     *          id="82bc02ce-9651-4336-baf0-fa04762ed8d2"
+                     *          xmlns="jabber:client">
+                     *      <body>yo</body>
+                     *      <active xmlns="http://jabber.org/protocol/chatstates"/>
+                     *  </message>
+                     */
+                    var sender_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    var fullname = _converse.xmppstatus.get('fullname');
+                    fullname = _.isEmpty(fullname)? _converse.bare_jid: fullname;
+                    _converse.api.chats.open(sender_jid)
+                    .then(() => {
+                        var msg_text = 'This message will not be sent, due to an error';
+                        var view = _converse.chatboxviews.get(sender_jid);
+                        var message = view.model.messages.create({
+                            'msgid': '82bc02ce-9651-4336-baf0-fa04762ed8d2',
+                            'fullname': fullname,
+                            'sender': 'me',
+                            'time': moment().format(),
+                            'message': msg_text
+                        });
+                        view.model.sendMessage(message);
+                        var $chat_content = $(view.el).find('.chat-content');
+                        var msg_txt = $chat_content.find('.chat-msg:last').find('.chat-msg__text').text();
+                        expect(msg_txt).toEqual(msg_text);
 
 
-                jasmine.clock().tick(3*ONE_MINUTE_LATER);
+                        // We send another message, for which an error will
+                        // not be received, to test that errors appear
+                        // after the relevant message.
+                        msg_text = 'This message will be sent, and also receive an error';
+                        message = view.model.messages.create({
+                            'msgid': '6fcdeee3-000f-4ce8-a17e-9ce28f0ae104',
+                            'fullname': fullname,
+                            'sender': 'me',
+                            'time': moment().format(),
+                            'message': msg_text
+                        });
+                        view.model.sendMessage(message);
+                        msg_txt = $chat_content.find('.chat-msg:last').find('.chat-msg__text').text();
+                        expect(msg_txt).toEqual(msg_text);
+
+                        /* <message xmlns="jabber:client"
+                         *          to="scotty@enterprise.com/_converse.js-84843526"
+                         *          type="error"
+                         *          id="82bc02ce-9651-4336-baf0-fa04762ed8d2"
+                         *          from="kirk@enterprise.com.com">
+                         *     <error type="cancel">
+                         *         <remote-server-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
+                         *         <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Server-to-server connection failed: Connecting failed: connection timeout</text>
+                         *     </error>
+                         * </message>
+                         */
+                        var error_txt = 'Server-to-server connection failed: Connecting failed: connection timeout';
+                        var stanza = $msg({
+                                'to': _converse.connection.jid,
+                                'type':'error',
+                                'id':'82bc02ce-9651-4336-baf0-fa04762ed8d2',
+                                'from': sender_jid
+                            })
+                            .c('error', {'type': 'cancel'})
+                            .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
+                            .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
+                                .t('Server-to-server connection failed: Connecting failed: connection timeout');
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                        expect($chat_content.find('.chat-error').text()).toEqual(error_txt);
+
+                        stanza = $msg({
+                                'to': _converse.connection.jid,
+                                'type':'error',
+                                'id':'some-other-unused-id',
+                                'from': sender_jid
+                            })
+                            .c('error', {'type': 'cancel'})
+                            .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
+                            .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
+                                .t('Server-to-server connection failed: Connecting failed: connection timeout');
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                        expect($chat_content.find('.chat-error').length).toEqual(2);
 
 
-                _converse.chatboxes.onMessage($msg({
-                        'from': sender_jid,
-                        'to': _converse.connection.jid,
-                        'type': 'chat',
-                        'id': (new Date()).getTime()
-                    }).c('body').t("Another message 3 minutes later").up()
-                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+                        // If the last message is already an error message,
+                        // then we don't render it another time.
+                        stanza = $msg({
+                                'to': _converse.connection.jid,
+                                'type':'error',
+                                'id':'another-unused-id',
+                                'from': sender_jid
+                            })
+                            .c('error', {'type': 'cancel'})
+                            .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
+                            .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
+                                .t('Server-to-server connection failed: Connecting failed: connection timeout');
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                        expect($chat_content.find('.chat-error').length).toEqual(2);
 
 
-                jasmine.clock().tick(11*ONE_MINUTE_LATER);
+                        // A different error message will however render
+                        stanza = $msg({
+                                'to': _converse.connection.jid,
+                                'type':'error',
+                                'id':'another-id',
+                                'from': sender_jid
+                            })
+                            .c('error', {'type': 'cancel'})
+                            .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
+                            .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
+                                .t('Something else went wrong as well');
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                        expect($chat_content.find('.chat-error').length).toEqual(3);
+                        done();
+                    });
+                }));
+            });
 
 
-                _converse.chatboxes.onMessage($msg({
-                        'from': sender_jid,
-                        'to': _converse.connection.jid,
-                        'type': 'chat',
-                        'id': (new Date()).getTime()
-                    }).c('body').t("Another message 14 minutes since we started").up()
-                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
 
 
-                jasmine.clock().tick(1000);
+            it("will cause the chat area to be scrolled down only if it was at the bottom originally",
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    function (done, _converse) {
 
 
-                // Insert <composing> message, to also check that
-                // text messages are inserted correctly with
-                // temporary chat events in the chat contents.
-                _converse.chatboxes.onMessage($msg({
-                        'id': 'aeb219',
-                        'to': _converse.bare_jid,
-                        'xmlns': 'jabber:client',
-                        'from': sender_jid,
-                        'type': 'chat'})
-                    .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
-                    .tree());
+                test_utils.createContacts(_converse, 'current');
+                _converse.emit('rosterContactsFetched');
+                test_utils.openControlBox();
 
 
-                jasmine.clock().tick(1*ONE_MINUTE_LATER);
+                let chatboxview;
+                const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                const message = 'This message is received while the chat area is scrolled up';
+                test_utils.openChatBoxFor(_converse, sender_jid)
+                .then(() => {
+                    chatboxview = _converse.chatboxviews.get(sender_jid);
+                    spyOn(chatboxview, 'onScrolledDown').and.callThrough();
 
 
-                _converse.chatboxes.onMessage($msg({
-                        'from': sender_jid,
-                        'to': _converse.connection.jid,
-                        'type': 'chat',
-                        'id': (new Date()).getTime()
-                    }).c('body').t("Another message 1 minute and 1 second since the previous one").up()
-                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+                    // Create enough messages so that there's a scrollbar.
+                    for (var i=0; i<20; i++) {
+                        _converse.chatboxes.onMessage($msg({
+                                from: sender_jid,
+                                to: _converse.connection.jid,
+                                type: 'chat',
+                                id: (new Date()).getTime()
+                            }).c('body').t('Message: '+i).up()
+                            .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
+                    }
+                    return test_utils.waitUntil(() => chatboxview.content.scrollTop, 1000)
+                .then(() => test_utils.waitUntil(() => !chatboxview.model.get('auto_scrolled'), 500))
+                }).then(() => {
+                    chatboxview.content.scrollTop = 0;
+                    return test_utils.waitUntil(() => chatboxview.model.get('scrolled'), 900);
+                }).then(() => {
+                    _converse.chatboxes.onMessage($msg({
+                            from: sender_jid,
+                            to: _converse.connection.jid,
+                            type: 'chat',
+                            id: (new Date()).getTime()
+                        }).c('body').t(message).up()
+                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
 
 
-                jasmine.clock().tick(1*ONE_MINUTE_LATER);
+                    // Now check that the message appears inside the chatbox in the DOM
+                    const $chat_content = $(chatboxview.el).find('.chat-content');
+                    const  msg_txt = $chat_content.find('.chat-msg:last').find('.chat-msg__text').text();
+                    expect(msg_txt).toEqual(message);
+                    return test_utils.waitUntil(() => u.isVisible(chatboxview.el.querySelector('.new-msgs-indicator')), 900);
+                }).then(() => {
+                    expect(chatboxview.model.get('scrolled')).toBe(true);
+                    expect(chatboxview.content.scrollTop).toBe(0);
+                    expect(u.isVisible(chatboxview.el.querySelector('.new-msgs-indicator'))).toBeTruthy();
+                    // Scroll down again
+                    chatboxview.content.scrollTop = chatboxview.content.scrollHeight;
+                    return test_utils.waitUntil(() => !u.isVisible(chatboxview.el.querySelector('.new-msgs-indicator')), 900);
+                }).then(done);
+            }));
 
 
-                var view = _converse.chatboxviews.get(sender_jid);
-                test_utils.sendMessage(view, "Another message within 10 minutes, but from a different person");
+            it("is ignored if it's intended for a different resource and filter_by_resource is set to true",
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched'], {},
+                    function (done, _converse) {
 
 
-                var chat_content = view.el.querySelector('.chat-content');
-                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(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(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(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");
+                test_utils.createContacts(_converse, 'current');
+                test_utils.openControlBox();
 
 
-                // Let's add a delayed, inbetween message
-                _converse.chatboxes.onMessage($msg({'id': 'aeb218', 'to': _converse.bare_jid})
-                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                        .c('delay', {'xmlns': 'urn:xmpp:delay',
-                                     'stamp': moment(base_time).add(5, 'minutes').format()
-                                    }).up()
-                        .c('message', {
-                            'xmlns': 'jabber:client',
-                            'to': _converse.bare_jid,
-                            'from': sender_jid,
-                            'type': 'chat'})
-                        .c('body').t("A delayed message, sent 5 minutes since we started")
-                        .tree());
+                test_utils.waitUntil(function () {
+                        return $(_converse.rosterview.el).find('.roster-group').length;
+                    }, 300)
+                .then(function () {
+                    // Send a message from a different resource
+                    var message, sender_jid, msg;
+                    spyOn(_converse, 'log');
+                    spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
+                    _converse.filter_by_resource = true;
+                    sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    msg = $msg({
+                            from: sender_jid,
+                            to: _converse.bare_jid+"/some-other-resource",
+                            type: 'chat',
+                            id: (new Date()).getTime()
+                        }).c('body').t("This message will not be shown").up()
+                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
+                    _converse.chatboxes.onMessage(msg);
 
 
-                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(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(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(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(7)'))).toBe(false);
+                    expect(_converse.log).toHaveBeenCalledWith(
+                            "onMessage: Ignoring incoming message intended for a different resource: dummy@localhost/some-other-resource",
+                            Strophe.LogLevel.INFO);
+                    expect(_converse.chatboxes.getChatBox).not.toHaveBeenCalled();
+                    _converse.filter_by_resource = false;
 
 
-                _converse.chatboxes.onMessage($msg({'id': 'aeb213', 'to': _converse.bare_jid})
-                    .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-                        .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':moment(base_time).add(4, 'minutes').format()}).up()
-                        .c('message', {
-                            'xmlns': 'jabber:client',
-                            'to': sender_jid,
-                            'from': _converse.bare_jid+"/some-other-resource",
-                            'type': 'chat'})
-                        .c('body').t("A carbon message 4 minutes later")
-                        .tree());
-                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(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(
-                    "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(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(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(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");
+                    message = "This message sent to a different resource will be shown";
+                    msg = $msg({
+                            from: sender_jid,
+                            to: _converse.bare_jid+"/some-other-resource",
+                            type: 'chat',
+                            id: '134234623462346'
+                        }).c('body').t(message).up()
+                        .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
+                    _converse.chatboxes.onMessage(msg);
 
 
-                jasmine.clock().uninstall();
-                done();
-            });
-        }));
+                    expect(_converse.chatboxes.getChatBox).toHaveBeenCalled();
+                    var chatboxview = _converse.chatboxviews.get(sender_jid);
+                    var $chat_content = $(chatboxview.el).find('.chat-content:last');
+                    var msg_txt = $chat_content.find('.chat-msg').find('.chat-msg__text').text();
+                    expect(msg_txt).toEqual(message);
+                    done();
+                });
+            }));
+        });
 
 
 
 
         describe("which contains an OOB URL", function () {
         describe("which contains an OOB URL", function () {