瀏覽代碼

When creating message objects, wait for confirmation from storage

Queue messages and handle them sequentially, each time waiting for promises to
resolve before handling the next message.

Updates #1899, which likely happens because an error message is received
before messages have been fetched.
JC Brand 5 年之前
父節點
當前提交
a7f28cd61d

+ 3 - 3
spec/autocomplete.js

@@ -15,7 +15,7 @@
 
         it("shows all autocompletion options when the user presses @",
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
 
             await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
@@ -43,7 +43,7 @@
                     to: 'romeo@montague.lit',
                     type: 'groupchat'
                 }).c('body').t('Hello world').tree();
-            await view.model.onMessage(msg);
+            await view.model.queueMessage(msg);
 
             // Test that pressing @ brings up all options
             const textarea = view.el.querySelector('textarea.chat-textarea');
@@ -68,7 +68,7 @@
 
         it("autocompletes when the user presses tab",
             mock.initConverse(
-                ['rosterGroupsFetched'], {},
+                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
 
             await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');

+ 6 - 7
spec/emojis.js

@@ -17,18 +17,17 @@
                     ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
 
-                await test_utils.waitForRoster(_converse, 'current');
-                test_utils.openControlBox(_converse);
-
                 const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+                await test_utils.waitForRoster(_converse, 'current');
+                await test_utils.openControlBox(_converse);
                 await test_utils.openChatBoxFor(_converse, contact_jid);
                 const view = _converse.chatboxviews.get(contact_jid);
-                const toolbar = view.el.querySelector('ul.chat-toolbar');
+                const toolbar = await u.waitUntil(() => view.el.querySelector('ul.chat-toolbar'));
                 expect(toolbar.querySelectorAll('li.toggle-smiley__container').length).toBe(1);
                 toolbar.querySelector('a.toggle-smiley').click();
-                await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')));
-                const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container'));
-                const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a'));
+                await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')), 1000);
+                const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container'), 1000);
+                const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a'), 1000);
                 item.click()
                 expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
                 toolbar.querySelector('a.toggle-smiley').click(); // Close the panel again

+ 22 - 42
spec/http-file-upload.js

@@ -15,14 +15,14 @@
 
         describe("Discovering support", function () {
 
-            it("is done automatically", mock.initConverse(async (done, _converse) => {
+            it("is done automatically",
+                    mock.initConverse(
+                        ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                            async function (done, _converse) {
                 const IQ_stanzas = _converse.connection.IQ_stanzas;
-                const IQ_ids =  _converse.connection.IQ_ids;
                 await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
-                await u.waitUntil(() => _.filter(
-                    IQ_stanzas,
-                    iq => iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]')).length
-                );
+                let selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]';
+                let stanza = await u.waitUntil(() => IQ_stanzas.find(iq => iq.querySelector(selector)), 1000);
 
                 /* <iq type='result'
                  *      from='plays.shakespeare.lit'
@@ -37,16 +37,11 @@
                  *  </query>
                  *  </iq>
                  */
-                let stanza = _.find(IQ_stanzas, function (iq) {
-                    return iq.querySelector(
-                        'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]');
-                });
-                const info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
                 stanza = $iq({
                     'type': 'result',
                     'from': 'montague.lit',
                     'to': 'romeo@montague.lit/orchard',
-                    'id': info_IQ_id
+                    'id': stanza.getAttribute('id'),
                 }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
                     .c('identity', {
                         'category': 'server',
@@ -59,19 +54,16 @@
 
                 let entities = await _converse.api.disco.entities.get();
                 expect(entities.length).toBe(2);
-                expect(_.includes(entities.pluck('jid'), 'montague.lit')).toBe(true);
-                expect(_.includes(entities.pluck('jid'), 'romeo@montague.lit')).toBe(true);
+                expect(entities.pluck('jid').includes('montague.lit')).toBe(true);
+                expect(entities.pluck('jid').includes('romeo@montague.lit')).toBe(true);
 
                 expect(entities.get(_converse.domain).features.length).toBe(2);
                 expect(entities.get(_converse.domain).identities.length).toBe(1);
 
                 // Converse.js sees that the entity has a disco#items feature,
                 // so it will make a query for it.
-                await u.waitUntil(() => _.filter(
-                        IQ_stanzas,
-                        iq => iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]')
-                    ).length
-                );
+                selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]';
+                await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).length, 1000);
                 /* <iq from='montague.tld'
                  *      id='step_01'
                  *      to='romeo@montague.tld/garden'
@@ -82,15 +74,13 @@
                  *  </query>
                  *  </iq>
                  */
-                stanza = _.find(IQ_stanzas, function (iq) {
-                    return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]');
-                });
-                const items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+                selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]';
+                stanza = IQ_stanzas.find(iq => iq.querySelector(selector), 500);
                 stanza = $iq({
                     'type': 'result',
                     'from': 'montague.lit',
                     'to': 'romeo@montague.lit/orchard',
-                    'id': items_IQ_id
+                    'id': stanza.getAttribute('id'),
                 }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
                     .c('item', {
                         'jid': 'upload.montague.lit',
@@ -98,28 +88,18 @@
 
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-                _converse.api.disco.entities.get().then(function (entities) {
+                _converse.api.disco.entities.get().then(entities => {
                     expect(entities.length).toBe(2);
                     expect(entities.get('montague.lit').items.length).toBe(1);
-                    return u.waitUntil(function () {
-                        // Converse.js sees that the entity has a disco#info feature,
-                        // so it will make a query for it.
-                        return _.filter(IQ_stanzas, function (iq) {
-                            return iq.querySelector('iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]');
-                        }).length > 0;
-                    }, 300);
+                    // Converse.js sees that the entity has a disco#info feature, so it will make a query for it.
+                    const selector = 'iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]';
+                    return u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).length > 0);
                 });
 
-                stanza = await u.waitUntil(() =>
-                    _.filter(
-                        IQ_stanzas,
-                        iq => iq.querySelector('iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]')
-                    ).pop()
-                );
-                const IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
-
+                selector = 'iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]';
+                stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).pop(), 1000);
                 expect(Strophe.serialize(stanza)).toBe(
-                    `<iq from="romeo@montague.lit/orchard" id="`+IQ_id+`" to="upload.montague.lit" type="get" xmlns="jabber:client">`+
+                    `<iq from="romeo@montague.lit/orchard" id="`+stanza.getAttribute('id')+`" to="upload.montague.lit" type="get" xmlns="jabber:client">`+
                         `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
                     `</iq>`);
 
@@ -144,7 +124,7 @@
                  * </query>
                  * </iq>
                  */
-                stanza = $iq({'type': 'result', 'to': 'romeo@montague.lit/orchard', 'id': IQ_id, 'from': 'upload.montague.lit'})
+                stanza = $iq({'type': 'result', 'to': 'romeo@montague.lit/orchard', 'id': stanza.getAttribute('id'), 'from': 'upload.montague.lit'})
                     .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
                         .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
                         .c('feature', {'var':'urn:xmpp:http:upload:0'}).up()

+ 4 - 4
spec/mam.js

@@ -296,7 +296,7 @@
                         </message>`);
                     spyOn(view.model, 'getDuplicateMessage').and.callThrough();
                     spyOn(view.model, 'updateMessage').and.callThrough();
-                    view.model.onMessage(stanza);
+                    view.model.queueMessage(stanza);
                     await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
                     expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
                     const result = view.model.getDuplicateMessage.calls.all()[0].returnValue
@@ -340,7 +340,7 @@
                             </result>
                         </message>`);
                     spyOn(view.model, 'getDuplicateMessage').and.callThrough();
-                    view.model.onMessage(stanza);
+                    view.model.queueMessage(stanza);
                     await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
                     expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
                     const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
@@ -370,7 +370,7 @@
                                 </forwarded>
                             </result>
                         </message>`);
-                    view.model.onMessage(stanza);
+                    view.model.queueMessage(stanza);
                     await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
                     expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
 
@@ -390,7 +390,7 @@
                         </message>`);
 
                     spyOn(view.model, 'getDuplicateMessage').and.callThrough();
-                    view.model.onMessage(stanza);
+                    view.model.queueMessage(stanza);
                     await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
                     expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
                     const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue

+ 21 - 19
spec/messages.js

@@ -377,7 +377,7 @@
                 .c('body').t("Older message").up()
                 .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'})
                 .tree();
-            await _converse.handleMessageStanza(msg);
+            _converse.handleMessageStanza(msg);
             await new Promise(resolve => view.once('messageInserted', resolve));
 
             msg = $msg({
@@ -389,7 +389,7 @@
                 .c('body').t("Inbetween message").up()
                 .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
                 .tree();
-            await _converse.handleMessageStanza(msg);
+            _converse.handleMessageStanza(msg);
             await new Promise(resolve => view.once('messageInserted', resolve));
 
             msg = $msg({
@@ -401,7 +401,7 @@
                 .c('body').t("another inbetween message").up()
                 .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
                 .tree();
-            await _converse.handleMessageStanza(msg);
+            _converse.handleMessageStanza(msg);
             await new Promise(resolve => view.once('messageInserted', resolve));
 
             msg = $msg({
@@ -413,7 +413,7 @@
                 .c('body').t("An earlier message on the next day").up()
                 .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'})
                 .tree();
-            await _converse.handleMessageStanza(msg);
+            _converse.handleMessageStanza(msg);
             await new Promise(resolve => view.once('messageInserted', resolve));
 
             msg = $msg({
@@ -425,7 +425,7 @@
                 .c('body').t("newer message from the next day").up()
                 .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'})
                 .tree();
-            await _converse.handleMessageStanza(msg);
+            _converse.handleMessageStanza(msg);
             await new Promise(resolve => view.once('messageInserted', resolve));
 
             // Insert <composing> message, to also check that
@@ -439,7 +439,8 @@
                     'type': 'chat'})
                 .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
                 .tree();
-            await _converse.handleMessageStanza(msg);
+            _converse.handleMessageStanza(msg);
+            await new Promise(resolve => view.once('messageInserted', resolve));
 
             msg = $msg({
                     'id': _converse.connection.getUniqueId(),
@@ -509,9 +510,9 @@
         }));
 
         it("is ignored if it's a malformed headline message",
-        mock.initConverse(
-            ['rosterGroupsFetched'], {},
-            async function (done, _converse) {
+                mock.initConverse(
+                    ['rosterGroupsFetched'], {},
+                    async function (done, _converse) {
 
             await test_utils.waitForRoster(_converse, 'current');
             await test_utils.openControlBox(_converse);
@@ -569,6 +570,7 @@
                         'type': 'chat'
                 }).c('body').t(msgtext).tree();
 
+            console.log('AAAAAAAAAAAAAAAAAAAAAAAAAAA')
             await _converse.handleMessageStanza(msg);
             const chatbox = _converse.chatboxes.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);
@@ -576,7 +578,8 @@
             expect(chatbox).toBeDefined();
             expect(view).toBeDefined();
             // Check that the message was received and check the message parameters
-            expect(chatbox.messages.length).toEqual(1);
+            console.log('BBBBBBBBBBBBBBBBBBBBBBBBBBB')
+            await u.waitUntil(() => chatbox.messages.length);
             const msg_obj = chatbox.messages.models[0];
             expect(msg_obj.get('message')).toEqual(msgtext);
             expect(msg_obj.get('fullname')).toBeUndefined();
@@ -584,10 +587,13 @@
             expect(msg_obj.get('sender')).toEqual('them');
             expect(msg_obj.get('is_delayed')).toEqual(false);
             // Now check that the message appears inside the chatbox in the DOM
-            await new Promise(resolve => view.once('messageInserted', resolve));
             const chat_content = view.el.querySelector('.chat-content');
+            console.log('CCCCCCCCCCCCCCCCCCCCCCCCCCC')
+            await u.waitUntil(() => chat_content.querySelector('.chat-msg .chat-msg__text'));
+
             expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext);
             expect(chat_content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
+            console.log('DDDDDDDDDDDDDDDDDDDDDDDDDDD')
             await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet')
             expect(chat_content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
             done();
@@ -624,7 +630,6 @@
             // Check that the chatbox and its view now exist
             const chatbox = await _converse.api.chats.get(recipient_jid);
             const view = _converse.api.chatviews.get(recipient_jid);
-            await new Promise(resolve => view.once('messageInserted', resolve));
             expect(chatbox).toBeDefined();
             expect(view).toBeDefined();
 
@@ -1405,6 +1410,7 @@
 
                 expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
                 // Check that the message was received and check the message parameters
+                await u.waitUntil(() => chatbox.messages.length);
                 expect(chatbox.messages.length).toEqual(1);
                 const msg_obj = chatbox.messages.models[0];
                 expect(msg_obj.get('message')).toEqual(message);
@@ -1440,6 +1446,7 @@
                     .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
                 );
                 const view = _converse.api.chatviews.get(sender_jid);
+                await u.waitUntil(() => view.model.messages.length);
                 expect(view.model.messages.length).toEqual(1);
                 const msg_obj = view.model.messages.at(0);
                 expect(msg_obj.get('message')).toEqual(message.trim());
@@ -1544,15 +1551,12 @@
                     expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
 
                     await _converse.handleMessageStanza(msg);
+                    const view = await u.waitUntil(() => _converse.api.chatviews.get(sender_jid));
+                    await new Promise(resolve => view.once('messageInserted', resolve));
                     expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
 
                     // Check that the chatbox and its view now exist
                     const chatbox = await _converse.api.chats.get(sender_jid);
-                    const view = _converse.api.chatviews.get(sender_jid);
-                    await new Promise(resolve => view.once('messageInserted', resolve));
-
-                    expect(chatbox).toBeDefined();
-                    expect(view).toBeDefined();
                     expect(chatbox.get('fullname') === sender_jid);
 
                     await u.waitUntil(() => view.el.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio');
@@ -1593,12 +1597,10 @@
 
                     let chatbox = await _converse.api.chats.get(sender_jid);
                     expect(chatbox).toBe(null);
-                    // onMessage is a handler for received XMPP messages
                     await _converse.handleMessageStanza(msg);
                     let view = _converse.chatboxviews.get(sender_jid);
                     expect(view).not.toBeDefined();
 
-                    // onMessage is a handler for received XMPP messages
                     _converse.allow_non_roster_messaging = true;
                     await _converse.handleMessageStanza(msg);
                     view = _converse.chatboxviews.get(sender_jid);

+ 1 - 1
spec/minchats.js

@@ -157,7 +157,7 @@
                     to: 'romeo@montague.lit',
                     type: 'groupchat'
                 }).c('body').t(message).tree();
-            view.model.onMessage(msg);
+            view.model.queueMessage(msg);
             await u.waitUntil(() => view.model.messages.length);
             expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy();
             expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe('1');

+ 4 - 4
spec/modtools.js

@@ -10,9 +10,9 @@
     describe("The groupchat moderator tool", function () {
 
         it("allows you to set affiliations and roles",
-            mock.initConverse(
-                ['rosterGroupsFetched'], {},
-                async function (done, _converse) {
+                mock.initConverse(
+                    ['rosterGroupsFetched'], {},
+                    async function (done, _converse) {
 
             spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
             const muc_jid = 'lounge@montague.lit';
@@ -26,7 +26,7 @@
             ];
             await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members);
             const view = _converse.chatboxviews.get(muc_jid);
-            await u.waitUntil(() => (view.model.occupants.length === 5));
+            await u.waitUntil(() => (view.model.occupants.length === 5), 1000);
 
             const textarea = view.el.querySelector('.chat-textarea');
             textarea.value = '/modtools';

+ 28 - 23
spec/muc.js

@@ -329,7 +329,7 @@
 
                 await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED);
                 await test_utils.returnMemberLists(_converse, muc_jid);
-                // await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2);
+                await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2);
 
                 const info_texts = Array.from(view.el.querySelectorAll('.chat-content .chat-info')).map(e => e.textContent.trim());
                 expect(info_texts[0]).toBe('A new groupchat has been created');
@@ -516,7 +516,7 @@
                         'type': 'groupchat'
                     }).c('body').t(message).tree();
 
-                await view.model.onMessage(msg);
+                await view.model.queueMessage(msg);
 
                 spyOn(view.model, 'clearMessages').and.callThrough();
                 await view.model.close();
@@ -547,7 +547,7 @@
                         'type': 'groupchat'
                     }).c('body').t(message).tree();
 
-                await view.model.onMessage(msg);
+                await view.model.queueMessage(msg);
                 await u.waitUntil(()  => view.el.querySelector('.chat-msg__text a'));
                 view.el.querySelector('.chat-msg__text a').click();
                 await u.waitUntil(() => _converse.chatboxes.length === 3)
@@ -1142,7 +1142,8 @@
                         'type': 'groupchat'
                     }).c('body').t('Some message').tree();
 
-                await view.model.onMessage(msg);
+                await view.model.queueMessage(msg);
+                await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.chat_content).pop());
 
                 let stanza = u.toStanza(
                     `<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="conversations@conference.siacs.eu/Guus">
@@ -1357,7 +1358,8 @@
                         'to': 'romeo@montague.lit',
                         'type': 'groupchat'
                     }).c('body').t(message).tree();
-                await view.model.onMessage(msg);
+                await view.model.queueMessage(msg);
+                await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.chat_content).pop());
                 expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Dyon van de Wege')).toBeTruthy();
                 expect(view.el.querySelector('.chat-msg__text').textContent.trim()).toBe('is tired');
 
@@ -1368,8 +1370,9 @@
                     to: 'romeo@montague.lit',
                     type: 'groupchat'
                 }).c('body').t(message).tree();
