浏览代码

Make the message view's `render` method async

So that we first render dynamic content (e.g. images) before inserting
it into the chat.

Also, add the `show_images_inline` setting (which is the cause of this
whole change).

Updated tests to handle this new change and start using async/await
instead of promise callbacks.
JC Brand 6 年之前
父节点
当前提交
e181aaf99b
共有 16 个文件被更改,包括 9679 次插入13086 次删除
  1. 7712 11064
      dist/converse.js
  2. 1 3
      package-lock.json
  3. 1 1
      package.json
  4. 103 126
      spec/chatbox.js
  5. 852 868
      spec/chatroom.js
  6. 250 268
      spec/http-file-upload.js
  7. 451 449
      spec/messages.js
  8. 91 98
      spec/roomslist.js
  9. 147 160
      spec/spoilers.js
  10. 2 2
      src/converse-chatboxes.js
  11. 20 15
      src/converse-chatboxviews.js
  12. 7 2
      src/converse-chatview.js
  13. 34 25
      src/converse-message-view.js
  14. 1 0
      src/converse-muc-views.js
  15. 1 1
      src/utils/core.js
  16. 6 4
      tests/utils.js

文件差异内容过多而无法显示
+ 7712 - 11064
dist/converse.js


+ 1 - 3
package-lock.json

@@ -2252,9 +2252,7 @@
       }
       }
     },
     },
     "backbone.browserStorage": {
     "backbone.browserStorage": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/backbone.browserStorage/-/backbone.browserStorage-0.0.3.tgz",
-      "integrity": "sha1-ikIi8I2bHQslLR14/1CUuNCKc2s=",
+      "version": "github:jcbrand/Backbone.browserStorage#7079bf7bf9a43474da1d48e31e3cda6c4a716382",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
         "backbone": "1.3.3",
         "backbone": "1.3.3",

+ 1 - 1
package.json

@@ -40,7 +40,7 @@
     "awesomplete-avoid-xss": "^1.1.2",
     "awesomplete-avoid-xss": "^1.1.2",
     "babel-loader": "^8.0.0-beta.3",
     "babel-loader": "^8.0.0-beta.3",
     "backbone": "1.3.3",
     "backbone": "1.3.3",
-    "backbone.browserStorage": "0.0.3",
+    "backbone.browserStorage": "jcbrand/Backbone.browserStorage#78e4a58f7cee10cad6af4fd5f3213331a396aa5a",
     "backbone.nativeview": "^0.3.3",
     "backbone.nativeview": "^0.3.3",
     "backbone.overview": "1.0.2",
     "backbone.overview": "1.0.2",
     "backbone.vdomview": "1.0.1",
     "backbone.vdomview": "1.0.1",

+ 103 - 126
spec/chatbox.js

@@ -58,61 +58,49 @@
             it("supports the /me command",
             it("supports the /me command",
                 mock.initConverseWithPromises(
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
                     null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
 
-                var view;
                 test_utils.createContacts(_converse, 'current');
                 test_utils.createContacts(_converse, 'current');
-                test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp'])
-                .then(() => test_utils.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')), 300)
-                .then(function () {
-                    test_utils.openControlBox();
-                    expect(_converse.chatboxes.length).toEqual(1);
-                    var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-                    var message = '/me is tired';
-                    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);
-                    view = _converse.chatboxviews.get(sender_jid);
+                await test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp']);
+                await test_utils.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
+                await test_utils.openControlBox();
+                expect(_converse.chatboxes.length).toEqual(1);
+                const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                let message = '/me is tired';
+                const 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.waitUntil(function () {
-                        return u.isVisible(view.el);
-                    }).then(function () {
-                        expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1);
-                        expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Max Frankfurter')).toBeTruthy();
-                        expect($(view.el).find('.chat-msg__text').text()).toBe(' is tired');
-
-                        message = '/me is as well';
-                        test_utils.sendMessage(view, message);
-                        expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2);
-
-                        return test_utils.waitUntil(() => $(view.el).find('.chat-msg__author:last').text().trim() === '**Max Mustermann');
-                    }).then(function () {
-                        expect($(view.el).find('.chat-msg__text:last').text()).toBe(' is as well');
-                        expect($(view.el).find('.chat-msg:last').hasClass('chat-msg--followup')).toBe(false);
-
-                        // Check that /me messages after a normal message don't
-                        // get the 'chat-msg--followup' class.
-                        message = 'This a normal message';
-                        test_utils.sendMessage(view, message);
-                        let message_el = view.el.querySelector('.message:last-child');
-                        expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
-
-                        message = '/me wrote a 3rd person message';
-                        test_utils.sendMessage(view, message);
-                        message_el = view.el.querySelector('.message:last-child');
-                        expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3);
-                        expect($(view.el).find('.chat-msg__text:last').text()).toBe(' wrote a 3rd person message');
-                        expect($(view.el).find('.chat-msg__author:last').is(':visible')).toBeTruthy();
-                        expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
-                        done();
-                    });
-                });
+                _converse.chatboxes.onMessage(msg);
+                const view = _converse.chatboxviews.get(sender_jid);
+                await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1);
+                expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Max Frankfurter')).toBeTruthy();
+                expect(view.el.querySelector('.chat-msg__text').textContent).toBe(' is tired');
+                message = '/me is as well';
+                await test_utils.sendMessage(view, message);
+                expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2);
+                await test_utils.waitUntil(() => $(view.el).find('.chat-msg__author:last').text().trim() === '**Max Mustermann');
+                expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe(' is as well');
+                expect($(view.el).find('.chat-msg:last').hasClass('chat-msg--followup')).toBe(false);
+                // Check that /me messages after a normal message don't
+                // get the 'chat-msg--followup' class.
+                message = 'This a normal message';
+                await test_utils.sendMessage(view, message);
+                let message_el = view.el.querySelector('.message:last-child');
+                expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
+                message = '/me wrote a 3rd person message';
+                await test_utils.sendMessage(view, message);
+                message_el = view.el.querySelector('.message:last-child');
+                expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3);
+                expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe(' wrote a 3rd person message');
+                expect(u.isVisible(sizzle('.chat-msg__author:last', view.el).pop())).toBeTruthy();
+                expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
+                done();
             }));
             }));
 
 
             it("is created when you click on a roster item", mock.initConverseWithPromises(
             it("is created when you click on a roster item", mock.initConverseWithPromises(
@@ -684,7 +672,7 @@
                     it("will be shown if received",
                     it("will be shown if received",
                         mock.initConverseWithPromises(
                         mock.initConverseWithPromises(
                             null, ['rosterGroupsFetched'], {},
                             null, ['rosterGroupsFetched'], {},
-                            function (done, _converse) {
+                            async function (done, _converse) {
 
 
                         test_utils.createContacts(_converse, 'current');
                         test_utils.createContacts(_converse, 'current');
                         test_utils.openControlBox();
                         test_utils.openControlBox();
@@ -705,28 +693,27 @@
                         expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                         expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                         var view = _converse.chatboxviews.get(sender_jid);
                         var view = _converse.chatboxviews.get(sender_jid);
                         expect(view).toBeDefined();
                         expect(view).toBeDefined();
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
 
-                        test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
-                        .then(function () {
-                            var view = _converse.chatboxviews.get(sender_jid);
-                            // Check that the notification appears inside the chatbox in the DOM
-                            let events = view.el.querySelectorAll('.chat-state-notification');
-                            expect(events.length).toBe(1);
-                            expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
+                        await test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
+                        // Check that the notification appears inside the chatbox in the DOM
+                        let events = view.el.querySelectorAll('.chat-state-notification');
+                        expect(events.length).toBe(1);
+                        expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
 
 
-                            // Check that it doesn't appear twice
-                            msg = $msg({
-                                    from: sender_jid,
-                                    to: _converse.connection.jid,
-                                    type: 'chat',
-                                    id: (new Date()).getTime()
-                                }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                            _converse.chatboxes.onMessage(msg);
-                            events = view.el.querySelectorAll('.chat-state-notification');
-                            expect(events.length).toBe(1);
-                            expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
-                            done();
-                        })
+                        // Check that it doesn't appear twice
+                        msg = $msg({
+                                from: sender_jid,
+                                to: _converse.connection.jid,
+                                type: 'chat',
+                                id: (new Date()).getTime()
+                            }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+                        _converse.chatboxes.onMessage(msg);
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        events = view.el.querySelectorAll('.chat-state-notification');
+                        expect(events.length).toBe(1);
+                        expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
+                        done();
                     }));
                     }));
 
 
                     it("can be a composing carbon message that this user sent from a different client",
                     it("can be a composing carbon message that this user sent from a different client",
@@ -841,38 +828,32 @@
                     }));
                     }));
 
 
                     it("will be shown if received",
                     it("will be shown if received",
-                mock.initConverseWithPromises(
-                    null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                            mock.initConverseWithPromises(
+                                null, ['rosterGroupsFetched'], {},
+                                async function (done, _converse) {
 
 
                         test_utils.createContacts(_converse, 'current');
                         test_utils.createContacts(_converse, 'current');
                         test_utils.openControlBox();
                         test_utils.openControlBox();
-                        test_utils.waitUntil(function () {
-                                return $(_converse.rosterview.el).find('.roster-group').length;
-                            }, 300)
-                        .then(function () {
-                            // TODO: only show paused state if the previous state was composing
-                            // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
-                            spyOn(_converse, 'emit');
-                            var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
-                            // <paused> state
-                            var msg = $msg({
-                                    from: sender_jid,
-                                    to: _converse.connection.jid,
-                                    type: 'chat',
-                                    id: (new Date()).getTime()
-                                }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                            _converse.chatboxes.onMessage(msg);
-                            expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
-                            var view = _converse.chatboxviews.get(sender_jid);
-                            test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
-                            .then(function () {
-                                var view = _converse.chatboxviews.get(sender_jid);
-                                var event = view.el.querySelector('.chat-info.chat-state-notification');
-                                expect(event.textContent).toEqual(mock.cur_names[1] + ' has stopped typing');
-                                done();
-                            });
-                        });
+                        await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                        // TODO: only show paused state if the previous state was composing
+                        // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
+                        spyOn(_converse, 'emit');
+                        const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        // <paused> state
+                        var msg = $msg({
+                                from: sender_jid,
+                                to: _converse.connection.jid,
+                                type: 'chat',
+                                id: (new Date()).getTime()
+                            }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+                        _converse.chatboxes.onMessage(msg);
+                        expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
+                        const view = _converse.chatboxviews.get(sender_jid);
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        await test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
+                        var event = view.el.querySelector('.chat-info.chat-state-notification');
+                        expect(event.textContent).toEqual(mock.cur_names[1] + ' has stopped typing');
+                        done();
                     }));
                     }));
 
 
                     it("can be a paused carbon message that this user sent from a different client",
                     it("can be a paused carbon message that this user sent from a different client",
@@ -1242,14 +1223,11 @@
             it("is incremented from zero when chatbox was closed after viewing previously received messages and the window is not focused now",
             it("is incremented from zero when chatbox was closed after viewing previously received messages and the window is not focused now",
                 mock.initConverseWithPromises(
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
                     null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
 
                 test_utils.createContacts(_converse, 'current');
                 test_utils.createContacts(_converse, 'current');
-
                 // initial state
                 // initial state
                 expect(_converse.msg_counter).toBe(0);
                 expect(_converse.msg_counter).toBe(0);
-
-                let view;
                 const message = 'This message will always increment the message counter from zero',
                 const message = 'This message will always increment the message counter from zero',
                     sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
                     sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
                     msgFactory = function () {
                     msgFactory = function () {
@@ -1267,29 +1245,28 @@
                 // leave converse-chat page
                 // leave converse-chat page
                 _converse.windowState = 'hidden';
                 _converse.windowState = 'hidden';
                 _converse.chatboxes.onMessage(msgFactory());
                 _converse.chatboxes.onMessage(msgFactory());
-                return test_utils.waitUntil(() => _converse.api.chats.get().length)
-                .then(() => {
-                    expect(_converse.msg_counter).toBe(1);
+                await test_utils.waitUntil(() => _converse.api.chats.get().length)
+                let view = _converse.chatboxviews.get(sender_jid);
+                await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                expect(_converse.msg_counter).toBe(1);
 
 
-                    // come back to converse-chat page
-                    _converse.saveWindowState(null, 'focus');
-                    view = _converse.chatboxviews.get(sender_jid);
-                    expect(u.isVisible(view.el)).toBeTruthy();
-                    expect(_converse.msg_counter).toBe(0);
+                // come back to converse-chat page
+                _converse.saveWindowState(null, 'focus');
+                expect(u.isVisible(view.el)).toBeTruthy();
+                expect(_converse.msg_counter).toBe(0);
 
 
-                    // close chatbox and leave converse-chat page again
-                    view.close();
-                    _converse.windowState = 'hidden';
+                // close chatbox and leave converse-chat page again
+                view.close();
+                _converse.windowState = 'hidden';
 
 
-                    // check that msg_counter is incremented from zero again
-                    _converse.chatboxes.onMessage(msgFactory());
-                    return test_utils.waitUntil(() => _converse.api.chats.get().length)
-                }).then(() => {
-                    view = _converse.chatboxviews.get(sender_jid);
-                    expect(u.isVisible(view.el)).toBeTruthy();
-                    expect(_converse.msg_counter).toBe(1);
-                    done();
-                });
+                // check that msg_counter is incremented from zero again
+                _converse.chatboxes.onMessage(msgFactory());
+                await test_utils.waitUntil(() => _converse.api.chats.get().length)
+                view = _converse.chatboxviews.get(sender_jid);
+                await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                expect(u.isVisible(view.el)).toBeTruthy();
+                expect(_converse.msg_counter).toBe(1);
+                done();
             }));
             }));
         });
         });
 
 

