Преглед изворни кода

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();