-                await view.model.onMessage(msg);
-                expect(_.includes(sizzle('.chat-msg__author:last', view.el).pop().textContent, '**Romeo Montague')).toBeTruthy();
+                await view.model.queueMessage(msg);
+                await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
+                expect(sizzle('.chat-msg__author:last', view.el).pop().textContent.includes('**Romeo Montague')).toBeTruthy();
                 expect(sizzle('.chat-msg__text:last', view.el).pop().textContent.trim()).toBe('is as well');
                 done();
             }));
@@ -1867,8 +1870,7 @@
 
                 await test_utils.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
 
-                let stanza = await u.waitUntil(() => _.filter(
-                    IQ_stanzas,
+                let stanza = await u.waitUntil(() => IQ_stanzas.filter(
                     iq => iq.querySelector(
                         `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
                     )).pop()
@@ -2088,8 +2090,9 @@
                     to: 'romeo@montague.lit',
                     type: 'groupchat'
                 }).c('body').t(text);
-                await view.model.onMessage(message.nodeTree);
+                await view.model.queueMessage(message.nodeTree);
                 const chat_content = view.el.querySelector('.chat-content');
+                await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
                 expect(chat_content.querySelectorAll('.chat-msg').length).toBe(1);
                 expect(chat_content.querySelector('.chat-msg__text').textContent.trim()).toBe(text);
                 expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
@@ -2134,7 +2137,7 @@
                                 by="lounge@montague.lit"/>
                         <origin-id xmlns="urn:xmpp:sid:0" id="${view.model.messages.at(0).get('origin_id')}"/>
                     </message>`);
-                await view.model.onMessage(stanza);
+                await view.model.queueMessage(stanza);
                 expect(chat_content.querySelectorAll('.chat-msg').length).toBe(1);
                 expect(sizzle('.chat-msg__text:last').pop().textContent.trim()).toBe(text);
                 expect(view.model.messages.length).toBe(1);
@@ -2156,7 +2159,7 @@
                 const promises = [];
                 for (let i=0; i<20; i++) {
                     promises.push(
-                        view.model.onMessage(
+                        view.model.queueMessage(
                             $msg({
                                 from: 'lounge@montague.lit/someone',
                                 to: 'romeo@montague.lit.com',
@@ -2169,13 +2172,14 @@
                 // Give enough time for `markScrolled` to have been called
                 setTimeout(async () => {
                     view.content.scrollTop = 0;
-                    await view.model.onMessage(
+                    await view.model.queueMessage(
                         $msg({
                             from: 'lounge@montague.lit/someone',
                             to: 'romeo@montague.lit.com',
                             type: 'groupchat',
                             id: u.getUniqueId(),
                         }).c('body').t(message).tree());
+                    await new Promise(resolve => view.once('messageInserted', resolve));
                     // Now check that the message appears inside the chatbox in the DOM
                     const chat_content = view.el.querySelector('.chat-content');
                     const msg_txt = sizzle('.chat-msg:last .chat-msg__text', chat_content).pop().textContent;
@@ -4969,7 +4973,7 @@
 
                 const nick = mock.chatroom_names[0];
 
-                await view.model.onMessage($msg({
+                await view.model.queueMessage($msg({
                         from: muc_jid+'/'+nick,
                         id: u.getUniqueId(),
                         to: 'romeo@montague.lit',
@@ -4980,7 +4984,7 @@
                 expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(1);
                 expect(roomspanel.el.querySelector('.msgs-indicator').textContent.trim()).toBe('1');
 
-                await view.model.onMessage($msg({
+                await view.model.queueMessage($msg({
                     'from': muc_jid+'/'+nick,
                     'id': u.getUniqueId(),
                     'to': 'romeo@montague.lit',
@@ -5106,7 +5110,7 @@
                             type: 'groupchat'
                         }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
 
-                    await view.model.onMessage(msg);
+                    await view.model.queueMessage(msg);
                     await u.waitUntil(() => view.el.querySelectorAll('.chat-state-notification').length);
 
                     // Check that the notification appears inside the chatbox in the DOM
@@ -5130,7 +5134,7 @@
                             to: 'romeo@montague.lit',
                             type: 'groupchat'
                         }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                    await view.model.onMessage(msg);
+                    await view.model.queueMessage(msg);
 
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
@@ -5150,7 +5154,7 @@
                             to: 'romeo@montague.lit',
                             type: 'groupchat'
                         }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                    await view.model.onMessage(msg);
+                    await view.model.queueMessage(msg);
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
                     expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
@@ -5170,7 +5174,8 @@
                         to: 'romeo@montague.lit',
                         type: 'groupchat'
                     }).c('body').t('hello world').tree();
-                    await view.model.onMessage(msg);
+                    await view.model.queueMessage(msg);
+                    await new Promise(resolve => view.once('messageInserted', resolve));
 
                     const messages = view.el.querySelectorAll('.message');
                     expect(messages.length).toBe(7);
@@ -5276,7 +5281,7 @@
                             to: 'romeo@montague.lit',
                             type: 'groupchat'
                         }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                    await view.model.onMessage(msg);
+                    await view.model.queueMessage(msg);
 
                     // Check that the notification appears inside the chatbox in the DOM
                     var events = view.el.querySelectorAll('.chat-event');
@@ -5297,7 +5302,7 @@
                             to: 'romeo@montague.lit',
                             type: 'groupchat'
                         }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                    await view.model.onMessage(msg);
+                    await view.model.queueMessage(msg);
 
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
@@ -5316,7 +5321,7 @@
                             to: 'romeo@montague.lit',
                             type: 'groupchat'
                         }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                    await view.model.onMessage(msg);
+                    await view.model.queueMessage(msg);
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
                     expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
@@ -5339,7 +5344,7 @@
                             to: 'romeo@montague.lit',
                             type: 'groupchat'
                         }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                    await view.model.onMessage(msg);
+                    await view.model.queueMessage(msg);
                     events = view.el.querySelectorAll('.chat-event');
                     expect(events.length).toBe(3);
                     expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');

+ 50 - 27
spec/muc_messages.js

@@ -20,8 +20,8 @@
 
                 const muc_jid = 'lounge@montague.lit';
                 await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
-
                 const view = _converse.api.chatviews.get(muc_jid);
+                await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length);
                 const presence = u.toStanza(`
                     <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
                         <x xmlns="http://jabber.org/protocol/muc#user">
@@ -49,6 +49,7 @@
                 await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
                 const view = _converse.api.chatviews.get(muc_jid);
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length);
+                expect(view.el.querySelectorAll('.chat-info').length).toBe(1);
 
                 const presence = u.toStanza(`
                     <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
@@ -59,9 +60,18 @@
                         </x>
                     </presence>
                 `);
+                // XXX: We wait for createInfoMessages to complete, if we don't
+                // we still get two info messages due to messages
+                // created from presences not being queued and run
+                // sequentially (i.e. by waiting for promises to resolve)
+                // like we do with message stanzas.
+                spyOn(view.model, 'createInfoMessages').and.callThrough();
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
+                await u.waitUntil(() => view.model.createInfoMessages.calls.count());
+                await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2);
+
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
-                await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length > 1);
+                await u.waitUntil(() => view.model.createInfoMessages.calls.count() === 2);
                 expect(view.el.querySelectorAll('.chat-info').length).toBe(2);
                 done();
             }));
@@ -91,9 +101,13 @@
                 </message>
             `);
             const view = _converse.api.chatviews.get(muc_jid);
-            await view.model.onMessage(received_stanza);
+            spyOn(view.model, 'onMessage').and.callThrough();
+
+
+            await view.model.queueMessage(received_stanza);
             spyOn(converse.env.log, 'warn');
             _converse.connection._dataRecv(test_utils.createRequest(received_stanza));
+            await u.waitUntil(() => view.model.onMessage.calls.count());
             expect(converse.env.log.warn).toHaveBeenCalledWith(
                 'onMessage: Ignoring unencapsulated forwarded groupchat message'
             );
@@ -120,7 +134,8 @@
                     to: 'romeo@montague.lit',
                     type: 'groupchat'
                 }).c('body').t(message).tree();
-            await view.model.onMessage(msg);
+            await view.model.queueMessage(msg);
+            await new Promise(resolve => view.once('messageInserted', resolve));
             expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy();
             done();
         }));
@@ -141,7 +156,7 @@
                     to: 'romeo@montague.lit',
                     type: 'groupchat'
                 }).c('body').t('First message').tree();
-            await view.model.onMessage(msg);
+            await view.model.queueMessage(msg);
             await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
 
             msg = $msg({
@@ -150,7 +165,7 @@
                     to: 'romeo@montague.lit',
                     type: 'groupchat'
                 }).c('body').t('Another message').tree();
-            await view.model.onMessage(msg);
+            await view.model.queueMessage(msg);
             await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
             expect(view.model.messages.length).toBe(2);
             done();
@@ -196,7 +211,7 @@
                 </message>`);
 
             spyOn(view.model, 'updateMessage');
-            await view.model.onMessage(stanza);
+            await view.model.queueMessage(stanza);
             await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 2);
             result = await view.model.getDuplicateMessage.calls.all()[1].returnValue;
             expect(result instanceof _converse.Message).toBe(true);
@@ -302,7 +317,7 @@
                 }).c('body').t('I am groot').tree();
             const view = _converse.api.chatviews.get(muc_jid);
             spyOn(converse.env.log, 'warn');
-            await view.model.onMessage(msg);
+            await view.model.queueMessage(msg);
             expect(converse.env.log.warn).toHaveBeenCalledWith(
                 'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+
                 'according to the XEP groupchat messages SHOULD NOT be carbon copied'
@@ -326,7 +341,7 @@
                 to: 'romeo@montague.lit',
                 type: 'groupchat'
             }).c('body').t('I wrote this message!').tree();
-            await view.model.onMessage(msg);
+            await view.model.queueMessage(msg);
             await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
             expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner');
             expect(view.model.messages.last().occupant.get('role')).toBe('moderator');
@@ -352,7 +367,8 @@
                 to: 'romeo@montague.lit',
                 type: 'groupchat'
             }).c('body').t('Another message!').tree();
-            await view.model.onMessage(msg);
+            await view.model.queueMessage(msg);
+            await new Promise(resolve => view.once('messageInserted', resolve));
             expect(view.model.messages.last().occupant.get('affiliation')).toBe('member');
             expect(view.model.messages.last().occupant.get('role')).toBe('participant');
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
@@ -371,14 +387,15 @@
                 .c('status').attrs({code:'110'}).up()
                 .c('status').attrs({code:'210'}).nodeTree;
             _converse.connection._dataRecv(test_utils.createRequest(presence));
+
             view.model.sendMessage('hello world');
             await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 3);
 
-            expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner');
-            expect(view.model.messages.last().occupant.get('role')).toBe('moderator');
+            const occupant = await u.waitUntil(() => view.model.messages.filter(m => m.get('type') === 'groupchat')[2].occupant);
+            expect(occupant.get('affiliation')).toBe('owner');
+            expect(occupant.get('role')).toBe('moderator');
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
-            expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat moderator owner');
-
+            await u.waitUntil(() => sizzle('.chat-msg', view.el).pop().classList.value.trim() === 'message chat-msg groupchat moderator owner');
 
             const add_events = view.model.occupants._events.add.length;
             msg = $msg({
@@ -387,7 +404,8 @@
                 to: 'romeo@montague.lit',
                 type: 'groupchat'
             }).c('body').t('Message from someone not in the MUC right now').tree();
-            await view.model.onMessage(msg);
+            await view.model.queueMessage(msg);
+            await new Promise(resolve => view.once('messageInserted', resolve));
             expect(view.model.messages.last().occupant).toBeUndefined();
             // Check that there's a new "add" event handler, for when the occupant appears.
             expect(view.model.occupants._events.add.length).toBe(add_events+1);
@@ -451,7 +469,7 @@
                     to: 'romeo@montague.lit',
                     type: 'groupchat'
                 }).c('body').t('I wrote this message!').tree();
-            await view.model.onMessage(msg);
+            await view.model.queueMessage(msg);
             expect(view.model.messages.last().get('sender')).toBe('me');
             done();
         }));
@@ -476,7 +494,7 @@
                 }).tree();
             _converse.connection._dataRecv(test_utils.createRequest(stanza));
             const msg_id = u.getUniqueId();
-            await view.model.onMessage($msg({
+            await view.model.queueMessage($msg({
                     'from': 'lounge@montague.lit/newguy',
                     'to': _converse.connection.jid,
                     'type': 'groupchat',
@@ -488,7 +506,7 @@
             expect(view.el.querySelector('.chat-msg__text').textContent)
                 .toBe('But soft, what light through yonder airlock breaks?');
 
-            await view.model.onMessage($msg({
+            await view.model.queueMessage($msg({
                     'from': 'lounge@montague.lit/newguy',
                     'to': _converse.connection.jid,
                     'type': 'groupchat',
@@ -500,7 +518,7 @@
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
             expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
 
-            await view.model.onMessage($msg({
+            await view.model.queueMessage($msg({
                     'from': 'lounge@montague.lit/newguy',
                     'to': _converse.connection.jid,
                     'type': 'groupchat',
@@ -597,12 +615,13 @@
             expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false);
 
             // Check that messages from other users are skipped
-            await view.model.onMessage($msg({
+            await view.model.queueMessage($msg({
                 'from': muc_jid+'/someone-else',
                 'id': u.getUniqueId(),
                 'to': 'romeo@montague.lit',
                 'type': 'groupchat'
             }).c('body').t('Hello world').tree());
+            await new Promise(resolve => view.once('messageInserted', resolve));
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
 
             // Test that pressing the down arrow cancels message correction
@@ -658,7 +677,7 @@
                                by="lounge@montague.lit"/>
                     <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
                 </message>`);
-            await view.model.onMessage(stanza);
+            await view.model.queueMessage(stanza);
             await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
             expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
             expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1);
@@ -842,11 +861,10 @@
                         .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'6', 'end':'10', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up()
                         .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up()
                         .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree;
-                await view.model.onMessage(msg);
-                const messages = view.el.querySelectorAll('.chat-msg__text');
-                expect(messages.length).toBe(1);
-                expect(messages[0].classList.length).toEqual(1);
-                expect(messages[0].innerHTML).toBe(
+                await view.model.queueMessage(msg);
+                const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
+                expect(message.classList.length).toEqual(1);
+                expect(message.innerHTML).toBe(
                     'hello <span class="mention">z3r0</span> '+
                     '<span class="mention mention--self badge badge-info">tom</span> '+
                     '<span class="mention">mr.robot</span>, how are you?');
@@ -887,7 +905,7 @@
                             type="groupchat">
                         <body>Boo!</body>
                     </message>`);
-                await view.model.onMessage(stanza);
+                await view.model.queueMessage(stanza);
 
                 // Run a few unit tests for the parseTextForReferences method
                 let [text, references] = view.model.parseTextForReferences('hello z3r0')
@@ -1008,6 +1026,7 @@
                         'affiliation': 'none',
                         'role': 'participant'
                     })));
+                await u.waitUntil(() => view.model.occupants.length === 2);
 
                 const textarea = view.el.querySelector('textarea.chat-textarea');
                 textarea.value = 'hello @Link Mauve'
@@ -1054,6 +1073,7 @@
                             'role': 'participant'
                         })));
                 });
+                await u.waitUntil(() => view.model.occupants.length === 5);
 
                 const textarea = view.el.querySelector('textarea.chat-textarea');
                 textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?'
@@ -1087,6 +1107,7 @@
                 expect(view.model.messages.at(0).get('correcting')).toBe(true);
                 expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
                 await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
+                await u.waitUntil(() => _converse.connection.send.calls.count() === 2);
 
                 textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?';
                 view.onKeyDown(enter_event);
@@ -1130,6 +1151,7 @@
                             'role': 'participant'
                         })));
                 });
+                await u.waitUntil(() => view.model.occupants.length === 5);
 
                 spyOn(_converse.connection, 'send');
                 const textarea = view.el.querySelector('textarea.chat-textarea');
@@ -1141,6 +1163,7 @@
                     'keyCode': 13 // Enter
                 }
                 view.onKeyDown(enter_event);
+                await new Promise(resolve => view.once('messageInserted', resolve));
 
                 const msg = _converse.connection.send.calls.all()[0].args[0];
                 expect(msg.toLocaleString())

+ 3 - 3
spec/notification.js

@@ -177,7 +177,7 @@
                         to: 'romeo@montague.lit',
                         type: 'groupchat'
                     }).c('body').t(text);
-                    await view.model.onMessage(message.nodeTree);
+                    await view.model.queueMessage(message.nodeTree);
                     await u.waitUntil(() => _converse.playSoundNotification.calls.count());
                     expect(_converse.playSoundNotification).toHaveBeenCalled();
 
@@ -188,7 +188,7 @@
                         to: 'romeo@montague.lit',
                         type: 'groupchat'
                     }).c('body').t(text);
-                    await view.model.onMessage(message.nodeTree);
+                    await view.model.queueMessage(message.nodeTree);
                     expect(_converse.playSoundNotification, 1);
                     _converse.play_sounds = false;
 
@@ -199,7 +199,7 @@
                         to: 'romeo@montague.lit',
                         type: 'groupchat'
                     }).c('body').t(text);
-                    await view.model.onMessage(message.nodeTree);
+                    await view.model.queueMessage(message.nodeTree);
                     expect(_converse.playSoundNotification, 1);
                     _converse.play_sounds = false;
                     done();

+ 1 - 0
spec/register.js

@@ -353,6 +353,7 @@
             // Hide the controlbox so that we can see whether the test
             // passed or failed
             u.addClass('hidden', _converse.chatboxviews.get('controlbox').el);
+            delete _converse.connection;
             done();
         }));
     });

+ 23 - 23
spec/retractions.js

@@ -25,7 +25,7 @@
                         by="lounge@montague.lit"/>
                 <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
             </message>`);
-        await view.model.onMessage(reflection_stanza);
+        await view.model.queueMessage(reflection_stanza);
         await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
 
         const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
@@ -58,7 +58,7 @@
                     </message>
                 `);
                 const view = _converse.api.chatviews.get(muc_jid);
-                await view.model.onMessage(received_stanza);
+                await view.model.queueMessage(received_stanza);
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
                 expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
                 expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
@@ -74,7 +74,7 @@
 
                 _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
                 await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
-                expect(view.model.handleRetraction.calls.first().returnValue).toBe(true);
+                expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true);
                 expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
                 expect(view.model.messages.length).toBe(2);
                 expect(view.model.messages.at(1).get('retracted')).toBeTruthy();
@@ -110,7 +110,7 @@
 
                 await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
                 await u.waitUntil(() => view.model.messages.length === 1);
-                expect(view.model.handleRetraction.calls.first().returnValue).toBe(true);
+                expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true);
                 expect(view.model.messages.length).toBe(1);
                 expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
                 expect(view.model.messages.at(0).get('dangling_retraction')).toBe(true);
@@ -136,7 +136,7 @@
                 expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1');
                 expect(message.get('time')).toBe(date);
                 expect(message.get('type')).toBe('groupchat');
-                expect(view.model.handleRetraction.calls.all().pop().returnValue).toBe(true);
+                expect(await view.model.handleRetraction.calls.all().pop().returnValue).toBe(true);
                 done();
             }));
         });
@@ -169,7 +169,7 @@
 
                 await u.waitUntil(() => view.model.handleModeration.calls.count() === 1);
                 await u.waitUntil(() => view.model.messages.length === 1);
-                expect(view.model.handleModeration.calls.first().returnValue).toBe(true);
+                expect(await view.model.handleModeration.calls.first().returnValue).toBe(true);
                 expect(view.model.messages.length).toBe(1);
                 expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
                 expect(view.model.messages.at(0).get('dangling_moderation')).toBe(true);
@@ -195,7 +195,7 @@
                 expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1');
                 expect(message.get('time')).toBe(date);
                 expect(message.get('type')).toBe('groupchat');
-                expect(view.model.handleModeration.calls.all().pop().returnValue).toBe(true);
+                expect(await view.model.handleModeration.calls.all().pop().returnValue).toBe(true);
                 done();
             }));
         });
@@ -400,7 +400,7 @@
                     </message>
                 `);
                 const view = _converse.api.chatviews.get(muc_jid);
-                await view.model.onMessage(received_stanza);
+                await view.model.queueMessage(received_stanza);
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
                 expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
                 expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
@@ -446,7 +446,7 @@
                         <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
                     </message>
                 `);
-                await view.model.onMessage(received_stanza);
+                await view.model.queueMessage(received_stanza);
                 await u.waitUntil(() => view.model.messages.length === 1);
                 expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
 
@@ -504,7 +504,7 @@
                             </moderated>
                         </apply-to>
                     </message>`);
-                await view.model.onMessage(retraction);
+                await view.model.queueMessage(retraction);
                 expect(view.model.messages.length).toBe(1);
                 expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
                 expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
@@ -530,7 +530,7 @@
                         <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
                     </message>
                 `);
-                await view.model.onMessage(received_stanza);
+                await view.model.queueMessage(received_stanza);
                 await u.waitUntil(() => view.el.querySelector('.chat-msg__content'));
                 expect(view.el.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null);
                 const result = await view.model.canRetractMessages();
@@ -557,7 +557,7 @@
                         <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
                     </message>
                 `);
-                await view.model.onMessage(received_stanza);
+                await view.model.queueMessage(received_stanza);
                 await u.waitUntil(() => view.model.messages.length === 1);
                 expect(view.model.messages.length).toBe(1);
 
@@ -585,7 +585,7 @@
                             </moderated>
                         </apply-to>
                     </message>`);
-                await view.model.onMessage(retraction);
+                await view.model.queueMessage(retraction);
 
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
                 expect(view.model.messages.length).toBe(1);
@@ -778,7 +778,7 @@
                                 by="lounge@montague.lit"/>
                         <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
                     </message>`);
-                await view.model.onMessage(reflection_stanza);
+                await view.model.queueMessage(reflection_stanza);
                 await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
                 expect(view.model.messages.length).toBe(1);
                 expect(view.model.messages.at(0).get('editable')).toBe(true);
@@ -794,7 +794,7 @@
                             </moderated>
                         </apply-to>
                     </message>`);
-                await view.model.onMessage(retraction);
+                await view.model.queueMessage(retraction);
                 expect(view.model.messages.length).toBe(1);
                 expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
                 expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
@@ -884,10 +884,10 @@
                 const message = view.model.messages.at(1);
                 expect(message.get('retracted')).toBeTruthy();
                 expect(message.get('is_tombstone')).toBe(true);
-                expect(view.model.handleRetraction.calls.first().returnValue).toBe(false);
-                expect(view.model.handleRetraction.calls.all()[1].returnValue).toBe(false);
-                expect(view.model.handleRetraction.calls.all()[2].returnValue).toBe(true);
-                expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
+                expect(await view.model.handleRetraction.calls.first().returnValue).toBe(false);
+                expect(await view.model.handleRetraction.calls.all()[1].returnValue).toBe(false);
+                expect(await view.model.handleRetraction.calls.all()[2].returnValue).toBe(true);
+                await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
                 expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
                 const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
                 expect(el.textContent.trim()).toBe('Mercutio has removed this message');
@@ -959,8 +959,8 @@
                 expect(message.get('is_tombstone')).toBe(true);
 
                 await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
-                expect(view.model.handleRetraction.calls.first().returnValue).toBe(false);
-                expect(view.model.handleRetraction.calls.all()[1].returnValue).toBe(true);
+                expect(await view.model.handleRetraction.calls.first().returnValue).toBe(false);
+                expect(await view.model.handleRetraction.calls.all()[1].returnValue).toBe(true);
                 expect(view.model.messages.length).toBe(1);
                 message = view.model.messages.at(0);
                 expect(message.get('retracted')).toBeTruthy();
@@ -1041,8 +1041,8 @@
                 expect(message.get('is_tombstone')).toBe(true);
 
                 await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
-                expect(view.model.handleModeration.calls.first().returnValue).toBe(false);
-                expect(view.model.handleModeration.calls.all()[1].returnValue).toBe(true);
+                expect(await view.model.handleModeration.calls.first().returnValue).toBe(false);
+                expect(await view.model.handleModeration.calls.all()[1].returnValue).toBe(true);
 
                 expect(view.model.messages.length).toBe(1);
                 message = view.model.messages.at(0);

+ 3 - 3
spec/roomslist.js

@@ -283,7 +283,7 @@
             const view = _converse.chatboxviews.get(room_jid);
             view.model.set({'minimized': true});
             const nick = mock.chatroom_names[0];
-            await view.model.onMessage(
+            await view.model.queueMessage(
                 $msg({
                     from: room_jid+'/'+nick,
                     id: u.getUniqueId(),
@@ -297,7 +297,7 @@
             expect(Array.from(room_el.classList).includes('unread-msgs')).toBeTruthy();
 
             // If the user is mentioned, the counter also gets updated
-            await view.model.onMessage(
+            await view.model.queueMessage(
                 $msg({
                     from: room_jid+'/'+nick,
                     id: u.getUniqueId(),
@@ -310,7 +310,7 @@
             expect(indicator_el.textContent).toBe('1');
 
             spyOn(view.model, 'incrementUnreadMsgCounter').and.callThrough();
-            await view.model.onMessage(
+            await view.model.queueMessage(
                 $msg({
                     from: room_jid+'/'+nick,
                     id: u.getUniqueId(),

+ 7 - 7
src/converse-omemo.js

@@ -189,7 +189,7 @@ converse.plugins.add('converse-omemo', {
                     let message, stanza;
                     try {
                         const devices = await _converse.getBundlesAndBuildSessions(this);
-                        message = this.messages.create(attrs);
+                        message = await this.createMessage(attrs);
                         stanza = await _converse.createOMEMOMessageStanza(this, message, devices);
                     } catch (e) {
                         this.handleMessageSendError(e);
@@ -307,7 +307,7 @@ converse.plugins.add('converse-omemo', {
             reportDecryptionError (e) {
                 if (_converse.loglevel === 'debug') {
                     const { __ } = _converse;
-                    this.messages.create({
+                    this.createMessage({
                         'message': __("Sorry, could not decrypt a received OMEMO message due to an error.") + ` ${e.name} ${e.message}`,
                         'type': 'error',
                     });
@@ -1208,11 +1208,11 @@ converse.plugins.add('converse-omemo', {
             if (chatroom.get('omemo_active')) {
                 const supported = await _converse.contactHasOMEMOSupport(occupant.get('jid'));
                 if (!supported) {
-                        chatroom.messages.create({
-                            'message': __("%1$s doesn't appear to have a client that supports OMEMO. " +
-                                          "Encrypted chat will no longer be possible in this grouchat.", occupant.get('nick')),
-                            'type': 'error'
-                        });
+                    chatroom.createMessage({
+                        'message': __("%1$s doesn't appear to have a client that supports OMEMO. " +
+                                      "Encrypted chat will no longer be possible in this grouchat.", occupant.get('nick')),
+                        'type': 'error'
+                    });
                     chatroom.save({'omemo_active': false, 'omemo_supported': false});
                 }
             }

+ 33 - 16
src/headless/converse-chat.js

@@ -383,6 +383,19 @@ converse.plugins.add('converse-chat', {
                 return this.messages.fetched;
             },
 
+            /**
+             * Queue an incoming `chat` message stanza for processing.
+             * @async
+             * @private
+             * @method _converse.ChatRoom#queueMessage
+             * @param { XMLElement } stanza - The message stanza.
+             */
+            queueMessage (stanza, original_stanza, from_jid) {
+                this.msg_chain = (this.msg_chain || this.messages.fetched);
+                this.msg_chain = this.msg_chain.then(() => this.onMessage(stanza, original_stanza, from_jid));
+                return this.msg_chain;
+            },
+
             async onMessage (stanza, original_stanza, from_jid) {
                 const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
                 const message = this.getDuplicateMessage(attrs);
@@ -392,12 +405,12 @@ converse.plugins.add('converse-chat', {
                     !this.handleReceipt (stanza, original_stanza, from_jid) &&
                     !this.handleChatMarker(stanza, from_jid)
                 ) {
-                    if (this.handleRetraction(attrs)) {
+                    if (await this.handleRetraction(attrs)) {
                         return;
                     }
                     this.setEditable(attrs, attrs.time, stanza);
                     if (u.shouldCreateMessage(attrs)) {
-                        const msg = this.handleCorrection(attrs) || this.messages.create(attrs);
+                        const msg = this.handleCorrection(attrs) || await this.createMessage(attrs);
                         this.incrementUnreadMsgCounter(msg);
                     }
                 }
@@ -468,9 +481,9 @@ converse.plugins.add('converse-chat', {
                 }
             },
 
-            createMessageFromError (error) {
+            async createMessageFromError (error) {
                 if (error instanceof _converse.TimeoutError) {
-                    const msg = this.messages.create({'type': 'error', 'message': error.message, 'retry': true});
+                    const msg = await this.createMessage({'type': 'error', 'message': error.message, 'retry': true});
                     msg.error = error;
                 }
             },
@@ -609,7 +622,7 @@ converse.plugins.add('converse-chat', {
              * @returns { Boolean } Returns `true` or `false` depending on
              *  whether a message was retracted or not.
              */
-            handleRetraction (attrs) {
+            async handleRetraction (attrs) {
                 const RETRACTION_ATTRIBUTES = ['retracted', 'retracted_id', 'editable'];
                 if (attrs.retracted) {
                     if (attrs.is_tombstone) {
@@ -618,7 +631,7 @@ converse.plugins.add('converse-chat', {
                     const message = this.messages.findWhere({'origin_id': attrs.retracted_id, 'from': attrs.from});
                     if (!message) {
                         attrs['dangling_retraction'] = true;
-                        this.messages.create(attrs);
+                        await this.createMessage(attrs);
                         return true;
                     }
                     message.save(pick(attrs, RETRACTION_ATTRIBUTES));
@@ -932,6 +945,10 @@ converse.plugins.add('converse-chat', {
                 }
             },
 
+            createMessage (attrs, options) {
+                return this.messages.create(attrs, Object.assign({'wait': true, 'promise':true}, options));
+            },
+
             /**
              * Responsible for sending off a text message inside an ongoing chat conversation.
              * @method _converse.ChatBox#sendMessage
@@ -943,7 +960,7 @@ converse.plugins.add('converse-chat', {
              * const chat = _converse.api.chats.get('buddy1@example.com');
              * chat.sendMessage('hello world');
              */
-            sendMessage (text, spoiler_hint) {
+            async sendMessage (text, spoiler_hint) {
                 const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint);
                 let message = this.messages.findWhere('correcting')
                 if (message) {
@@ -961,7 +978,7 @@ converse.plugins.add('converse-chat', {
                     });
                 } else {
                     this.setEditable(attrs, (new Date()).toISOString());
-                    message = this.messages.create(attrs);
+                    message = await this.createMessage(attrs);
                 }
                 _converse.api.send(this.createMessageStanza(message));
                 return message;
@@ -996,7 +1013,7 @@ converse.plugins.add('converse-chat', {
                 const result = await _converse.api.disco.features.get(Strophe.NS.HTTPUPLOAD, _converse.domain);
                 const item = result.pop();
                 if (!item) {
-                    this.messages.create({
+                    this.createMessage({
                         'message': __("Sorry, looks like file upload is not supported by your server."),
                         'type': 'error',
                         'is_ephemeral': true
@@ -1008,16 +1025,16 @@ converse.plugins.add('converse-chat', {
                       slot_request_url = get(item, 'id');
 
                 if (!slot_request_url) {
-                    this.messages.create({
+                    this.createMessage({
                         'message': __("Sorry, looks like file upload is not supported by your server."),
                         'type': 'error',
                         'is_ephemeral': true
                     });
                     return;
                 }
-                Array.from(files).forEach(file => {
+                Array.from(files).forEach(async file => {
                     if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) {
-                        return this.messages.create({
+                        return this.createMessage({
                             'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
                                 file.name, filesize(max_file_size)),
                             'type': 'error',
@@ -1031,7 +1048,7 @@ converse.plugins.add('converse-chat', {
                             'slot_request_url': slot_request_url
                         });
                         this.setEditable(attrs, (new Date()).toISOString());
-                        const message = this.messages.create(attrs, {'silent': true});
+                        const message = await this.createMessage(attrs, {'silent': true});
                         message.file = file;
                         this.messages.trigger('add', message);
                         message.getRequestSlotURL();
@@ -1129,7 +1146,7 @@ converse.plugins.add('converse-chat', {
                 return;
             }
             const attrs = await chatbox.getMessageAttributesFromStanza(stanza, stanza);
-            await chatbox.messages.create(attrs);
+            await chatbox.createMessage(attrs);
         }
 
 
@@ -1198,7 +1215,7 @@ converse.plugins.add('converse-chat', {
             const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0;
             const roster_nick = get(contact, 'attributes.nickname');
             const chatbox = await _converse.api.chats.get(contact_jid, {'nickname': roster_nick}, has_body);
-            chatbox && await chatbox.onMessage(stanza, original_stanza, from_jid);
+            chatbox && await chatbox.queueMessage(stanza, original_stanza, from_jid);
             /**
              * Triggered when a message stanza is been received and processed.
              * @event _converse#message
@@ -1286,7 +1303,7 @@ converse.plugins.add('converse-chat', {
 
         _converse.api.listen.on('clearSession', () => {
             if (_converse.shouldClearCache()) {
-                _converse.chatboxes.filter(c => c.messages && c.messages.clearStore({'silent': true}));
+                return Promise.all(_converse.chatboxes.map(c => c.messages && c.messages.clearStore({'silent': true})));
             }
         });
         /************************ END Event Handlers ************************/

+ 1 - 1
src/headless/converse-headlines.js

@@ -99,7 +99,7 @@ converse.plugins.add('converse-headlines', {
                     'from': from_jid
                 });
                 const attrs = await chatbox.getMessageAttributesFromStanza(message, message);
-                await chatbox.messages.create(attrs);
+                await chatbox.createMessage(attrs);
                 _converse.api.trigger('message', {'chatbox': chatbox, 'stanza': message});
             }
         }

+ 2 - 4
src/headless/converse-mam.js

@@ -88,9 +88,7 @@ converse.plugins.add('converse-mam', {
                 if (!(await _converse.api.disco.supports(Strophe.NS.MAM, mam_jid))) {
                     return;
                 }
-                const message_handler = is_groupchat ?
-                    this.onMessage.bind(this) :
-                    _converse.handleMessageStanza.bind(_converse.chatboxes);
+                const msg_handler = is_groupchat ? s => this.queueMessage(s) : s => _converse.handleMessageStanza(s);
 
                 const query = Object.assign({
                         'groupchat': is_groupchat,
@@ -102,7 +100,7 @@ converse.plugins.add('converse-mam', {
                 /* eslint-disable no-await-in-loop */
                 for (const message of result.messages) {
                     try {
-                        await message_handler(message);
+                        await msg_handler(message);
                     } catch (e) {
                         log.error(e);
                     }

+ 32 - 17
src/headless/converse-muc.js

@@ -462,7 +462,7 @@ converse.plugins.add('converse-muc', {
             },
 
             async clearMessageQueue () {
-                await Promise.all(this.message_queue.map(m => this.onMessage(m)));
+                await Promise.all(this.message_queue.map(m => this.queueMessage(m)));
                 this.message_queue = [];
             },
 
@@ -573,7 +573,7 @@ converse.plugins.add('converse-muc', {
                             log.warn(`received a mam message with type "chat".`);
                             return true;
                         }
-                        this.onMessage(stanza);
+                        this.queueMessage(stanza);
                         return true;
                     }, null, 'message', 'groupchat', null, room_jid,
                     {'matchBareFromJid': true}
@@ -1766,7 +1766,7 @@ converse.plugins.add('converse-muc', {
              * @returns { Boolean } Returns `true` or `false` depending on
              *  whether a message was moderated or not.
              */
-            handleModeration (attrs) {
+            async handleModeration (attrs) {
                 const MODERATION_ATTRIBUTES = [
                     'editable',
                     'moderated',
@@ -1781,7 +1781,7 @@ converse.plugins.add('converse-muc', {
                     const message = this.messages.findWhere(query);
                     if (!message) {
                         attrs['dangling_moderation'] = true;
-                        this.messages.create(attrs);
+                        await this.createMessage(attrs);
                         return true;
                     }
                     message.save(pick(attrs, MODERATION_ATTRIBUTES));
@@ -1801,19 +1801,32 @@ converse.plugins.add('converse-muc', {
             },
 
             /**
-             * Handler for all MUC messages sent to this groupchat.
+             * Queue an incoming message stanza meant for this {@link _converse.Chatroom} for processing.
+             * @async
              * @private
-             * @method _converse.ChatRoom#onMessage
+             * @method _converse.ChatRoom#queueMessage
              * @param { XMLElement } stanza - The message stanza.
              */
-            async onMessage (stanza) {
-                if (!this.messages.fetched || this.messages.fetched.isPending) {
-                    // We're not ready to accept messages before we've fetched
-                    // from our store, so we stuff them into a queue.
+            queueMessage (stanza) {
+                if (this.messages.fetched) {
+                    this.msg_chain = (this.msg_chain || this.messages.fetched);
+                    this.msg_chain = this.msg_chain.then(() => this.onMessage(stanza));
+                    return this.msg_chain;
+                } else {
                     this.message_queue.push(stanza);
-                    return;
+                    return Promise.resolve();
                 }
+            },
 
+            /**
+             * Handler for all MUC messages sent to this groupchat. This method
+             * shouldn't be called directly, instead {@link _converse.ChatRoom#queueMessage}
+             * should be called.
+             * @private
+             * @method _converse.ChatRoom#onMessage
+             * @param { XMLElement } stanza - The message stanza.
+             */
+            async onMessage (stanza) {
                 if (sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length) {
                     return log.warn('onMessage: Ignoring unencapsulated forwarded groupchat message');
                 }
@@ -1832,7 +1845,7 @@ converse.plugins.add('converse-muc', {
                         return log.warn(`onMessage: Ignoring alleged MAM groupchat message from ${stanza.getAttribute('from')}`);
                     }
                 }
-                this.createInfoMessages(stanza);
+                await this.createInfoMessages(stanza);
                 this.fetchFeaturesIfConfigurationChanged(stanza);
 
                 const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
@@ -1844,8 +1857,8 @@ converse.plugins.add('converse-muc', {
                     return _converse.api.trigger('message', {'stanza': original_stanza});
                 }
 
-                if (this.handleRetraction(attrs) ||
-                        this.handleModeration(attrs) ||
+                if (await this.handleRetraction(attrs) ||
+                        await this.handleModeration(attrs) ||
                         this.subjectChangeHandled(attrs) ||
                         this.ignorableCSN(attrs)) {
                     return _converse.api.trigger('message', {'stanza': original_stanza});
@@ -1853,7 +1866,7 @@ converse.plugins.add('converse-muc', {
                 this.setEditable(attrs, attrs.time);
 
                 if (u.shouldCreateGroupchatMessage(attrs)) {
-                    const msg = this.handleCorrection(attrs) || await this.messages.create(attrs, {promise: true});
+                    const msg = this.handleCorrection(attrs) || await this.createMessage(attrs);
                     this.incrementUnreadMsgCounter(msg);
                 }
                 _converse.api.trigger('message', {'stanza': original_stanza, 'chatbox': this});
@@ -1870,7 +1883,7 @@ converse.plugins.add('converse-muc', {
                             'message': text,
                             'is_ephemeral': true
                         }
-                        this.messages.create(attrs);
+                        this.createMessage(attrs);
                     }
                 }
             },
@@ -1960,7 +1973,7 @@ converse.plugins.add('converse-muc', {
                             // XXX: very naive duplication checking
                             return;
                         }
-                        this.messages.create(data);
+                        this.createMessage(data);
                     }
                 });
             },
@@ -2649,3 +2662,5 @@ converse.plugins.add('converse-muc', {
         /************************ END API ************************/
     }
 });
+
+

+ 1 - 1
tests/utils.js

@@ -123,7 +123,7 @@
         // Opens a new chatroom
         const model = await _converse.api.controlbox.open('controlbox');
         await u.waitUntil(() => model.get('connected'));
-        utils.openControlBox();
+        await utils.openControlBox(_converse);
         const view = await _converse.chatboxviews.get('controlbox');
         const roomspanel = view.roomspanel;
         roomspanel.el.querySelector('.show-add-muc-modal').click();