文件差异内容过多而无法显示
+ 852 - 868
spec/chatroom.js


+ 250 - 268
spec/http-file-upload.js

@@ -268,224 +268,210 @@
 
 
                 describe("when clicked and a file chosen", function () {
                 describe("when clicked and a file chosen", function () {
 
 
-                    it("is uploaded and sent out", mock.initConverseWithAsync(function (done, _converse) {
-                        test_utils.waitUntilDiscoConfirmed(
+                    it("is uploaded and sent out", mock.initConverseWithAsync(
+                        async function (done, _converse) {
+
+                        await test_utils.waitUntilDiscoConfirmed(
                             _converse, _converse.domain,
                             _converse, _converse.domain,
                             [{'category': 'server', 'type':'IM'}],
                             [{'category': 'server', 'type':'IM'}],
-                            ['http://jabber.org/protocol/disco#items'], [], 'info').then(function () {
+                            ['http://jabber.org/protocol/disco#items'], [], 'info');
 
 
-                            var send_backup = XMLHttpRequest.prototype.send;
-                            var IQ_stanzas = _converse.connection.IQ_stanzas;
-                            let contact_jid;
+                        const send_backup = XMLHttpRequest.prototype.send;
+                        const IQ_stanzas = _converse.connection.IQ_stanzas;
+
+                        await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
+                        await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
+                        test_utils.createContacts(_converse, 'current');
+                        _converse.emit('rosterContactsFetched');
+                        const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        await test_utils.openChatBoxFor(_converse, contact_jid);
+                        const view = _converse.chatboxviews.get(contact_jid);
+                        const file = {
+                            'type': 'image/jpeg',
+                            'size': '23456' ,
+                            'lastModifiedDate': "",
+                            'name': "my-juliet.jpg"
+                        };
+                        view.model.sendFiles([file]);
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+
+                        await test_utils.waitUntil(() => _.filter(IQ_stanzas, iq => iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request')).length);
+                        var iq = IQ_stanzas.pop();
+                        expect(iq.toLocaleString()).toBe(
+                            `<iq from="dummy@localhost/resource" `+
+                                `id="${iq.nodeTree.getAttribute("id")}" `+
+                                `to="upload.montague.tld" `+
+                                `type="get" `+
+                                `xmlns="jabber:client">`+
+                            `<request `+
+                                `content-type="image/jpeg" `+
+                                `filename="my-juliet.jpg" `+
+                                `size="23456" `+
+                                `xmlns="urn:xmpp:http:upload:0"/>`+
+                            `</iq>`);
 
 
-                           test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items')
-                            .then(() => test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []))
+                        var base_url = document.URL.split(window.location.pathname)[0];
+                        var message = base_url+"/logo/conversejs-filled.svg";
+
+                        var stanza = Strophe.xmlHtmlNode(
+                            "<iq from='upload.montague.tld'"+
+                            "    id='"+iq.nodeTree.getAttribute('id')+"'"+
+                            "    to='dummy@localhost/resource'"+
+                            "    type='result'>"+
+                            "<slot xmlns='urn:xmpp:http:upload:0'>"+
+                            "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
+                            "    <header name='Authorization'>Basic Base64String==</header>"+
+                            "    <header name='Cookie'>foo=bar; user=romeo</header>"+
+                            "    </put>"+
+                            "    <get url='"+message+"' />"+
+                            "</slot>"+
+                            "</iq>").firstElementChild;
+
+                        spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
+                            const message = view.model.messages.at(0);
+                            expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
+                            message.set('progress', 0.5);
+                            test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
                             .then(() => {
                             .then(() => {
-                                test_utils.createContacts(_converse, 'current');
-                                _converse.emit('rosterContactsFetched');
-                                contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
-                                return test_utils.openChatBoxFor(_converse, contact_jid);
+                                message.set('progress', 1);
+                                test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1')
                             }).then(() => {
                             }).then(() => {
-                                var view = _converse.chatboxviews.get(contact_jid);
-                                var file = {
-                                    'type': 'image/jpeg',
-                                    'size': '23456' ,
-                                    'lastModifiedDate': "",
-                                    'name': "my-juliet.jpg"
-                                };
-                                view.model.sendFiles([file]);
-                                return test_utils.waitUntil(function () {
-                                    return _.filter(IQ_stanzas, function (iq) {
-                                        return iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request');
-                                    }).length > 0;
-                                }).then(function () {
-                                    var iq = IQ_stanzas.pop();
-                                    expect(iq.toLocaleString()).toBe(
-                                        `<iq from="dummy@localhost/resource" `+
-                                            `id="${iq.nodeTree.getAttribute("id")}" `+
-                                            `to="upload.montague.tld" `+
-                                            `type="get" `+
-                                            `xmlns="jabber:client">`+
-                                        `<request `+
-                                            `content-type="image/jpeg" `+
-                                            `filename="my-juliet.jpg" `+
-                                            `size="23456" `+
-                                            `xmlns="urn:xmpp:http:upload:0"/>`+
-                                        `</iq>`);
-
-                                    var base_url = document.URL.split(window.location.pathname)[0];
-                                    var message = base_url+"/logo/conversejs-filled.svg";
-
-                                    var stanza = Strophe.xmlHtmlNode(
-                                        "<iq from='upload.montague.tld'"+
-                                        "    id='"+iq.nodeTree.getAttribute('id')+"'"+
-                                        "    to='dummy@localhost/resource'"+
-                                        "    type='result'>"+
-                                        "<slot xmlns='urn:xmpp:http:upload:0'>"+
-                                        "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
-                                        "    <header name='Authorization'>Basic Base64String==</header>"+
-                                        "    <header name='Cookie'>foo=bar; user=romeo</header>"+
-                                        "    </put>"+
-                                        "    <get url='"+message+"' />"+
-                                        "</slot>"+
-                                        "</iq>").firstElementChild;
-
-                                    spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
-                                        const message = view.model.messages.at(0);
-                                        expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
-                                        message.set('progress', 0.5);
-                                        expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0.5');
-                                        message.set('progress', 1);
-                                        expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('1');
-                                        message.save({
-                                            'upload': _converse.SUCCESS,
-                                            'oob_url': message.get('get'),
-                                            'message': message.get('get')
-                                        });
-                                    });
-                                    var sent_stanza;
-                                    spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
-                                        sent_stanza = stanza;
-                                    });
-                                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                                    return test_utils.waitUntil(function () {
-                                        return sent_stanza;
-                                    }, 1000).then(function () {
-                                        expect(sent_stanza.toLocaleString()).toBe(
-                                            `<message from="dummy@localhost/resource" `+
-                                                `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
-                                                `to="irini.vlastuin@localhost" `+
-                                                `type="chat" `+
-                                                `xmlns="jabber:client">`+
-                                                    `<body>${message}</body>`+
-                                                    `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
-                                                    `<x xmlns="jabber:x:oob">`+
-                                                        `<url>${message}</url>`+
-                                                    `</x>`+
-                                            `</message>`);
-                                        return test_utils.waitUntil(() => view.el.querySelector('.chat-image'), 1000);
-                                    }).then(function () {
-                                        // Check that the image renders
-                                        expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual(
-                                            `<!-- src/templates/image.html -->\n`+
-                                            `<a href="${window.location.origin}/logo/conversejs-filled.svg" target="_blank" rel="noopener">`+
-                                                `<img class="chat-image img-thumbnail" src="${window.location.origin}/logo/conversejs-filled.svg">`+
-                                            `</a>`);
-                                        XMLHttpRequest.prototype.send = send_backup;
-                                        done();
-                                    });
+                                message.save({
+                                    'upload': _converse.SUCCESS,
+                                    'oob_url': message.get('get'),
+                                    'message': message.get('get')
                                 });
                                 });
+                                return new Promise((resolve, reject) => view.model.messages.once('rendered', resolve));
                             });
                             });
                         });
                         });
+                        let sent_stanza;
+                        spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza));
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                        await test_utils.waitUntil(() => sent_stanza, 1000);
+                        expect(sent_stanza.toLocaleString()).toBe(
+                            `<message from="dummy@localhost/resource" `+
+                                `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
+                                `to="irini.vlastuin@localhost" `+
+                                `type="chat" `+
+                                `xmlns="jabber:client">`+
+                                    `<body>${message}</body>`+
+                                    `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
+                                    `<x xmlns="jabber:x:oob">`+
+                                        `<url>${message}</url>`+
+                                    `</x>`+
+                            `</message>`);
+                        await test_utils.waitUntil(() => view.el.querySelector('.chat-image'), 1000);
+                        // Check that the image renders
+                        expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual(
+                            `<!-- src/templates/image.html -->\n`+
+                            `<a href="${window.location.origin}/logo/conversejs-filled.svg" target="_blank" rel="noopener">`+
+                                `<img class="chat-image img-thumbnail" src="${window.location.origin}/logo/conversejs-filled.svg">`+
+                            `</a>`);
+                        XMLHttpRequest.prototype.send = send_backup;
+                        done();
                     }));
                     }));
 
 
-                    it("is uploaded and sent out from a groupchat", mock.initConverseWithAsync(function (done, _converse) {
-                        test_utils.waitUntilDiscoConfirmed(
+                    it("is uploaded and sent out from a groupchat", mock.initConverseWithAsync(
+                        async function (done, _converse) {
+
+                        await test_utils.waitUntilDiscoConfirmed(
                             _converse, _converse.domain,
                             _converse, _converse.domain,
                             [{'category': 'server', 'type':'IM'}],
                             [{'category': 'server', 'type':'IM'}],
-                            ['http://jabber.org/protocol/disco#items'], [], 'info').then(function () {
+                            ['http://jabber.org/protocol/disco#items'], [], 'info');
+
+                        const send_backup = XMLHttpRequest.prototype.send;
+                        const IQ_stanzas = _converse.connection.IQ_stanzas;
 
 
-                            var send_backup = XMLHttpRequest.prototype.send;
-                            var IQ_stanzas = _converse.connection.IQ_stanzas;
-
-                            test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items').then(function () {
-                                test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []).then(function () {
-                                    test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy').then(function () {
-                                        var view = _converse.chatboxviews.get('lounge@localhost');
-                                        var file = {
-                                            'type': 'image/jpeg',
-                                            'size': '23456' ,
-                                            'lastModifiedDate': "",
-                                            'name': "my-juliet.jpg"
-                                        };
-                                        view.model.sendFiles([file]);
-
-                                        return test_utils.waitUntil(function () {
-                                            return _.filter(IQ_stanzas, function (iq) {
-                                                return iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request');
-                                            }).length > 0;
-                                        }).then(function () {
-                                            var iq = IQ_stanzas.pop();
-                                            expect(iq.toLocaleString()).toBe(
-                                                `<iq from="dummy@localhost/resource" `+
-                                                    `id="${iq.nodeTree.getAttribute("id")}" `+
-                                                    `to="upload.montague.tld" `+
-                                                    `type="get" `+
-                                                    `xmlns="jabber:client">`+
-                                                `<request `+
-                                                    `content-type="image/jpeg" `+
-                                                    `filename="my-juliet.jpg" `+
-                                                    `size="23456" `+
-                                                    `xmlns="urn:xmpp:http:upload:0"/>`+
-                                                `</iq>`);
-
-                                            var base_url = document.URL.split(window.location.pathname)[0];
-                                            var message = base_url+"/logo/conversejs-filled.svg";
-
-                                            var stanza = Strophe.xmlHtmlNode(
-                                                "<iq from='upload.montague.tld'"+
-                                                "    id='"+iq.nodeTree.getAttribute('id')+"'"+
-                                                "    to='dummy@localhost/resource'"+
-                                                "    type='result'>"+
-                                                "<slot xmlns='urn:xmpp:http:upload:0'>"+
-                                                "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
-                                                "    <header name='Authorization'>Basic Base64String==</header>"+
-                                                "    <header name='Cookie'>foo=bar; user=romeo</header>"+
-                                                "    </put>"+
-                                                "    <get url='"+message+"' />"+
-                                                "</slot>"+
-                                                "</iq>").firstElementChild;
-
-                                            spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
-                                                const message = view.model.messages.at(0);
-                                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
-                                                message.set('progress', 0.5);
-                                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0.5');
-                                                message.set('progress', 1);
-                                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('1');
-                                                message.save({
-                                                    'upload': _converse.SUCCESS,
-                                                    'oob_url': message.get('get'),
-                                                    'message': message.get('get')
-                                                });
-                                            });
-                                            var sent_stanza;
-                                            spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
-                                                sent_stanza = stanza;
-                                            });
-                                            _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                                            return test_utils.waitUntil(() => sent_stanza, 1000).then(function () {
-                                                expect(sent_stanza.toLocaleString()).toBe(
-                                                    `<message `+
-                                                        `from="dummy@localhost/resource" `+
-                                                        `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
-                                                        `to="lounge@localhost" `+
-                                                        `type="groupchat" `+
-                                                        `xmlns="jabber:client">`+
-                                                            `<body>${message}</body>`+
-                                                            `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
-                                                            `<x xmlns="jabber:x:oob">`+
-                                                                `<url>${message}</url>`+
-                                                            `</x>`+
-                                                    `</message>`);
-                                                return test_utils.waitUntil(() => view.el.querySelector('.chat-image'), 1000);
-                                            }).then(function () {
-                                                // Check that the image renders
-                                                expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual(
-                                                    `<!-- src/templates/image.html -->\n`+
-                                                    `<a href="${window.location.origin}/logo/conversejs-filled.svg" target="_blank" rel="noopener">`+
-                                                        `<img class="chat-image img-thumbnail" src="${window.location.origin}/logo/conversejs-filled.svg">`+
-                                                    `</a>`);
-                                                XMLHttpRequest.prototype.send = send_backup;
-                                                done();
-                                            });
-                                        });
-                                    });
+                        await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
+                        await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
+                        await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy');
+                        const view = _converse.chatboxviews.get('lounge@localhost');
+                        const file = {
+                            'type': 'image/jpeg',
+                            'size': '23456' ,
+                            'lastModifiedDate': "",
+                            'name': "my-juliet.jpg"
+                        };
+                        view.model.sendFiles([file]);
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+
+                        await test_utils.waitUntil(() => _.filter(IQ_stanzas, iq => iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request')).length);
+                        var iq = IQ_stanzas.pop();
+                        expect(iq.toLocaleString()).toBe(
+                            `<iq from="dummy@localhost/resource" `+
+                                `id="${iq.nodeTree.getAttribute("id")}" `+
+                                `to="upload.montague.tld" `+
+                                `type="get" `+
+                                `xmlns="jabber:client">`+
+                            `<request `+
+                                `content-type="image/jpeg" `+
+                                `filename="my-juliet.jpg" `+
+                                `size="23456" `+
+                                `xmlns="urn:xmpp:http:upload:0"/>`+
+                            `</iq>`);
+
+                        var base_url = document.URL.split(window.location.pathname)[0];
+                        var message = base_url+"/logo/conversejs-filled.svg";
+
+                        var stanza = Strophe.xmlHtmlNode(
+                            "<iq from='upload.montague.tld'"+
+                            "    id='"+iq.nodeTree.getAttribute('id')+"'"+
+                            "    to='dummy@localhost/resource'"+
+                            "    type='result'>"+
+                            "<slot xmlns='urn:xmpp:http:upload:0'>"+
+                            "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
+                            "    <header name='Authorization'>Basic Base64String==</header>"+
+                            "    <header name='Cookie'>foo=bar; user=romeo</header>"+
+                            "    </put>"+
+                            "    <get url='"+message+"' />"+
+                            "</slot>"+
+                            "</iq>").firstElementChild;
+
+                        spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
+                            const message = view.model.messages.at(0);
+                            expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
+                            message.set('progress', 0.5);
+                            test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
+                            .then(() => {
+                                message.set('progress', 1);
+                                test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1')
+                            }).then(() => {
+                                message.save({
+                                    'upload': _converse.SUCCESS,
+                                    'oob_url': message.get('get'),
+                                    'message': message.get('get')
                                 });
                                 });
+                                return new Promise((resolve, reject) => view.model.messages.once('rendered', resolve));
                             });
                             });
                         });
                         });
+                        let sent_stanza;
+                        spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza));
+                        _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                        await test_utils.waitUntil(() => sent_stanza, 1000);
+                        expect(sent_stanza.toLocaleString()).toBe(
+                            `<message `+
+                                `from="dummy@localhost/resource" `+
+                                `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
+                                `to="lounge@localhost" `+
+                                `type="groupchat" `+
+                                `xmlns="jabber:client">`+
+                                    `<body>${message}</body>`+
+                                    `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
+                                    `<x xmlns="jabber:x:oob">`+
+                                        `<url>${message}</url>`+
+                                    `</x>`+
+                            `</message>`);
+                        await test_utils.waitUntil(() => view.el.querySelector('.chat-image'), 1000);
+                        // Check that the image renders
+                        expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual(
+                            `<!-- src/templates/image.html -->\n`+
+                            `<a href="${window.location.origin}/logo/conversejs-filled.svg" target="_blank" rel="noopener">`+
+                                `<img class="chat-image img-thumbnail" src="${window.location.origin}/logo/conversejs-filled.svg">`+
+                            `</a>`);
+                        XMLHttpRequest.prototype.send = send_backup;
+                        done();
                     }));
                     }));
 
 
                     it("shows an error message if the file is too large", mock.initConverseWithAsync(function (done, _converse) {
                     it("shows an error message if the file is too large", mock.initConverseWithAsync(function (done, _converse) {
@@ -617,82 +603,78 @@
             describe("While a file is being uploaded", function () {
             describe("While a file is being uploaded", function () {
 
 
                 it("shows a progress bar", mock.initConverseWithPromises(
                 it("shows a progress bar", mock.initConverseWithPromises(
-                            null, ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
+                    null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                    async function (done, _converse) {
 
 
-                    test_utils.waitUntilDiscoConfirmed(
+                    await test_utils.waitUntilDiscoConfirmed(
                         _converse, _converse.domain,
                         _converse, _converse.domain,
                         [{'category': 'server', 'type':'IM'}],
                         [{'category': 'server', 'type':'IM'}],
-                        ['http://jabber.org/protocol/disco#items'], [], 'info').then(function () {
+                        ['http://jabber.org/protocol/disco#items'], [], 'info');
 
 
-                        var send_backup = XMLHttpRequest.prototype.send;
-                        var IQ_stanzas = _converse.connection.IQ_stanzas;
-                        let view, contact_jid;
+                    const send_backup = XMLHttpRequest.prototype.send;
+                    const IQ_stanzas = _converse.connection.IQ_stanzas;
 
 
-                        test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items')
-                        .then(() => test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []))
+                    await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
+                    await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
+                    test_utils.createContacts(_converse, 'current');
+                    _converse.emit('rosterContactsFetched');
+                    const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    await test_utils.openChatBoxFor(_converse, contact_jid);
+                    const view = _converse.chatboxviews.get(contact_jid);
+                    const file = {
+                        'type': 'image/jpeg',
+                        'size': '23456' ,
+                        'lastModifiedDate': "",
+                        'name': "my-juliet.jpg"
+                    };
+                    view.model.sendFiles([file]);
+                    await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                    await test_utils.waitUntil(() => _.filter(IQ_stanzas, (iq) => iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request')).length)
+                    const iq = IQ_stanzas.pop();
+                    expect(iq.toLocaleString()).toBe(
+                        `<iq from="dummy@localhost/resource" `+
+                            `id="${iq.nodeTree.getAttribute("id")}" `+
+                            `to="upload.montague.tld" `+
+                            `type="get" `+
+                            `xmlns="jabber:client">`+
+                        `<request `+
+                            `content-type="image/jpeg" `+
+                            `filename="my-juliet.jpg" `+
+                            `size="23456" `+
+                            `xmlns="urn:xmpp:http:upload:0"/>`+
+                        `</iq>`);
+
+                    const base_url = document.URL.split(window.location.pathname)[0];
+                    const message = base_url+"/logo/conversejs-filled.svg";
+                    const stanza = Strophe.xmlHtmlNode(
+                        "<iq from='upload.montague.tld'"+
+                        "    id='"+iq.nodeTree.getAttribute('id')+"'"+
+                        "    to='dummy@localhost/resource'"+
+                        "    type='result'>"+
+                        "<slot xmlns='urn:xmpp:http:upload:0'>"+
+                        "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
+                        "    <header name='Authorization'>Basic Base64String==</header>"+
+                        "    <header name='Cookie'>foo=bar; user=romeo</header>"+
+                        "    </put>"+
+                        "    <get url='"+message+"' />"+
+                        "</slot>"+
+                        "</iq>").firstElementChild;
+                    spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
+                        const message = view.model.messages.at(0);
+                        expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
+                        message.set('progress', 0.5);
+                        test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
                         .then(() => {
                         .then(() => {
-                            test_utils.createContacts(_converse, 'current');
-                            _converse.emit('rosterContactsFetched');
-
-                            contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
-                            return test_utils.openChatBoxFor(_converse, contact_jid);
+                            message.set('progress', 1);
+                            test_utils.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1')
                         }).then(() => {
                         }).then(() => {
-                            view = _converse.chatboxviews.get(contact_jid);
-                            const file = {
-                                'type': 'image/jpeg',
-                                'size': '23456' ,
-                                'lastModifiedDate': "",
-                                'name': "my-juliet.jpg"
-                            };
-                            view.model.sendFiles([file]);
-                            return test_utils.waitUntil(() => _.filter(IQ_stanzas, (iq) => iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request')).length)
-                        }).then(function () {
-                            const iq = IQ_stanzas.pop();
-                            expect(iq.toLocaleString()).toBe(
-                                `<iq from="dummy@localhost/resource" `+
-                                    `id="${iq.nodeTree.getAttribute("id")}" `+
-                                    `to="upload.montague.tld" `+
-                                    `type="get" `+
-                                    `xmlns="jabber:client">`+
-                                `<request `+
-                                    `content-type="image/jpeg" `+
-                                    `filename="my-juliet.jpg" `+
-                                    `size="23456" `+
-                                    `xmlns="urn:xmpp:http:upload:0"/>`+
-                                `</iq>`);
-
-                            const base_url = document.URL.split(window.location.pathname)[0];
-                            const message = base_url+"/logo/conversejs-filled.svg";
-                            const stanza = Strophe.xmlHtmlNode(
-                                "<iq from='upload.montague.tld'"+
-                                "    id='"+iq.nodeTree.getAttribute('id')+"'"+
-                                "    to='dummy@localhost/resource'"+
-                                "    type='result'>"+
-                                "<slot xmlns='urn:xmpp:http:upload:0'>"+
-                                "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
-                                "    <header name='Authorization'>Basic Base64String==</header>"+
-                                "    <header name='Cookie'>foo=bar; user=romeo</header>"+
-                                "    </put>"+
-                                "    <get url='"+message+"' />"+
-                                "</slot>"+
-                                "</iq>").firstElementChild;
-                            spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
-                                const message = view.model.messages.at(0);
-                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
-                                message.set('progress', 0.5);
-                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0.5');
-                                message.set('progress', 1);
-                                expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('1');
-                                expect(view.el.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB');
-                                done();
-                            });
-                            var sent_stanza;
-                            spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
-                                sent_stanza = stanza;
-                            });
-                            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                            expect(view.el.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB');
+                            done();
                         });
                         });
                     });
                     });
+                    let sent_stanza;
+                    spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza));
+                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
                 }));
                 }));
             });
             });
         });
         });

文件差异内容过多而无法显示
+ 451 - 449
spec/messages.js


+ 91 - 98
spec/roomslist.js

@@ -95,113 +95,106 @@
             { whitelisted_plugins: ['converse-roomslist'],
             { whitelisted_plugins: ['converse-roomslist'],
               allow_bookmarks: false // Makes testing easier, otherwise we
               allow_bookmarks: false // Makes testing easier, otherwise we
                                      // have to mock stanza traffic.
                                      // have to mock stanza traffic.
-            }, function (done, _converse) {
+            }, async function (done, _converse) {
 
 
-            let view;
             const IQ_stanzas = _converse.connection.IQ_stanzas;
             const IQ_stanzas = _converse.connection.IQ_stanzas;
             const room_jid = 'coven@chat.shakespeare.lit';
             const room_jid = 'coven@chat.shakespeare.lit';
             test_utils.openControlBox();
             test_utils.openControlBox();
-            _converse.api.rooms.open(room_jid, {'nick': 'some1'})
-            .then(() => {
-                return test_utils.waitUntil(() => _.get(_.filter(
-                    IQ_stanzas,
-                    iq => iq.nodeTree.querySelector(
-                        `iq[to="${room_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
-                    )).pop(), 'nodeTree'));
-            }).then(last_stanza => {
-                view = _converse.chatboxviews.get(room_jid);
-                const IQ_id = last_stanza.getAttribute('id');
-                const features_stanza = $iq({
-                        'from': 'coven@chat.shakespeare.lit',
-                        'id': IQ_id,
-                        'to': 'dummy@localhost/desktop',
-                        'type': 'result'
-                    })
-                    .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
-                        .c('identity', {
-                            'category': 'conference',
-                            'name': 'A Dark Cave',
-                            'type': 'text'
-                        }).up()
-                        .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up()
-                        .c('feature', {'var': 'muc_passwordprotected'}).up()
-                        .c('feature', {'var': 'muc_hidden'}).up()
-                        .c('feature', {'var': 'muc_temporary'}).up()
-                        .c('feature', {'var': 'muc_open'}).up()
-                        .c('feature', {'var': 'muc_unmoderated'}).up()
-                        .c('feature', {'var': 'muc_nonanonymous'}).up()
-                        .c('feature', {'var': 'urn:xmpp:mam:0'}).up()
-                        .c('x', { 'xmlns':'jabber:x:data', 'type':'result'})
-                            .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
-                                .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up()
-                            .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'})
-                                .c('value').t('This is the description').up().up()
-                            .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'})
-                                .c('value').t(0);
-                _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
-                return test_utils.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING)
-            }).then(function () {
-                var presence = $pres({
-                        to: _converse.connection.jid,
-                        from: 'coven@chat.shakespeare.lit/some1',
-                        id: 'DC352437-C019-40EC-B590-AF29E879AF97'
-                }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
-                    .c('item').attrs({
-                        affiliation: 'member',
-                        jid: _converse.bare_jid,
-                        role: 'participant'
+            await _converse.api.rooms.open(room_jid, {'nick': 'some1'});
+            const last_stanza = await test_utils.waitUntil(() => _.get(_.filter(
+                IQ_stanzas,
+                iq => iq.nodeTree.querySelector(
+                    `iq[to="${room_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
+                )).pop(), 'nodeTree'));
+            const view = _converse.chatboxviews.get(room_jid);
+            const IQ_id = last_stanza.getAttribute('id');
+            const features_stanza = $iq({
+                    'from': 'coven@chat.shakespeare.lit',
+                    'id': IQ_id,
+                    'to': 'dummy@localhost/desktop',
+                    'type': 'result'
+                })
+                .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'})
+                    .c('identity', {
+                        'category': 'conference',
+                        'name': 'A Dark Cave',
+                        'type': 'text'
                     }).up()
                     }).up()
-                    .c('status').attrs({code:'110'});
-                _converse.connection._dataRecv(test_utils.createRequest(presence));
+                    .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up()
+                    .c('feature', {'var': 'muc_passwordprotected'}).up()
+                    .c('feature', {'var': 'muc_hidden'}).up()
+                    .c('feature', {'var': 'muc_temporary'}).up()
+                    .c('feature', {'var': 'muc_open'}).up()
+                    .c('feature', {'var': 'muc_unmoderated'}).up()
+                    .c('feature', {'var': 'muc_nonanonymous'}).up()
+                    .c('feature', {'var': 'urn:xmpp:mam:0'}).up()
+                    .c('x', { 'xmlns':'jabber:x:data', 'type':'result'})
+                        .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
+                            .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up()
+                        .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'})
+                            .c('value').t('This is the description').up().up()
+                        .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'})
+                            .c('value').t(0);
+            _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
+            await test_utils.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING)
+            let presence = $pres({
+                    to: _converse.connection.jid,
+                    from: 'coven@chat.shakespeare.lit/some1',
+                    id: 'DC352437-C019-40EC-B590-AF29E879AF97'
+            }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
+                .c('item').attrs({
+                    affiliation: 'member',
+                    jid: _converse.bare_jid,
+                    role: 'participant'
+                }).up()
+                .c('status').attrs({code:'110'});
+            _converse.connection._dataRecv(test_utils.createRequest(presence));
 
 
-                const room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
-                expect(room_els.length).toBe(1);
-                const info_el = _converse.rooms_list_view.el.querySelector(".room-info");
-                info_el.click();
+            const room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
+            expect(room_els.length).toBe(1);
+            const info_el = _converse.rooms_list_view.el.querySelector(".room-info");
+            info_el.click();
 
 
-                const modal = view.model.room_details_modal;
-                return test_utils.waitUntil(() => u.isVisible(modal.el), 2000);
-            }).then(() => {
-                const modal = view.model.room_details_modal;
-                let els = modal.el.querySelectorAll('p.room-info');
-                expect(els[0].textContent).toBe("Name: A Dark Cave")
-                expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit")
-                expect(els[2].textContent).toBe("Description: This is the description")
-                expect(els[3].textContent).toBe("Online users: 1")
-                const features_list = modal.el.querySelector('.features-list');
-                expect(features_list.textContent.replace(/(\n|\s{2,})/g, '')).toBe(
-                    'Password protected - This groupchat requires a password before entry'+
-                    'Hidden - This groupchat is not publicly searchable'+
-                    'Open - Anyone can join this groupchat'+
-                    'Temporary - This groupchat will disappear once the last person leaves'+
-                    'Not anonymous - All other groupchat participants can see your XMPP username'+
-                    'Not moderated - Participants entering this groupchat can write right away'
-                );
-                const presence = $pres({
-                        to: 'dummy@localhost/_converse.js-29092160',
-                        from: 'coven@chat.shakespeare.lit/newguy'
-                    })
-                    .c('x', {xmlns: Strophe.NS.MUC_USER})
-                    .c('item', {
-                        'affiliation': 'none',
-                        'jid': 'newguy@localhost/_converse.js-290929789',
-                        'role': 'participant'
-                    });
-                _converse.connection._dataRecv(test_utils.createRequest(presence));
+            const  modal = view.model.room_details_modal;
+            await test_utils.waitUntil(() => u.isVisible(modal.el), 2000);
+            let els = modal.el.querySelectorAll('p.room-info');
+            expect(els[0].textContent).toBe("Name: A Dark Cave")
+            expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit")
+            expect(els[2].textContent).toBe("Description: This is the description")
+            expect(els[3].textContent).toBe("Online users: 1")
+            const features_list = modal.el.querySelector('.features-list');
+            expect(features_list.textContent.replace(/(\n|\s{2,})/g, '')).toBe(
+                'Password protected - This groupchat requires a password before entry'+
+                'Hidden - This groupchat is not publicly searchable'+
+                'Open - Anyone can join this groupchat'+
+                'Temporary - This groupchat will disappear once the last person leaves'+
+                'Not anonymous - All other groupchat participants can see your XMPP username'+
+                'Not moderated - Participants entering this groupchat can write right away'
+            );
+            presence = $pres({
+                    to: 'dummy@localhost/_converse.js-29092160',
+                    from: 'coven@chat.shakespeare.lit/newguy'
+                })
+                .c('x', {xmlns: Strophe.NS.MUC_USER})
+                .c('item', {
+                    'affiliation': 'none',
+                    'jid': 'newguy@localhost/_converse.js-290929789',
+                    'role': 'participant'
+                });
+            _converse.connection._dataRecv(test_utils.createRequest(presence));
 
 
-                els = modal.el.querySelectorAll('p.room-info');
-                expect(els[3].textContent).toBe("Online users: 2")
+            els = modal.el.querySelectorAll('p.room-info');
+            expect(els[3].textContent).toBe("Online users: 2")
 
 
-                view.model.set({'subject': {'author': 'someone', 'text': 'Hatching dark plots'}});
-                els = modal.el.querySelectorAll('p.room-info');
-                expect(els[0].textContent).toBe("Name: A Dark Cave")
-                expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit")
-                expect(els[2].textContent).toBe("Description: This is the description")
-                expect(els[3].textContent).toBe("Topic: Hatching dark plots")
-                expect(els[4].textContent).toBe("Topic author: someone")
-                expect(els[5].textContent).toBe("Online users: 2")
-                done();
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+            view.model.set({'subject': {'author': 'someone', 'text': 'Hatching dark plots'}});
+            els = modal.el.querySelectorAll('p.room-info');
+            expect(els[0].textContent).toBe("Name: A Dark Cave")
+            expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit")
+            expect(els[2].textContent).toBe("Description: This is the description")
+            expect(els[3].textContent).toBe("Topic: Hatching dark plots")
+            expect(els[4].textContent).toBe("Topic author: someone")
+            expect(els[5].textContent).toBe("Online users: 2")
+            done();
         }));
         }));
 
 
         it("can be closed", mock.initConverseWithPromises(
         it("can be closed", mock.initConverseWithPromises(

+ 147 - 160
spec/spoilers.js

@@ -1,30 +1,30 @@
 (function (root, factory) {
 (function (root, factory) {
     define(["jasmine", "mock", "test-utils"], factory);
     define(["jasmine", "mock", "test-utils"], factory);
 } (this, function (jasmine, mock, test_utils) {
 } (this, function (jasmine, mock, test_utils) {
-    var _ = converse.env._;
-    var Strophe = converse.env.Strophe;
-    var $msg = converse.env.$msg;
-    var $pres = converse.env.$pres;
-    var u = converse.env.utils;
+    const _ = converse.env._;
+    const Strophe = converse.env.Strophe;
+    const $msg = converse.env.$msg;
+    const $pres = converse.env.$pres;
+    const u = converse.env.utils;
 
 
-    return describe("A spoiler message", function () {
+    describe("A spoiler message", function () {
 
 
         it("can be received with a hint",
         it("can be received with a hint",
             mock.initConverseWithPromises(
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched'], {},
                 null, ['rosterGroupsFetched'], {},
-                function (done, _converse) {
+                async function (done, _converse) {
 
 
             test_utils.createContacts(_converse, 'current');
             test_utils.createContacts(_converse, 'current');
-            var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
 
 
             /* <message to='romeo@montague.net/orchard' from='juliet@capulet.net/balcony' id='spoiler2'>
             /* <message to='romeo@montague.net/orchard' from='juliet@capulet.net/balcony' id='spoiler2'>
              *      <body>And at the end of the story, both of them die! It is so tragic!</body>
              *      <body>And at the end of the story, both of them die! It is so tragic!</body>
              *      <spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>
              *      <spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>
              *  </message>
              *  </message>
              */
              */
-            var spoiler_hint = "Love story end"
-            var spoiler = "And at the end of the story, both of them die! It is so tragic!";
-            var msg = $msg({
+            const spoiler_hint = "Love story end"
+            const spoiler = "And at the end of the story, both of them die! It is so tragic!";
+            const msg = $msg({
                     'xmlns': 'jabber:client',
                     'xmlns': 'jabber:client',
                     'to': _converse.bare_jid,
                     'to': _converse.bare_jid,
                     'from': sender_jid,
                     'from': sender_jid,
@@ -36,36 +36,30 @@
                 .tree();
                 .tree();
             _converse.chatboxes.onMessage(msg);
             _converse.chatboxes.onMessage(msg);
 
 
-            var view = _converse.chatboxviews.get(sender_jid);
-
-            return test_utils.waitUntil(() => view.model.vcard.get('fullname') === 'Max Frankfurter')
-            .then(function () {
-                expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Frankfurter');
-
-                var message_content = view.el.querySelector('.chat-msg__text');
-                expect(message_content.textContent).toBe(spoiler);
-
-                var spoiler_hint_el = view.el.querySelector('.spoiler-hint');
-                expect(spoiler_hint_el.textContent).toBe(spoiler_hint);
-                done();
-            });
+            const view = _converse.chatboxviews.get(sender_jid);
+            await test_utils.waitUntil(() => view.model.vcard.get('fullname') === 'Max Frankfurter')
+            expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Frankfurter');
+            const message_content = view.el.querySelector('.chat-msg__text');
+            expect(message_content.textContent).toBe(spoiler);
+            const spoiler_hint_el = view.el.querySelector('.spoiler-hint');
+            expect(spoiler_hint_el.textContent).toBe(spoiler_hint);
+            done();
         }));
         }));
 
 
         it("can be received without a hint",
         it("can be received without a hint",
             mock.initConverseWithPromises(
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched'], {},
                 null, ['rosterGroupsFetched'], {},
-                function (done, _converse) {
+                async function (done, _converse) {
 
 
             test_utils.createContacts(_converse, 'current');
             test_utils.createContacts(_converse, 'current');
-            var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
-
+            const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             /* <message to='romeo@montague.net/orchard' from='juliet@capulet.net/balcony' id='spoiler2'>
             /* <message to='romeo@montague.net/orchard' from='juliet@capulet.net/balcony' id='spoiler2'>
              *      <body>And at the end of the story, both of them die! It is so tragic!</body>
              *      <body>And at the end of the story, both of them die! It is so tragic!</body>
              *      <spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>
              *      <spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>
              *  </message>
              *  </message>
              */
              */
-            var spoiler = "And at the end of the story, both of them die! It is so tragic!";
-            var msg = $msg({
+            const spoiler = "And at the end of the story, both of them die! It is so tragic!";
+            const msg = $msg({
                     'xmlns': 'jabber:client',
                     'xmlns': 'jabber:client',
                     'to': _converse.bare_jid,
                     'to': _converse.bare_jid,
                     'from': sender_jid,
                     'from': sender_jid,
@@ -75,25 +69,20 @@
                       'xmlns': 'urn:xmpp:spoiler:0',
                       'xmlns': 'urn:xmpp:spoiler:0',
                     }).tree();
                     }).tree();
             _converse.chatboxes.onMessage(msg);
             _converse.chatboxes.onMessage(msg);
-
-            var view = _converse.chatboxviews.get(sender_jid);
-            return test_utils.waitUntil(() => view.model.vcard.get('fullname') === 'Max Frankfurter')
-            .then(function () {
-                expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, 'Max Frankfurter')).toBeTruthy();
-
-                var message_content = view.el.querySelector('.chat-msg__text');
-                expect(message_content.textContent).toBe(spoiler);
-
-                var spoiler_hint_el = view.el.querySelector('.spoiler-hint');
-                expect(spoiler_hint_el.textContent).toBe('');
-                done();
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+            const view = _converse.chatboxviews.get(sender_jid);
+            await test_utils.waitUntil(() => view.model.vcard.get('fullname') === 'Max Frankfurter')
+            expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, 'Max Frankfurter')).toBeTruthy();
+            const message_content = view.el.querySelector('.chat-msg__text');
+            expect(message_content.textContent).toBe(spoiler);
+            const spoiler_hint_el = view.el.querySelector('.spoiler-hint');
+            expect(spoiler_hint_el.textContent).toBe('');
+            done();
         }));
         }));
 
 
         it("can be sent without a hint",
         it("can be sent without a hint",
             mock.initConverseWithPromises(
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                function (done, _converse) {
+                async function (done, _converse) {
 
 
             test_utils.createContacts(_converse, 'current', 1);
             test_utils.createContacts(_converse, 'current', 1);
             _converse.emit('rosterContactsFetched');
             _converse.emit('rosterContactsFetched');
@@ -105,147 +94,145 @@
             // have a resource, that resource is then queried to see
             // have a resource, that resource is then queried to see
             // whether Strophe.NS.SPOILER is supported, in which case
             // whether Strophe.NS.SPOILER is supported, in which case
             // the spoiler button will appear.
             // the spoiler button will appear.
-            var presence = $pres({
+            const presence = $pres({
                 'from': contact_jid+'/phone',
                 'from': contact_jid+'/phone',
                 'to': 'dummy@localhost'
                 'to': 'dummy@localhost'
             });
             });
             _converse.connection._dataRecv(test_utils.createRequest(presence));
             _converse.connection._dataRecv(test_utils.createRequest(presence));
-            test_utils.openChatBoxFor(_converse, contact_jid)
-            .then(() => test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]))
-            .then(() => {
-                var view = _converse.chatboxviews.get(contact_jid);
-                spyOn(view, 'onMessageSubmitted').and.callThrough();
-                spyOn(_converse.connection, 'send');
-
-                var spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
-                spoiler_toggle.click();
-
-                var textarea = view.el.querySelector('.chat-textarea');
-                textarea.value = 'This is the spoiler';
-                view.keyPressed({
-                    target: textarea,
-                    preventDefault: _.noop,
-                    keyCode: 13
-                });
-                expect(view.onMessageSubmitted).toHaveBeenCalled();
-
-                /* Test the XML stanza 
-                 *
-                 * <message from="dummy@localhost/resource"
-                 *          to="max.frankfurter@localhost"
-                 *          type="chat"
-                 *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
-                 *          xmlns="jabber:client">
-                 *    <body>This is the spoiler</body>
-                 *    <active xmlns="http://jabber.org/protocol/chatstates"/>
-                 *    <spoiler xmlns="urn:xmpp:spoiler:0"/>
-                 * </message>"
-                 */
-                var stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
-                var spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]');
-                expect(_.isNull(spoiler_el)).toBeFalsy();
-                expect(spoiler_el.textContent).toBe('');
-
-                var body_el = stanza.querySelector('body');
-                expect(body_el.textContent).toBe('This is the spoiler');
-
-                /* Test the HTML spoiler message */
-                expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Mustermann');
-
-                var spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
-                expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
-
-                spoiler_toggle = view.el.querySelector('.spoiler-toggle');
-                expect(spoiler_toggle.textContent).toBe('Show more');
-                spoiler_toggle.click();
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
-                expect(spoiler_toggle.textContent).toBe('Show less');
-                spoiler_toggle.click();
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
-                done();
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+            await test_utils.openChatBoxFor(_converse, contact_jid);
+            await test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]);
+            const view = _converse.chatboxviews.get(contact_jid);
+            spyOn(view, 'onMessageSubmitted').and.callThrough();
+            spyOn(_converse.connection, 'send');
+
+            let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+            spoiler_toggle.click();
+
+            const textarea = view.el.querySelector('.chat-textarea');
+            textarea.value = 'This is the spoiler';
+            view.keyPressed({
+                target: textarea,
+                preventDefault: _.noop,
+                keyCode: 13
+            });
+            expect(view.onMessageSubmitted).toHaveBeenCalled();
+            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+
+            /* Test the XML stanza 
+             *
+             * <message from="dummy@localhost/resource"
+             *          to="max.frankfurter@localhost"
+             *          type="chat"
+             *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
+             *          xmlns="jabber:client">
+             *    <body>This is the spoiler</body>
+             *    <active xmlns="http://jabber.org/protocol/chatstates"/>
+             *    <spoiler xmlns="urn:xmpp:spoiler:0"/>
+             * </message>"
+             */
+            const stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
+            const spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]');
+            expect(_.isNull(spoiler_el)).toBeFalsy();
+            expect(spoiler_el.textContent).toBe('');
+
+            const body_el = stanza.querySelector('body');
+            expect(body_el.textContent).toBe('This is the spoiler');
+
+            /* Test the HTML spoiler message */
+            expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Mustermann');
+
+            const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
+            expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+
+            spoiler_toggle = view.el.querySelector('.spoiler-toggle');
+            expect(spoiler_toggle.textContent).toBe('Show more');
+            spoiler_toggle.click();
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
+            expect(spoiler_toggle.textContent).toBe('Show less');
+            spoiler_toggle.click();
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+            done();
         }));
         }));
 
 
         it("can be sent with a hint",
         it("can be sent with a hint",
             mock.initConverseWithPromises(
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                function (done, _converse) {
+                async function (done, _converse) {
 
 
             test_utils.createContacts(_converse, 'current', 1);
             test_utils.createContacts(_converse, 'current', 1);
             _converse.emit('rosterContactsFetched');
             _converse.emit('rosterContactsFetched');
 
 
             test_utils.openControlBox();
             test_utils.openControlBox();
-            var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
 
 
             // XXX: We need to send a presence from the contact, so that we
             // XXX: We need to send a presence from the contact, so that we
             // have a resource, that resource is then queried to see
             // have a resource, that resource is then queried to see
             // whether Strophe.NS.SPOILER is supported, in which case
             // whether Strophe.NS.SPOILER is supported, in which case
             // the spoiler button will appear.
             // the spoiler button will appear.
-            var presence = $pres({
+            const presence = $pres({
                 'from': contact_jid+'/phone',
                 'from': contact_jid+'/phone',
                 'to': 'dummy@localhost'
                 'to': 'dummy@localhost'
             });
             });
             _converse.connection._dataRecv(test_utils.createRequest(presence));
             _converse.connection._dataRecv(test_utils.createRequest(presence));
-            test_utils.openChatBoxFor(_converse, contact_jid)
-            .then(() => test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]))
-            .then(() => {
-                var view = _converse.chatboxviews.get(contact_jid);
-                var spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
-                spoiler_toggle.click();
-
-                spyOn(view, 'onMessageSubmitted').and.callThrough();
-                spyOn(_converse.connection, 'send');
-
-                var textarea = view.el.querySelector('.chat-textarea');
-                textarea.value = 'This is the spoiler';
-                var hint_input = view.el.querySelector('.spoiler-hint');
-                hint_input.value = 'This is the hint';
-
-                view.keyPressed({
-                    target: textarea,
-                    preventDefault: _.noop,
-                    keyCode: 13
-                });
-                expect(view.onMessageSubmitted).toHaveBeenCalled();
-
-                /* Test the XML stanza 
-                 *
-                 * <message from="dummy@localhost/resource"
-                 *          to="max.frankfurter@localhost"
-                 *          type="chat"
-                 *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
-                 *          xmlns="jabber:client">
-                 *    <body>This is the spoiler</body>
-                 *    <active xmlns="http://jabber.org/protocol/chatstates"/>
-                 *    <spoiler xmlns="urn:xmpp:spoiler:0">This is the hint</spoiler>
-                 * </message>"
-                 */
-                var stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
-                var spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]');
-
-                expect(_.isNull(spoiler_el)).toBeFalsy();
-                expect(spoiler_el.textContent).toBe('This is the hint');
-
-                var body_el = stanza.querySelector('body');
-                expect(body_el.textContent).toBe('This is the spoiler');
-
-                /* Test the HTML spoiler message */
-                expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Mustermann');
-
-                var spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
-                expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
-
-                spoiler_toggle = view.el.querySelector('.spoiler-toggle');
-                expect(spoiler_toggle.textContent).toBe('Show more');
-                spoiler_toggle.click();
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
-                expect(spoiler_toggle.textContent).toBe('Show less');
-                spoiler_toggle.click();
-                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
-                done();
+            await test_utils.openChatBoxFor(_converse, contact_jid);
+            await test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]);
+            const view = _converse.chatboxviews.get(contact_jid);
+            let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+            spoiler_toggle.click();
+
+            spyOn(view, 'onMessageSubmitted').and.callThrough();
+            spyOn(_converse.connection, 'send');
+
+            const textarea = view.el.querySelector('.chat-textarea');
+            textarea.value = 'This is the spoiler';
+            const hint_input = view.el.querySelector('.spoiler-hint');
+            hint_input.value = 'This is the hint';
+
+            view.keyPressed({
+                target: textarea,
+                preventDefault: _.noop,
+                keyCode: 13
             });
             });
+            expect(view.onMessageSubmitted).toHaveBeenCalled();
+            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+
+            /* Test the XML stanza 
+             *
+             * <message from="dummy@localhost/resource"
+             *          to="max.frankfurter@localhost"
+             *          type="chat"
+             *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
+             *          xmlns="jabber:client">
+             *    <body>This is the spoiler</body>
+             *    <active xmlns="http://jabber.org/protocol/chatstates"/>
+             *    <spoiler xmlns="urn:xmpp:spoiler:0">This is the hint</spoiler>
+             * </message>"
+             */
+            const stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
+            const spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]');
+
+            expect(_.isNull(spoiler_el)).toBeFalsy();
+            expect(spoiler_el.textContent).toBe('This is the hint');
+
+            const body_el = stanza.querySelector('body');
+            expect(body_el.textContent).toBe('This is the spoiler');
+
+            /* Test the HTML spoiler message */
+            expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Max Mustermann');
+
+            const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
+            expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+
+            spoiler_toggle = view.el.querySelector('.spoiler-toggle');
+            expect(spoiler_toggle.textContent).toBe('Show more');
+            spoiler_toggle.click();
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
+            expect(spoiler_toggle.textContent).toBe('Show less');
+            spoiler_toggle.click();
+            expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+            done();
         }));
         }));
     });
     });
 }));
 }));

+ 2 - 2
src/converse-chatboxes.js

@@ -297,7 +297,7 @@
                             'message': _converse.chatboxes.getMessageBody(stanza),
                             'message': _converse.chatboxes.getMessageBody(stanza),
                             'references': this.getReferencesFromStanza(stanza),
                             'references': this.getReferencesFromStanza(stanza),
                             'older_versions': older_versions,
                             'older_versions': older_versions,
-                            'edited': true
+                            'edited': moment().format()
                         });
                         });
                         return true;
                         return true;
                     }
                     }
@@ -395,7 +395,7 @@
                         older_versions.push(message.get('message'));
                         older_versions.push(message.get('message'));
                         message.save({
                         message.save({
                             'correcting': false,
                             'correcting': false,
-                            'edited': true,
+                            'edited': moment().format(),
                             'message': attrs.message,
                             'message': attrs.message,
                             'older_versions': older_versions,
                             'older_versions': older_versions,
                             'references': attrs.references
                             'references': attrs.references

+ 20 - 15
src/converse-chatboxviews.js

@@ -17,8 +17,10 @@
     const { Backbone, _ } = converse.env;
     const { Backbone, _ } = converse.env;
 
 
     const AvatarMixin = {
     const AvatarMixin = {
-        renderAvatar () {
-            const canvas_el = this.el.querySelector('canvas');
+
+        renderAvatar (el) {
+            el = el || this.el;
+            const canvas_el = el.querySelector('canvas');
             if (_.isNull(canvas_el)) {
             if (_.isNull(canvas_el)) {
                 return;
                 return;
             }
             }
@@ -27,19 +29,22 @@
                     img_src = "data:" + image_type + ";base64," + image,
                     img_src = "data:" + image_type + ";base64," + image,
                     img = new Image();
                     img = new Image();
 
 
-            img.onload = () => {
-                const ctx = canvas_el.getContext('2d'),
-                        ratio = img.width / img.height;
-                ctx.clearRect(0, 0, canvas_el.width, canvas_el.height);
-                if (ratio < 1) {
-                    const scaled_img_with = canvas_el.width*ratio,
-                            x = Math.floor((canvas_el.width-scaled_img_with)/2);
-                    ctx.drawImage(img, x, 0, scaled_img_with, canvas_el.height);
-                } else {
-                    ctx.drawImage(img, 0, 0, canvas_el.width, canvas_el.height*ratio);
-                }
-            };
-            img.src = img_src;
+            return new Promise((resolve, reject) => {
+                img.onload = () => {
+                    const ctx = canvas_el.getContext('2d'),
+                            ratio = img.width / img.height;
+                    ctx.clearRect(0, 0, canvas_el.width, canvas_el.height);
+                    if (ratio < 1) {
+                        const scaled_img_with = canvas_el.width*ratio,
+                                x = Math.floor((canvas_el.width-scaled_img_with)/2);
+                        ctx.drawImage(img, x, 0, scaled_img_with, canvas_el.height);
+                    } else {
+                        ctx.drawImage(img, 0, 0, canvas_el.width, canvas_el.height*ratio);
+                    }
+                    resolve();
+                };
+                img.src = img_src;
+            });
         },
         },
     };
     };
 
 

+ 7 - 2
src/converse-chatview.js

@@ -312,6 +312,7 @@
 
 
                     this.model.messages.on('add', this.onMessageAdded, this);
                     this.model.messages.on('add', this.onMessageAdded, this);
                     this.model.messages.on('rendered', this.scrollDown, this);
                     this.model.messages.on('rendered', this.scrollDown, this);
+                    this.model.messages.on('edited', (view) => this.markFollowups(view.el));
 
 
                     this.model.on('show', this.show, this);
                     this.model.on('show', this.show, this);
                     this.model.on('destroy', this.remove, this);
                     this.model.on('destroy', this.remove, this);
@@ -673,7 +674,8 @@
                     if (view.model.get('type') === 'error') {
                     if (view.model.get('type') === 'error') {
                         const previous_msg_el = this.content.querySelector(`[data-msgid="${view.model.get('msgid')}"]`);
                         const previous_msg_el = this.content.querySelector(`[data-msgid="${view.model.get('msgid')}"]`);
                         if (previous_msg_el) {
                         if (previous_msg_el) {
-                            return previous_msg_el.insertAdjacentElement('afterend', view.el);
+                            previous_msg_el.insertAdjacentElement('afterend', view.el);
+                            return this.trigger('messageInserted', view.el);
                         }
                         }
                     }
                     }
                     const current_msg_date = moment(view.model.get('time')) || moment,
                     const current_msg_date = moment(view.model.get('time')) || moment,
@@ -692,6 +694,7 @@
                         previous_msg_el.insertAdjacentElement('afterend', view.el);
                         previous_msg_el.insertAdjacentElement('afterend', view.el);
                         this.markFollowups(view.el);
                         this.markFollowups(view.el);
                     }
                     }
+                    return this.trigger('messageInserted', view.el);
                 },
                 },
 
 
                 markFollowups (el) {
                 markFollowups (el) {
@@ -731,7 +734,7 @@
                     }
                     }
                 },
                 },
 
 
-                showMessage (message) {
+                async showMessage (message) {
                     /* Inserts a chat message into the content area of the chat box.
                     /* Inserts a chat message into the content area of the chat box.
                      *
                      *
                      * Will also insert a new day indicator if the message is on a
                      * Will also insert a new day indicator if the message is on a
@@ -741,6 +744,8 @@
                      *  (Backbone.Model) message: The message object
                      *  (Backbone.Model) message: The message object
                      */
                      */
                     const view = new _converse.MessageView({'model': message});
                     const view = new _converse.MessageView({'model': message});
+                    await view.render();
+                    
                     this.clearChatStateNotification(message);
                     this.clearChatStateNotification(message);
                     this.insertMessage(view);
                     this.insertMessage(view);
                     this.insertDayIndicator(view.el);
                     this.insertDayIndicator(view.el);

+ 34 - 25
src/converse-message-view.js

@@ -41,8 +41,11 @@
                 { __ } = _converse;
                 { __ } = _converse;
 
 
 
 
-            _converse.MessageVersionsModal = _converse.BootstrapModal.extend({
+            _converse.api.settings.update({
+                'show_images_inline': true
+            });
 
 
+            _converse.MessageVersionsModal = _converse.BootstrapModal.extend({
                 toHTML () {
                 toHTML () {
                     return tpl_message_versions_modal(_.extend(
                     return tpl_message_versions_modal(_.extend(
                         this.model.toJSON(), {
                         this.model.toJSON(), {
@@ -61,17 +64,11 @@
                     if (this.model.vcard) {
                     if (this.model.vcard) {
                         this.model.vcard.on('change', this.render, this);
                         this.model.vcard.on('change', this.render, this);
                     }
                     }
-                    this.model.on('change:correcting', this.onMessageCorrection, this);
-                    this.model.on('change:message', this.render, this);
-                    this.model.on('change:progress', this.renderFileUploadProgresBar, this);
-                    this.model.on('change:type', this.render, this);
-                    this.model.on('change:upload', this.render, this);
+                    this.model.on('change', this.onChanged, this);
                     this.model.on('destroy', this.remove, this);
                     this.model.on('destroy', this.remove, this);
-                    this.render();
                 },
                 },
 
 
-                render () {
-                    const is_followup = u.hasClass('chat-msg--followup', this.el);
+                async render () {
                     let msg;
                     let msg;
                     if (this.model.isOnlyChatStateNotification()) {
                     if (this.model.isOnlyChatStateNotification()) {
                         this.renderChatStateNotification()
                         this.renderChatStateNotification()
@@ -80,22 +77,34 @@
                     } else if (this.model.get('type') === 'error') {
                     } else if (this.model.get('type') === 'error') {
                         this.renderErrorMessage();
                         this.renderErrorMessage();
                     } else {
                     } else {
-                        this.renderChatMessage();
-                    }
-                    if (is_followup) {
-                        u.addClass('chat-msg--followup', this.el);
+                        await this.renderChatMessage();
                     }
                     }
                     return this.el;
                     return this.el;
                 },
                 },
 
 
-                onMessageCorrection () {
-                    this.render();
-                    if (!this.model.get('correcting') && this.model.changed.message) {
-                        this.el.addEventListener('animationend', () => u.removeClass('onload', this.el));
-                        u.addClass('onload', this.el);
+                async onChanged (item) {
+                    // Jot down whether it was edited because the `changed`
+                    // attr gets removed when this.render() gets called further
+                    // down.
+                    const edited = item.changed.edited;
+                    if (this.model.changed.progress) {
+                        return this.renderFileUploadProgresBar();
+                    }
+                    if (_.filter(['correcting', 'message', 'type', 'upload'],
+                                 prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop)).length) {
+                        await this.render();
+                    }
+                    if (edited) {
+                        this.onMessageEdited();
                     }
                     }
                 },
                 },
 
 
+                onMessageEdited () {
+                    this.el.addEventListener('animationend', () => u.removeClass('onload', this.el));
+                    this.model.collection.trigger('edited', this);
+                    u.addClass('onload', this.el);
+                },
+
                 replaceElement (msg) {
                 replaceElement (msg) {
                     if (!_.isNil(this.el.parentElement)) {
                     if (!_.isNil(this.el.parentElement)) {
                         this.el.parentElement.replaceChild(msg, this.el);
                         this.el.parentElement.replaceChild(msg, this.el);
@@ -104,7 +113,7 @@
                     return this.el;
                     return this.el;
                 },
                 },
 
 
-                renderChatMessage () {
+                async renderChatMessage () {
                     const is_me_message = this.isMeCommand(),
                     const is_me_message = this.isMeCommand(),
                           moment_time = moment(this.model.get('time')),
                           moment_time = moment(this.model.get('time')),
                           role = this.model.vcard ? this.model.vcard.get('role') : null,
                           role = this.model.vcard ? this.model.vcard.get('role') : null,
@@ -148,14 +157,14 @@
                             _.partial(u.addEmoji, _converse, _)
                             _.partial(u.addEmoji, _converse, _)
                         )(text);
                         )(text);
                     }
                     }
-                    u.renderImageURLs(_converse, msg_content).then(() => {
-                        this.model.collection.trigger('rendered');
-                    });
-                    this.replaceElement(msg);
-
+                    if (_converse.show_images_inline) {
+                        await u.renderImageURLs(_converse, msg_content);
+                    }
                     if (this.model.get('type') !== 'headline') {
                     if (this.model.get('type') !== 'headline') {
-                        this.renderAvatar();
+                        await this.renderAvatar(msg);
                     }
                     }
+                    this.replaceElement(msg);
+                    this.model.collection.trigger('rendered', this);
                 },
                 },
 
 
                 renderErrorMessage () {
                 renderErrorMessage () {

+ 1 - 0
src/converse-muc-views.js

@@ -478,6 +478,7 @@
                 initialize () {
                 initialize () {
                     _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
                     _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
                     this.model.on('change', this.render, this);
                     this.model.on('change', this.render, this);
+                    this.model.occupants.on('add', this.render, this);
                     this.model.occupants.on('change', this.render, this);
                     this.model.occupants.on('change', this.render, this);
                 },
                 },
 
 

+ 1 - 1
src/utils/core.js

@@ -71,7 +71,7 @@
         'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
         'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
     }, console);
     }, console);
 
 
-    var isImage = function (url) {
+    const isImage = function (url) {
         return new Promise((resolve, reject) => {
         return new Promise((resolve, reject) => {
             var img = new Image();
             var img = new Image();
             var timer = window.setTimeout(function () {
             var timer = window.setTimeout(function () {

+ 6 - 4
tests/utils.js

@@ -297,13 +297,15 @@
                .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
                .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
     }
     }
 
 
-    utils.sendMessage = function (chatboxview, message) {
-        chatboxview.el.querySelector('.chat-textarea').value = message;
-        chatboxview.keyPressed({
-            target: chatboxview.el.querySelector('textarea.chat-textarea'),
+    utils.sendMessage = function (view, message) {
+        const promise = new Promise((resolve, reject) => view.on('messageInserted', resolve));
+        view.el.querySelector('.chat-textarea').value = message;
+        view.keyPressed({
+            target: view.el.querySelector('textarea.chat-textarea'),
             preventDefault: _.noop,
             preventDefault: _.noop,
             keyCode: 13
             keyCode: 13
         });
         });
+        return promise;
     };
     };
     return utils;
     return utils;
 }));
 }));

部分文件因为文件数量过多而无法显示