Prechádzať zdrojové kódy

Refactor out `createMessage`.

Changes:

* Avoids leaky abstraction of MUC code into converse-chatboxes
* Avoid creating unnecessary message objects (e.g. without <body)
* Add fix for #1369.
* Rename spec/chatroom.js to spec/muc.js
JC Brand 6 rokov pred
rodič
commit
3c0e3d3fab

+ 46 - 50
dist/converse.js

@@ -51920,11 +51920,11 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_1__["default"].plugins
       'afterShown': _.noop
     });
 
-    function onHeadlineMessage(message) {
+    async function onHeadlineMessage(message) {
       /* Handler method for all incoming messages of type "headline". */
-      const from_jid = message.getAttribute('from');
-
       if (utils.isHeadlineMessage(_converse, message)) {
+        const from_jid = message.getAttribute('from');
+
         if (_.includes(from_jid, '@') && !_converse.api.contacts.get(from_jid) && !_converse.allow_non_roster_messaging) {
           return;
         }
@@ -51942,19 +51942,21 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_1__["default"].plugins
           'from': from_jid
         });
 
-        chatbox.createMessage(message, message);
+        const attrs = await chatbox.getMessageAttributesFromStanza(message, message);
+        await chatbox.messages.create(attrs);
 
         _converse.emit('message', {
           'chatbox': chatbox,
           'stanza': message
         });
       }
-
-      return true;
     }
 
     function registerHeadlineHandler() {
-      _converse.connection.addHandler(onHeadlineMessage, null, 'message');
+      _converse.connection.addHandler(message => {
+        onHeadlineMessage(message);
+        return true;
+      }, null, 'message');
     }
 
     _converse.on('connected', registerHeadlineHandler);
@@ -61803,8 +61805,10 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
           'is_delayed': !_.isNil(delay),
           'is_spoiler': !_.isNil(spoiler),
           'message': _converse.chatboxes.getMessageBody(stanza) || undefined,
-          'references': this.getReferencesFromStanza(stanza),
           'msgid': stanza.getAttribute('id'),
+          'references': this.getReferencesFromStanza(stanza),
+          'subject': _.propertyOf(stanza.querySelector('subject'))('textContent'),
+          'thread': _.propertyOf(stanza.querySelector('thread'))('textContent'),
           'time': delay ? delay.getAttribute('stamp') : moment().format(),
           'type': stanza.getAttribute('type')
         };
@@ -61837,25 +61841,6 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         return attrs;
       },
 
-      async createMessage(message, original_stanza) {
-        /* Create a Backbone.Message object inside this chat box
-         * based on the identified message stanza.
-         */
-        const attrs = await this.getMessageAttributesFromStanza(message, original_stanza),
-              is_csn = u.isOnlyChatStateNotification(attrs);
-
-        if (is_csn && (attrs.is_delayed || attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick'))) {
-          // XXX: MUC leakage
-          // No need showing delayed or our own CSN messages
-          return;
-        } else if (!is_csn && !attrs.file && !attrs.plaintext && !attrs.message && !attrs.oob_url && attrs.type !== 'error') {
-          // TODO: handle <subject> messages (currently being done by ChatRoom)
-          return;
-        } else {
-          return this.messages.create(attrs);
-        }
-      },
-
       isHidden() {
         /* Returns a boolean to indicate whether a newly received
          * message will be visible to the user or not.
@@ -61952,7 +61937,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         });
       },
 
-      onErrorMessage(message) {
+      async onErrorMessage(message) {
         /* Handler method for all incoming error message stanzas
         */
         const from_jid = Strophe.getBareJidFromJid(message.getAttribute('from'));
@@ -61990,8 +61975,8 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
           _converse.log(message, Strophe.LogLevel.ERROR);
         }
 
-        chatbox.createMessage(message, message);
-        return true;
+        const attrs = await chatbox.getMessageAttributesFromStanza(message, message);
+        chatbox.messages.create(attrs);
       },
 
       getMessageBody(stanza) {
@@ -62023,7 +62008,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         _converse.api.send(receipt_stanza);
       },
 
-      onMessage(stanza) {
+      async onMessage(stanza) {
         /* Handler method for all incoming single-user chat "message"
          * stanzas.
          *
@@ -62104,7 +62089,9 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
 
           if (!message) {
             // Only create the message when we're sure it's not a duplicate
-            chatbox.createMessage(stanza, original_stanza).then(msg => chatbox.incrementUnreadMsgCounter(msg)).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+            const attrs = await chatbox.getMessageAttributesFromStanza(stanza, original_stanza);
+            const msg = chatbox.messages.create(attrs);
+            chatbox.incrementUnreadMsgCounter(msg);
           }
         }
 
@@ -62112,8 +62099,6 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
           'stanza': original_stanza,
           'chatbox': chatbox
         });
-
-        return true;
       },
 
       getChatBox(jid, attrs = {}, create) {
@@ -65298,7 +65283,9 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam
 
           this.addSpinner();
 
-          _converse.api.archive.query(_.extend({
+          _converse.api.archive.query( // TODO: only query from the last message we have
+          // in our history
+          _.extend({
             'groupchat': is_groupchat,
             'before': '',
             // Page backwards from the most recent message
@@ -66839,28 +66826,37 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
           return;
         }
 
-        const jid = stanza.getAttribute('from'),
-              resource = Strophe.getResourceFromJid(jid),
-              sender = resource && Strophe.unescapeNode(resource) || '';
-
-        if (!this.handleMessageCorrection(stanza)) {
-          if (sender === '') {
-            return;
-          }
+        const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
 
-          const subject_el = stanza.querySelector('subject');
+        if (!attrs.nick) {
+          return;
+        }
 
-          if (subject_el) {
-            const subject = _.propertyOf(subject_el)('textContent') || '';
+        if (!this.handleMessageCorrection(stanza)) {
+          if (attrs.subject && !attrs.thread && !attrs.message) {
+            // https://xmpp.org/extensions/xep-0045.html#subject-mod
+            // -----------------------------------------------------
+            // The subject is changed by sending a message of type "groupchat" to the <room@service>,
+            // where the <message/> MUST contain a <subject/> element that specifies the new subject but
+            // MUST NOT contain a <body/> element (or a <thread/> element).
             _utils_form__WEBPACK_IMPORTED_MODULE_7__["default"].safeSave(this, {
               'subject': {
-                'author': sender,
-                'text': subject
+                'author': attrs.nick,
+                'text': attrs.subject || ''
               }
             });
+            return;
+          }
+
+          const is_csn = _utils_form__WEBPACK_IMPORTED_MODULE_7__["default"].isOnlyChatStateNotification(attrs),
+                own_message = Strophe.getResourceFromJid(attrs.from) == this.get('nick');
+
+          if (is_csn && (attrs.is_delayed || own_message)) {
+            // No need showing delayed or our own CSN messages
+            return;
           }
 
-          const msg = await this.createMessage(stanza, original_stanza);
+          const msg = await this.messages.create(attrs);
 
           if (forwarded && msg && msg.get('sender') === 'me') {
             msg.save({
@@ -66871,7 +66867,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
           this.incrementUnreadMsgCounter(msg);
         }
 
-        if (sender !== this.get('nick')) {
+        if (attrs.nick !== this.get('nick')) {
           // We only emit an event if it's not our own message
           _converse.emit('message', {
             'stanza': original_stanza,

+ 104 - 107
spec/autocomplete.js

@@ -62,114 +62,111 @@
         it("autocompletes when the user presses tab",
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
-
-            test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy')
-            .then(() => {
-                const view = _converse.chatboxviews.get('lounge@localhost');
-                expect(view.model.occupants.length).toBe(1);
-                let presence = $pres({
-                        'to': 'dummy@localhost/resource',
-                        'from': 'lounge@localhost/some1'
-                    })
-                    .c('x', {xmlns: Strophe.NS.MUC_USER})
-                    .c('item', {
-                        'affiliation': 'none',
-                        'jid': 'some1@localhost/resource',
-                        'role': 'participant'
-                    });
-                _converse.connection._dataRecv(test_utils.createRequest(presence));
-                expect(view.model.occupants.length).toBe(2);
-
-                const textarea = view.el.querySelector('textarea.chat-textarea');
-                textarea.value = "hello som";
-
-                // Press tab
-                const tab_event = {
-                    'target': textarea,
-                    'preventDefault': _.noop,
-                    'stopPropagation': _.noop,
-                    'keyCode': 9
-                }
-                view.keyPressed(tab_event);
-                view.keyUp(tab_event);
-                expect(view.el.querySelector('.suggestion-box__results').hidden).toBeFalsy();
-                expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(1);
-                expect(view.el.querySelector('.suggestion-box__results li').textContent).toBe('some1');
-
-                const backspace_event = {
-                    'target': textarea,
-                    'preventDefault': _.noop,
-                    'keyCode': 8
-                }
-                for (var i=0; i<3; i++) {
-                    // Press backspace 3 times to remove "som"
-                    view.keyPressed(backspace_event);
-                    textarea.value = textarea.value.slice(0, textarea.value.length-1)
-                    view.keyUp(backspace_event);
-                }
-                expect(view.el.querySelector('.suggestion-box__results').hidden).toBeTruthy();
-
-                presence = $pres({
-                        'to': 'dummy@localhost/resource',
-                        'from': 'lounge@localhost/some2'
-                    })
-                    .c('x', {xmlns: Strophe.NS.MUC_USER})
-                    .c('item', {
-                        'affiliation': 'none',
-                        'jid': 'some2@localhost/resource',
-                        'role': 'participant'
-                    });
-                _converse.connection._dataRecv(test_utils.createRequest(presence));
-
-                textarea.value = "hello s s";
-                view.keyPressed(tab_event);
-                view.keyUp(tab_event);
-                expect(view.el.querySelector('.suggestion-box__results').hidden).toBeFalsy();
-                expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2);
-
-                const up_arrow_event = {
-                    'target': textarea,
-                    'preventDefault': () => (up_arrow_event.defaultPrevented = true),
-                    'stopPropagation': _.noop,
-                    'keyCode': 38
-                }
-                view.keyPressed(up_arrow_event);
-                view.keyUp(up_arrow_event);
-                expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2);
-                expect(view.el.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1');
-                expect(view.el.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2');
-
-                view.keyPressed({
-                    'target': textarea,
-                    'preventDefault': _.noop,
-                    'stopPropagation': _.noop,
-                    'keyCode': 13 // Enter
+                    async function (done, _converse) {
+
+            await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy');
+            const view = _converse.chatboxviews.get('lounge@localhost');
+            expect(view.model.occupants.length).toBe(1);
+            let presence = $pres({
+                    'to': 'dummy@localhost/resource',
+                    'from': 'lounge@localhost/some1'
+                })
+                .c('x', {xmlns: Strophe.NS.MUC_USER})
+                .c('item', {
+                    'affiliation': 'none',
+                    'jid': 'some1@localhost/resource',
+                    'role': 'participant'
                 });
-                expect(textarea.value).toBe('hello s @some2 ');
-
-                // Test that pressing tab twice selects
-                presence = $pres({
-                        'to': 'dummy@localhost/resource',
-                        'from': 'lounge@localhost/z3r0'
-                    })
-                    .c('x', {xmlns: Strophe.NS.MUC_USER})
-                    .c('item', {
-                        'affiliation': 'none',
-                        'jid': 'z3r0@localhost/resource',
-                        'role': 'participant'
-                    });
-                _converse.connection._dataRecv(test_utils.createRequest(presence));
-                textarea.value = "hello z";
-                view.keyPressed(tab_event);
-                view.keyUp(tab_event);
-
-                view.keyPressed(tab_event);
-                view.keyUp(tab_event);
-                expect(textarea.value).toBe('hello @z3r0 ');
-
-                done();
-            }).catch(_.partial(console.error, _));
+            _converse.connection._dataRecv(test_utils.createRequest(presence));
+            expect(view.model.occupants.length).toBe(2);
+
+            const textarea = view.el.querySelector('textarea.chat-textarea');
+            textarea.value = "hello som";
+
+            // Press tab
+            const tab_event = {
+                'target': textarea,
+                'preventDefault': _.noop,
+                'stopPropagation': _.noop,
+                'keyCode': 9
+            }
+            view.keyPressed(tab_event);
+            view.keyUp(tab_event);
+            expect(view.el.querySelector('.suggestion-box__results').hidden).toBeFalsy();
+            expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(1);
+            expect(view.el.querySelector('.suggestion-box__results li').textContent).toBe('some1');
+
+            const backspace_event = {
+                'target': textarea,
+                'preventDefault': _.noop,
+                'keyCode': 8
+            }
+            for (var i=0; i<3; i++) {
+                // Press backspace 3 times to remove "som"
+                view.keyPressed(backspace_event);
+                textarea.value = textarea.value.slice(0, textarea.value.length-1)
+                view.keyUp(backspace_event);
+            }
+            expect(view.el.querySelector('.suggestion-box__results').hidden).toBeTruthy();
+
+            presence = $pres({
+                    'to': 'dummy@localhost/resource',
+                    'from': 'lounge@localhost/some2'
+                })
+                .c('x', {xmlns: Strophe.NS.MUC_USER})
+                .c('item', {
+                    'affiliation': 'none',
+                    'jid': 'some2@localhost/resource',
+                    'role': 'participant'
+                });
+            _converse.connection._dataRecv(test_utils.createRequest(presence));
+
+            textarea.value = "hello s s";
+            view.keyPressed(tab_event);
+            view.keyUp(tab_event);
+            expect(view.el.querySelector('.suggestion-box__results').hidden).toBeFalsy();
+            expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2);
+
+            const up_arrow_event = {
+                'target': textarea,
+                'preventDefault': () => (up_arrow_event.defaultPrevented = true),
+                'stopPropagation': _.noop,
+                'keyCode': 38
+            }
+            view.keyPressed(up_arrow_event);
+            view.keyUp(up_arrow_event);
+            expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2);
+            expect(view.el.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1');
+            expect(view.el.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2');
+
+            view.keyPressed({
+                'target': textarea,
+                'preventDefault': _.noop,
+                'stopPropagation': _.noop,
+                'keyCode': 13 // Enter
+            });
+            expect(textarea.value).toBe('hello s @some2 ');
+
+            // Test that pressing tab twice selects
+            presence = $pres({
+                    'to': 'dummy@localhost/resource',
+                    'from': 'lounge@localhost/z3r0'
+                })
+                .c('x', {xmlns: Strophe.NS.MUC_USER})
+                .c('item', {
+                    'affiliation': 'none',
+                    'jid': 'z3r0@localhost/resource',
+                    'role': 'participant'
+                });
+            _converse.connection._dataRecv(test_utils.createRequest(presence));
+            textarea.value = "hello z";
+            view.keyPressed(tab_event);
+            view.keyUp(tab_event);
+
+            view.keyPressed(tab_event);
+            view.keyUp(tab_event);
+            expect(textarea.value).toBe('hello @z3r0 ');
+            done();
         }));
     });
 }));

+ 26 - 28
spec/chatbox.js

@@ -44,7 +44,7 @@
                         type: 'chat',
                         id: (new Date()).getTime()
                     }).c('body').t('hello world').tree();
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 await test_utils.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
                 expect(view.content.lastElementChild.textContent.trim().indexOf('hello world')).not.toBe(-1);
                 done();
@@ -71,7 +71,7 @@
                     }).c('body').t(message).up()
                     .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
 
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 const view = _converse.chatboxviews.get(sender_jid);
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                 expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1);
@@ -322,7 +322,7 @@
                 done();
             }));
 
-           it("can be closed by clicking a DOM element with class 'close-chatbox-button'",
+            it("can be closed by clicking a DOM element with class 'close-chatbox-button'",
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
@@ -533,7 +533,7 @@
                 it("does not open a new chatbox",
                     mock.initConverseWithPromises(
                         null, ['rosterGroupsFetched'], {},
-                        function (done, _converse) {
+                        async function (done, _converse) {
 
                     test_utils.createContacts(_converse, 'current');
                     test_utils.openControlBox();
@@ -547,7 +547,7 @@
                             'type': 'chat',
                             'id': (new Date()).getTime()
                         }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                    _converse.chatboxes.onMessage(msg);
+                    await _converse.chatboxes.onMessage(msg);
                     expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                     done();
                 }));
@@ -660,21 +660,20 @@
 
                         // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
                         spyOn(_converse, 'emit');
-                        var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
                         test_utils.openChatBoxFor(_converse, sender_jid);
 
                         // <composing> state
-                        var msg = $msg({
+                        let msg = $msg({
                                 from: sender_jid,
                                 to: _converse.connection.jid,
                                 type: 'chat',
                                 id: (new Date()).getTime()
                             }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        _converse.chatboxes.onMessage(msg);
+                        await _converse.chatboxes.onMessage(msg);
                         expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                         var view = _converse.chatboxviews.get(sender_jid);
                         expect(view).toBeDefined();
-                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
                         await test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
                         // Check that the notification appears inside the chatbox in the DOM
@@ -689,8 +688,7 @@
                                 type: 'chat',
                                 id: (new Date()).getTime()
                             }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        _converse.chatboxes.onMessage(msg);
-                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        await _converse.chatboxes.onMessage(msg);
                         events = view.el.querySelectorAll('.chat-state-notification');
                         expect(events.length).toBe(1);
                         expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
@@ -725,7 +723,7 @@
                                     'to': recipient_jid,
                                     'type': 'chat'
                             }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        _converse.chatboxes.onMessage(msg);
+                        await _converse.chatboxes.onMessage(msg);
                         await test_utils.waitUntil(() => view.model.messages.length);
                         // Check that the chatbox and its view now exist
                         var chatbox = _converse.chatboxes.get(recipient_jid);
@@ -818,7 +816,7 @@
                                 type: 'chat',
                                 id: (new Date()).getTime()
                             }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        _converse.chatboxes.onMessage(msg);
+                        await _converse.chatboxes.onMessage(msg);
                         expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                         const view = _converse.chatboxviews.get(sender_jid);
                         await new Promise((resolve, reject) => view.once('messageInserted', resolve));
@@ -856,7 +854,7 @@
                                     'to': recipient_jid,
                                     'type': 'chat'
                             }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        _converse.chatboxes.onMessage(msg);
+                        await _converse.chatboxes.onMessage(msg);
                         await test_utils.waitUntil(() => view.model.messages.length);
                         // Check that the chatbox and its view now exist
                         var chatbox = _converse.chatboxes.get(recipient_jid);
@@ -998,7 +996,7 @@
                                 'type': 'chat'})
                             .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
                             .tree();
-                        _converse.chatboxes.onMessage(msg);
+                        await _converse.chatboxes.onMessage(msg);
                         await test_utils.waitUntil(() => view.model.messages.length);
                         expect(view.el.querySelectorAll('.chat-state-notification').length).toBe(1);
                         msg = $msg({
@@ -1007,7 +1005,7 @@
                                 type: 'chat',
                                 id: (new Date()).getTime()
                             }).c('body').c('inactive', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        _converse.chatboxes.onMessage(msg);
+                        await _converse.chatboxes.onMessage(msg);
                         await test_utils.waitUntil(() => (view.model.messages.length > 1));
                         expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                         expect(view.el.querySelectorAll('.chat-state-notification').length).toBe(0);
@@ -1034,7 +1032,7 @@
                                 type: 'chat',
                                 id: (new Date()).getTime()
                             }).c('body').c('gone', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        _converse.chatboxes.onMessage(msg);
+                        await _converse.chatboxes.onMessage(msg);
                         expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                         const view = _converse.chatboxviews.get(sender_jid);
                         await test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1]);
@@ -1121,7 +1119,7 @@
                 spyOn(_converse, 'incrementMsgCounter').and.callThrough();
                 spyOn(_converse, 'clearMsgCounter').and.callThrough();
 
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                 expect(_converse.incrementMsgCounter).toHaveBeenCalled();
                 expect(_converse.clearMsgCounter).not.toHaveBeenCalled();
@@ -1149,7 +1147,7 @@
             it("is not incremented when the message is received and the window is focused",
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
-                    function (done, _converse) {
+                    async function (done, _converse) {
 
                 test_utils.createContacts(_converse, 'current');
                 test_utils.openControlBox();
@@ -1157,8 +1155,8 @@
                 expect(_converse.msg_counter).toBe(0);
                 spyOn(_converse, 'incrementMsgCounter').and.callThrough();
                 _converse.saveWindowState(null, 'focus');
-                var message = 'This message will not increment the message counter';
-                var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
+                const message = 'This message will not increment the message counter';
+                const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
                     msg = $msg({
                         from: sender_jid,
                         to: _converse.connection.jid,
@@ -1166,7 +1164,7 @@
                         id: (new Date()).getTime()
                     }).c('body').t(message).up()
                       .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 expect(_converse.incrementMsgCounter).not.toHaveBeenCalled();
                 expect(_converse.msg_counter).toBe(0);
                 done();
@@ -1237,7 +1235,7 @@
 
                 const view = await test_utils.openChatBoxFor(_converse, sender_jid)
                 view.model.save('scrolled', true);
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 await test_utils.waitUntil(() => view.model.messages.length);
                 expect(view.model.get('num_unread')).toBe(1);
                 done();
@@ -1256,7 +1254,7 @@
 
                 await test_utils.openChatBoxFor(_converse, sender_jid);
                 const chatbox = _converse.chatboxes.get(sender_jid);
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 expect(chatbox.get('num_unread')).toBe(0);
                 done();
             }));
@@ -1359,14 +1357,14 @@
                 const chatbox = _converse.chatboxes.get(sender_jid);
                 chatbox.save('scrolled', true);
                 msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread');
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 await test_utils.waitUntil(() => chatbox.messages.length);
                 const selector = 'a.open-chat:contains("' + chatbox.get('fullname') + '") .msgs-indicator';
                 indicator_el = sizzle(selector, _converse.rosterview.el).pop();
                 expect(indicator_el.textContent).toBe('1');
 
                 msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread too');
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 await test_utils.waitUntil(() => chatbox.messages.length > 1);
                 indicator_el = sizzle(selector, _converse.rosterview.el).pop();
                 expect(indicator_el.textContent).toBe('2');
@@ -1390,14 +1388,14 @@
                 chatboxview.minimize();
 
                 msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread');
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 await test_utils.waitUntil(() => chatbox.messages.length);
                 const selector = 'a.open-chat:contains("' + chatbox.get('fullname') + '") .msgs-indicator';
                 indicator_el = sizzle(selector, _converse.rosterview.el).pop();
                 expect(indicator_el.textContent).toBe('1');
 
                 msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread too');
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 await test_utils.waitUntil(() => chatbox.messages.length > 1);
                 indicator_el = sizzle(selector, _converse.rosterview.el).pop();
                 expect(indicator_el.textContent).toBe('2');

+ 51 - 53
spec/messages.js

@@ -310,7 +310,7 @@
                         'type': 'chat'})
                     .c('body').t("message")
                     .tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await test_utils.waitUntil(() => _converse.api.chats.get().length);
             const view = _converse.chatboxviews.get(sender_jid);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
@@ -325,7 +325,7 @@
                         'type': 'chat'})
                     .c('body').t("Older message")
                     .tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             msg = $msg({'id': 'aeb215', 'to': _converse.bare_jid})
@@ -338,7 +338,7 @@
                         'type': 'chat'})
                     .c('body').t("Inbetween message").up()
                     .tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             msg = $msg({'id': 'aeb216', 'to': _converse.bare_jid})
@@ -351,7 +351,7 @@
                         'type': 'chat'})
                     .c('body').t("another inbetween message")
                     .tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             msg = $msg({'id': 'aeb217', 'to': _converse.bare_jid})
@@ -364,7 +364,7 @@
                         'type': 'chat'})
                     .c('body').t("An earlier message on the next day")
                     .tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             msg = $msg({'id': 'aeb218', 'to': _converse.bare_jid})
@@ -377,7 +377,7 @@
                         'type': 'chat'})
                     .c('body').t("newer message from the next day")
                     .tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             // Insert <composing> message, to also check that
@@ -391,7 +391,7 @@
                     'type': 'chat'})
                 .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
                 .tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             msg = $msg({
@@ -403,7 +403,7 @@
                 .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
                 .c('body').t("latest message")
                 .tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             const chat_content = view.el.querySelector('.chat-content');
@@ -464,7 +464,7 @@
         it("is ignored if it's a malformed headline message",
         mock.initConverseWithPromises(
             null, ['rosterGroupsFetched'], {},
-            function (done, _converse) {
+            async function (done, _converse) {
 
             test_utils.createContacts(_converse, 'current');
             test_utils.openControlBox();
@@ -481,7 +481,7 @@
                     type: 'chat',
                     id: (new Date()).getTime()
                 }).c('body').t("This headline message will not be shown").tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             expect(_converse.log.calledWith(
                 "onMessage: Ignoring incoming headline message from JID: localhost",
                 Strophe.LogLevel.INFO
@@ -523,7 +523,7 @@
                         'to': _converse.bare_jid+'/another-resource',
                         'type': 'chat'
                 }).c('body').t(msgtext).tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await test_utils.waitUntil(() => _converse.api.chats.get().length)
             const chatbox = _converse.chatboxes.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);
@@ -574,7 +574,7 @@
                         'to': recipient_jid,
                         'type': 'chat'
                 }).c('body').t(msgtext).tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await test_utils.waitUntil(() => _converse.api.chats.get().length);
             // Check that the chatbox and its view now exist
             const chatbox = _converse.chatboxes.get(recipient_jid);
@@ -598,25 +598,25 @@
         it("will be discarded if it's a malicious message meant to look like a carbon copy",
         mock.initConverseWithPromises(
             null, ['rosterGroupsFetched'], {},
-            function (done, _converse) {
+            async function (done, _converse) {
 
             test_utils.createContacts(_converse, 'current');
             test_utils.openControlBox();
             /* <message from="mallory@evil.example" to="b@xmpp.example">
-            *    <received xmlns='urn:xmpp:carbons:2'>
-            *      <forwarded xmlns='urn:xmpp:forward:0'>
-            *          <message from="alice@xmpp.example" to="bob@xmpp.example/client1">
-            *              <body>Please come to Creepy Valley tonight, alone!</body>
-            *          </message>
-            *      </forwarded>
-            *    </received>
-            * </message>
-            */
+             *    <received xmlns='urn:xmpp:carbons:2'>
+             *      <forwarded xmlns='urn:xmpp:forward:0'>
+             *          <message from="alice@xmpp.example" to="bob@xmpp.example/client1">
+             *              <body>Please come to Creepy Valley tonight, alone!</body>
+             *          </message>
+             *      </forwarded>
+             *    </received>
+             * </message>
+             */
             spyOn(_converse, 'log');
-            var msgtext = 'Please come to Creepy Valley tonight, alone!';
-            var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
-            var impersonated_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
-            var msg = $msg({
+            const msgtext = 'Please come to Creepy Valley tonight, alone!';
+            const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
+            const impersonated_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
+            const msg = $msg({
                     'from': sender_jid,
                     'id': (new Date()).getTime(),
                     'to': _converse.connection.jid,
@@ -630,10 +630,10 @@
                         'to': _converse.connection.jid,
                         'type': 'chat'
                 }).c('body').t(msgtext).tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
 
             // Check that chatbox for impersonated user is not created.
-            var chatbox = _converse.chatboxes.get(impersonated_jid);
+            let chatbox = _converse.chatboxes.get(impersonated_jid);
             expect(chatbox).not.toBeDefined();
 
             // Check that the chatbox for the malicous user is not created
@@ -673,7 +673,7 @@
                 id: (new Date()).getTime()
             }).c('body').t(message).up()
             .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
 
             await test_utils.waitUntil(() => chatview.model.messages.length);
             expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
@@ -734,7 +734,7 @@
             }).c('body').t(message).up()
             .c('delay', { xmlns:'urn:xmpp:delay', from: 'localhost', stamp: one_day_ago.format() })
             .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
@@ -766,7 +766,7 @@
                 id: new Date().getTime()
             }).c('body').t(message).up()
             .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
@@ -1196,7 +1196,7 @@
         it("received may emit a message delivery receipt",
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                function (done, _converse) {
+                async function (done, _converse) {
             test_utils.createContacts(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             const msg_id = u.getUniqueId();
@@ -1211,7 +1211,7 @@
                     'id': msg_id,
                 }).c('body').t('Message!').up()
                 .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, sent_stanzas[0].tree()).pop();
             expect(Strophe.serialize(receipt)).toBe(`<received id="${msg_id}" xmlns="${Strophe.NS.RECEIPTS}"/>`);
             done();
@@ -1241,7 +1241,7 @@
                         'id': msg_id
                 }).c('body').t('Message!').up()
                 .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await test_utils.waitUntil(() => _converse.api.chats.get().length);
             expect(_converse.chatboxes.sendReceiptStanza).not.toHaveBeenCalled();
             done();
@@ -1270,7 +1270,7 @@
                         'id': msg_id
                 }).c('body').t('Message!').up()
                 .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
-            _converse.chatboxes.onMessage(msg);
+            await _converse.chatboxes.onMessage(msg);
             await test_utils.waitUntil(() => _converse.api.chats.get().length);
             expect(_converse.chatboxes.sendReceiptStanza).not.toHaveBeenCalled();
             done();
@@ -1475,7 +1475,7 @@
                     // We don't already have an open chatbox for this user
                     expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
 
-                    _converse.chatboxes.onMessage(msg);
+                    await _converse.chatboxes.onMessage(msg);
                     await test_utils.waitUntil(() => _converse.api.chats.get().length);
                     expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
 
@@ -1527,7 +1527,7 @@
                     let chatbox = _converse.chatboxes.get(sender_jid);
                     expect(chatbox).not.toBeDefined();
                     // onMessage is a handler for received XMPP messages
-                    _converse.chatboxes.onMessage(msg);
+                    await _converse.chatboxes.onMessage(msg);
 
                     await test_utils.waitUntil(() => _converse.api.chats.get().length)
                     const view = _converse.chatboxviews.get(sender_jid);
@@ -1536,7 +1536,7 @@
                     expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                     // onMessage is a handler for received XMPP messages
                     _converse.allow_non_roster_messaging =true;
-                    _converse.chatboxes.onMessage(msg);
+                    await _converse.chatboxes.onMessage(msg);
                     await test_utils.waitUntil(() => view.model.messages.length);
                     expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                     // Check that the chatbox and its view now exist
@@ -1824,7 +1824,7 @@
                         id: (new Date()).getTime()
                     }).c('body').t("This message will not be shown").up()
                     .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 await test_utils.waitUntil(() => _converse.api.chats.get().length);
                 expect(_converse.log).toHaveBeenCalledWith(
                         "onMessage: Ignoring incoming message intended for a different resource: dummy@localhost/some-other-resource",
@@ -1840,7 +1840,7 @@
                         id: '134234623462346'
                     }).c('body').t(message).up()
                     .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-                _converse.chatboxes.onMessage(msg);
+                await _converse.chatboxes.onMessage(msg);
                 await test_utils.waitUntil(() => _converse.chatboxviews.keys().length > 1, 1000);
                 const view = _converse.chatboxviews.get(sender_jid);
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
@@ -2043,9 +2043,9 @@
                     to: 'dummy@localhost',
                     type: 'groupchat'
                 }).c('body').t(message).tree();
-            view.model.onMessage(msg);
+            await view.model.onMessage(msg);
             await new Promise((resolve, reject) => view.once('messageInserted', resolve));
-            expect($(view.el).find('.chat-msg').hasClass('mentioned')).toBeTruthy();
+            expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy();
             done();
         }));
 
@@ -2064,8 +2064,7 @@
                     to: 'dummy@localhost',
                     type: 'groupchat'
                 }).c('body').t('I wrote this message!').tree();
-            view.model.onMessage(msg);
-            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+            await view.model.onMessage(msg);
             expect(view.model.messages.last().get('sender')).toBe('me');
             done();
         }));
@@ -2091,7 +2090,7 @@
                 }).tree();
             _converse.connection._dataRecv(test_utils.createRequest(stanza));
             const msg_id = u.getUniqueId();
-            view.model.onMessage($msg({
+            await view.model.onMessage($msg({
                     'from': 'lounge@localhost/newguy',
                     'to': _converse.connection.jid,
                     'type': 'groupchat',
@@ -2102,10 +2101,10 @@
             expect(view.el.querySelector('.chat-msg__text').textContent)
                 .toBe('But soft, what light through yonder airlock breaks?');
 
-            view.model.onMessage($msg({
+            await view.model.onMessage($msg({
                     'from': 'lounge@localhost/newguy',
                     'to': _converse.connection.jid,
-                    'type': 'chat',
+                    'type': 'groupchat',
                     'id': u.getUniqueId(),
                 }).c('body').t('But soft, what light through yonder chimney breaks?').up()
                     .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
@@ -2114,10 +2113,10 @@
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
             expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
 
-            view.model.onMessage($msg({
+            await view.model.onMessage($msg({
                     'from': 'lounge@localhost/newguy',
                     'to': _converse.connection.jid,
-                    'type': 'chat',
+                    'type': 'groupchat',
                     'id': u.getUniqueId(),
                 }).c('body').t('But soft, what light through yonder window breaks?').up()
                     .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
@@ -2207,7 +2206,7 @@
             expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false);
 
             // Check that messages from other users are skipped
-            view.model.onMessage($msg({
+            await view.model.onMessage($msg({
                 'from': room_jid+'/someone-else',
                 'id': (new Date()).getTime(),
                 'to': 'dummy@localhost',
@@ -2264,8 +2263,7 @@
                     'to': 'dummy@localhost',
                     'type': 'groupchat',
                 }).c('body').t(body).up().tree();
-            view.model.onMessage(msg);
-            await new Promise((resolve, reject) => view.model.messages.once('rendered', resolve));
+            await view.model.onMessage(msg);
             expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(1);
             done();
         }));
@@ -2302,7 +2300,7 @@
                         .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'6', 'end':'10', 'type':'mention', 'uri':'xmpp:z3r0@localhost'}).up()
                         .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:dummy@localhost'}).up()
                         .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@localhost'}).nodeTree;
-                view.model.onMessage(msg);
+                await view.model.onMessage(msg);
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                 expect(view.el.querySelectorAll('.chat-msg__text').length).toBe(1);
                 expect(view.el.querySelector('.chat-msg__text').outerHTML).toBe(

+ 19 - 24
spec/chatroom.js → spec/muc.js

@@ -411,7 +411,7 @@
                         'type': 'groupchat'
                     }).c('body').t(message).tree();
 
-                view.model.onMessage(msg);
+                await view.model.onMessage(msg);
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                 view.el.querySelector('.chat-msg__text a').click();
                 await test_utils.waitUntil(() => _converse.chatboxes.length === 3)
@@ -933,7 +933,7 @@
                         'type': 'groupchat'
                     }).c('body').t('Some message').tree();
 
-                view.model.onMessage(msg);
+                await view.model.onMessage(msg);
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
                 let stanza = Strophe.xmlHtmlNode(
@@ -1151,7 +1151,7 @@
                         'to': 'dummy@localhost',
                         'type': 'groupchat'
                     }).c('body').t(message).tree();
-                view.model.onMessage(msg);
+                await view.model.onMessage(msg);
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                 expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Dyon van de Wege')).toBeTruthy();
                 expect(view.el.querySelector('.chat-msg__text').textContent).toBe('is tired');
@@ -1163,7 +1163,7 @@
                     to: 'dummy@localhost',
                     type: 'groupchat'
                 }).c('body').t(message).tree();
-                view.model.onMessage(msg);
+                await view.model.onMessage(msg);
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                 expect(_.includes(sizzle('.chat-msg__author:last', view.el).pop().textContent, '**Max Mustermann')).toBeTruthy();
                 expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe('is as well');
@@ -1836,7 +1836,7 @@
                     to: 'dummy@localhost',
                     type: 'groupchat'
                 }).c('body').t(text);
-                view.model.onMessage(message.nodeTree);
+                await view.model.onMessage(message.nodeTree);
                 await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                 const chat_content = view.el.querySelector('.chat-content');
                 expect(chat_content.querySelectorAll('.chat-msg').length).toBe(1);
@@ -1878,7 +1878,7 @@
                     type: 'groupchat',
                     id: view.model.messages.at(0).get('msgid')
                 }).c('body').t(text);
-                view.model.onMessage(message.nodeTree);
+                await view.model.onMessage(message.nodeTree);
                 expect(chat_content.querySelectorAll('.chat-msg').length).toBe(1);
                 expect(sizzle('.chat-msg__text:last').pop().textContent).toBe(text);
                 expect(view.model.messages.length).toBe(1);
@@ -1912,7 +1912,7 @@
                 // Give enough time for `markScrolled` to have been called
                 setTimeout(async () => {
                     view.content.scrollTop = 0;
-                    view.model.onMessage(
+                    await view.model.onMessage(
                         $msg({
                             from: 'lounge@localhost/someone',
                             to: 'dummy@localhost.com',
@@ -1945,6 +1945,7 @@
                     '</message>').firstChild;
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
                 const view = _converse.chatboxviews.get('jdev@conference.jabber.org');
+                await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                 const chat_content = view.el.querySelector('.chat-content');
                 expect(sizzle('.chat-event:last').pop().textContent).toBe('Topic set by ralphm');
                 expect(sizzle('.chat-topic:last').pop().textContent).toBe(text);
@@ -3999,7 +4000,7 @@
                 var contact_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
                 const nick = mock.chatroom_names[0];
 
-                view.model.onMessage($msg({
+                await view.model.onMessage($msg({
                         from: room_jid+'/'+nick,
                         id: (new Date()).getTime(),
                         to: 'dummy@localhost',
@@ -4010,7 +4011,7 @@
                 expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(1);
                 expect(roomspanel.el.querySelector('.msgs-indicator').textContent).toBe('1');
 
-                view.model.onMessage($msg({
+                await view.model.onMessage($msg({
                     'from': room_jid+'/'+nick,
                     'id': (new Date()).getTime(),
                     'to': 'dummy@localhost',
@@ -4076,14 +4077,14 @@
                         // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
 
                         // <composing> state
-                        var msg = $msg({
+                        let msg = $msg({
                                 from: room_jid+'/newguy',
                                 id: (new Date()).getTime(),
                                 to: 'dummy@localhost',
                                 type: 'groupchat'
                             }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
 
-                        view.model.onMessage(msg);
+                        await view.model.onMessage(msg);
                         await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-state-notification').length);
 
                         // Check that the notification appears inside the chatbox in the DOM
@@ -4109,8 +4110,7 @@
                                 to: 'dummy@localhost',
                                 type: 'groupchat'
                             }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        view.model.onMessage(msg);
-                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        await view.model.onMessage(msg);
 
                         events = view.el.querySelectorAll('.chat-event');
                         expect(events.length).toBe(3);
@@ -4131,8 +4131,7 @@
                                 to: 'dummy@localhost',
                                 type: 'groupchat'
                             }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        view.model.onMessage(msg);
-                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        await view.model.onMessage(msg);
                         events = view.el.querySelectorAll('.chat-event');
                         expect(events.length).toBe(3);
                         expect(events[0].textContent).toEqual('some1 has entered the groupchat');
@@ -4153,7 +4152,7 @@
                             to: 'dummy@localhost',
                             type: 'groupchat'
                         }).c('body').t('hello world').tree();
-                        view.model.onMessage(msg);
+                        await view.model.onMessage(msg);
                         await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
                         const messages = view.el.querySelectorAll('.message');
@@ -4259,8 +4258,7 @@
                                 to: 'dummy@localhost',
                                 type: 'groupchat'
                             }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        view.model.onMessage(msg);
-                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        await view.model.onMessage(msg);
 
                         // Check that the notification appears inside the chatbox in the DOM
                         var events = view.el.querySelectorAll('.chat-event');
@@ -4280,8 +4278,7 @@
                                 to: 'dummy@localhost',
                                 type: 'groupchat'
                             }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        view.model.onMessage(msg);
-                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        await view.model.onMessage(msg);
 
                         events = view.el.querySelectorAll('.chat-event');
                         expect(events.length).toBe(3);
@@ -4300,8 +4297,7 @@
                                 to: 'dummy@localhost',
                                 type: 'groupchat'
                             }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        view.model.onMessage(msg);
-                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        await view.model.onMessage(msg);
                         events = view.el.querySelectorAll('.chat-event');
                         expect(events.length).toBe(3);
                         expect(events[0].textContent).toEqual('some1 has entered the groupchat');
@@ -4320,8 +4316,7 @@
                                 to: 'dummy@localhost',
                                 type: 'groupchat'
                             }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-                        view.model.onMessage(msg);
-                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        await view.model.onMessage(msg);
                         events = view.el.querySelectorAll('.chat-event');
                         expect(events.length).toBe(3);
                         expect(events[0].textContent).toEqual('some1 has entered the groupchat');

+ 52 - 46
spec/notification.js

@@ -16,16 +16,17 @@
                     it("is shown when a new private message is received",
                         mock.initConverseWithPromises(
                             null, ['rosterGroupsFetched'], {},
-                            function (done, _converse) {
+                            async function (done, _converse) {
 
                         // TODO: not yet testing show_desktop_notifications setting
                         test_utils.createContacts(_converse, 'current');
+                        await test_utils.createContacts(_converse, 'current');
                         spyOn(_converse, 'showMessageNotification').and.callThrough();
                         spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
                         spyOn(_converse, 'isMessageToHiddenChat').and.returnValue(true);
-                        
-                        var message = 'This message will show a desktop notification';
-                        var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
+
+                        const message = 'This message will show a desktop notification';
+                        const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
                             msg = $msg({
                                 from: sender_jid,
                                 to: _converse.connection.jid,
@@ -33,60 +34,63 @@
                                 id: (new Date()).getTime()
                             }).c('body').t(message).up()
                             .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
-                        _converse.chatboxes.onMessage(msg); // This will emit 'message'
+                        await _converse.chatboxes.onMessage(msg); // This will emit 'message'
+                        await test_utils.waitUntil(() => _converse.api.chatviews.get(sender_jid));
                         expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled();
                         expect(_converse.showMessageNotification).toHaveBeenCalled();
                         done();
                     }));
 
-                    it("is shown when you are mentioned in a chat room",
+                    it("is shown when you are mentioned in a groupchat",
                         mock.initConverseWithPromises(
                             null, ['rosterGroupsFetched'], {},
-                            function (done, _converse) {
-
-                        test_utils.createContacts(_converse, 'current');
-                        test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy').then(function () {
-                            var view = _converse.chatboxviews.get('lounge@localhost');
-                            if (!$(view.el).find('.chat-area').length) { view.renderChatArea(); }
-                            var no_notification = false;
-                            if (typeof window.Notification === 'undefined') {
-                                no_notification = true;
-                                window.Notification = function () {
-                                    return {
-                                        'close': function () {}
-                                    };
+                            async function (done, _converse) {
+
+                        await test_utils.createContacts(_converse, 'current');
+                        await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy');
+                        const view = _converse.api.chatviews.get('lounge@localhost');
+                        if (!view.el.querySelectorAll('.chat-area').length) {
+                            view.renderChatArea();
+                        }
+                        let no_notification = false;
+                        if (typeof window.Notification === 'undefined') {
+                            no_notification = true;
+                            window.Notification = function () {
+                                return {
+                                    'close': function () {}
                                 };
-                            }
-                            spyOn(_converse, 'showMessageNotification').and.callThrough();
-                            spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
-                            
-                            var message = 'dummy: This message will show a desktop notification';
-                            var nick = mock.chatroom_names[0],
-                                msg = $msg({
-                                    from: 'lounge@localhost/'+nick,
-                                    id: (new Date()).getTime(),
-                                    to: 'dummy@localhost',
-                                    type: 'groupchat'
-                                }).c('body').t(message).tree();
-                            _converse.chatboxes.onMessage(msg); // This will emit 'message'
-                            expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled();
-                            expect(_converse.showMessageNotification).toHaveBeenCalled();
-                            if (no_notification) {
-                                delete window.Notification;
-                            }
-                            done();
-                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                            };
+                        }
+                        spyOn(_converse, 'showMessageNotification').and.callThrough();
+                        spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
+
+                        const message = 'dummy: This message will show a desktop notification';
+                        const nick = mock.chatroom_names[0],
+                            msg = $msg({
+                                from: 'lounge@localhost/'+nick,
+                                id: (new Date()).getTime(),
+                                to: 'dummy@localhost',
+                                type: 'groupchat'
+                            }).c('body').t(message).tree();
+                        await _converse.chatboxes.onMessage(msg); // This will emit 'message'
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                        expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled();
+                        expect(_converse.showMessageNotification).toHaveBeenCalled();
+                        if (no_notification) {
+                            delete window.Notification;
+                        }
+                        done();
                     }));
 
                     it("is shown for headline messages",
                         mock.initConverseWithPromises(
                             null, ['rosterGroupsFetched'], {},
-                            function (done, _converse) {
+                            async function (done, _converse) {
 
                         spyOn(_converse, 'showMessageNotification').and.callThrough();
                         spyOn(_converse, 'isMessageToHiddenChat').and.returnValue(true);
                         spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
-                        var stanza = $msg({
+                        const stanza = $msg({
                                 'type': 'headline',
                                 'from': 'notify.example.com',
                                 'to': 'dummy@localhost',
@@ -97,6 +101,9 @@
                             .c('x', {'xmlns': 'jabber:x:oob'})
                             .c('url').t('imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18');
                         _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                        await test_utils.waitUntil(() => _converse.chatboxviews.keys().length);
+                        const view = _converse.chatboxviews.get('notify.example.com');
+                        await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                         expect(
                             _.includes(_converse.chatboxviews.keys(),
                                 'notify.example.com')
@@ -156,7 +163,7 @@
         describe("When play_sounds is set to true", function () {
             describe("A notification sound", function () {
 
-                it("is played when the current user is mentioned in a chat room",
+                it("is played when the current user is mentioned in a groupchat",
                     mock.initConverseWithPromises(
                         null, ['rosterGroupsFetched'], {},
                         async function (done, _converse) {
@@ -176,8 +183,7 @@
                         to: 'dummy@localhost',
                         type: 'groupchat'
                     }).c('body').t(text);
-                    view.model.onMessage(message.nodeTree);
-
+                    await view.model.onMessage(message.nodeTree);
                     await test_utils.waitUntil(() => _converse.playSoundNotification.calls.count());
                     expect(_converse.playSoundNotification).toHaveBeenCalled();
 
@@ -188,7 +194,7 @@
                         to: 'dummy@localhost',
                         type: 'groupchat'
                     }).c('body').t(text);
-                    view.model.onMessage(message.nodeTree);
+                    await view.model.onMessage(message.nodeTree);
                     expect(_converse.playSoundNotification, 1);
                     _converse.play_sounds = false;
 
@@ -199,7 +205,7 @@
                         to: 'dummy@localhost',
                         type: 'groupchat'
                     }).c('body').t(text);
-                    view.model.onMessage(message.nodeTree);
+                    await view.model.onMessage(message.nodeTree);
                     expect(_converse.playSoundNotification, 1);
                     _converse.play_sounds = false;
                     done();

+ 3 - 4
spec/roomslist.js

@@ -285,7 +285,7 @@
             view.model.set({'minimized': true});
             const contact_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
             const nick = mock.chatroom_names[0];
-            view.model.onMessage(
+            await view.model.onMessage(
                 $msg({
                     from: room_jid+'/'+nick,
                     id: (new Date()).getTime(),
@@ -293,13 +293,12 @@
                     type: 'groupchat'
                 }).c('body').t('foo').tree());
 
-            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
             // If the user isn't mentioned, the counter doesn't get incremented, but the text of the groupchat is bold
             let room_el = _converse.rooms_list_view.el.querySelector(".available-chatroom");
             expect(_.includes(room_el.classList, 'unread-msgs')).toBeTruthy();
 
             // If the user is mentioned, the counter also gets updated
-            view.model.onMessage(
+            await view.model.onMessage(
                 $msg({
                     from: room_jid+'/'+nick,
                     id: (new Date()).getTime(),
@@ -311,7 +310,7 @@
             spyOn(view.model, 'incrementUnreadMsgCounter').and.callThrough();
             let indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
             expect(indicator_el.textContent).toBe('1');
-            view.model.onMessage(
+            await view.model.onMessage(
                 $msg({
                     from: room_jid+'/'+nick,
                     id: (new Date()).getTime(),

+ 8 - 5
src/converse-headline.js

@@ -105,10 +105,10 @@ converse.plugins.add('converse-headline', {
             'afterShown': _.noop
         });
 
-        function onHeadlineMessage (message) {
+        async function onHeadlineMessage (message) {
             /* Handler method for all incoming messages of type "headline". */
-            const from_jid = message.getAttribute('from');
             if (utils.isHeadlineMessage(_converse, message)) {
+                const from_jid = message.getAttribute('from');
                 if (_.includes(from_jid, '@') && 
                         !_converse.api.contacts.get(from_jid) &&
                         !_converse.allow_non_roster_messaging) {
@@ -125,14 +125,17 @@ converse.plugins.add('converse-headline', {
                     'type': _converse.HEADLINES_TYPE,
                     'from': from_jid
                 });
-                chatbox.createMessage(message, message);
+                const attrs = await chatbox.getMessageAttributesFromStanza(message, message);
+                await chatbox.messages.create(attrs);
                 _converse.emit('message', {'chatbox': chatbox, 'stanza': message});
             }
-            return true;
         }
 
         function registerHeadlineHandler () {
-            _converse.connection.addHandler(onHeadlineMessage, null, 'message');
+            _converse.connection.addHandler(message => {
+                onHeadlineMessage(message);
+                return true
+            }, null, 'message');
         }
         _converse.on('connected', registerHeadlineHandler);
         _converse.on('reconnected', registerHeadlineHandler);

+ 10 - 28
src/headless/converse-chatboxes.js

@@ -544,8 +544,10 @@ converse.plugins.add('converse-chatboxes', {
                     'is_delayed': !_.isNil(delay),
                     'is_spoiler': !_.isNil(spoiler),
                     'message': _converse.chatboxes.getMessageBody(stanza) || undefined,
-                    'references': this.getReferencesFromStanza(stanza),
                     'msgid': stanza.getAttribute('id'),
+                    'references': this.getReferencesFromStanza(stanza),
+                    'subject': _.propertyOf(stanza.querySelector('subject'))('textContent'),
+                    'thread': _.propertyOf(stanza.querySelector('thread'))('textContent'),
                     'time': delay ? delay.getAttribute('stamp') : moment().format(),
                     'type': stanza.getAttribute('type')
                 };
@@ -573,25 +575,6 @@ converse.plugins.add('converse-chatboxes', {
                 return attrs;
             },
 
-            async createMessage (message, original_stanza) {
-                /* Create a Backbone.Message object inside this chat box
-                 * based on the identified message stanza.
-                 */
-                const attrs = await this.getMessageAttributesFromStanza(message, original_stanza),
-                      is_csn = u.isOnlyChatStateNotification(attrs);
-
-                if (is_csn && (attrs.is_delayed || (attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick')))) {
-                    // XXX: MUC leakage
-                    // No need showing delayed or our own CSN messages
-                    return;
-                } else if (!is_csn && !attrs.file && !attrs.plaintext && !attrs.message && !attrs.oob_url && attrs.type !== 'error') {
-                    // TODO: handle <subject> messages (currently being done by ChatRoom)
-                    return;
-                } else {
-                    return this.messages.create(attrs);
-                }
-            },
-
             isHidden () {
                 /* Returns a boolean to indicate whether a newly received
                  * message will be visible to the user or not.
@@ -680,7 +663,7 @@ converse.plugins.add('converse-chatboxes', {
                 });
             },
 
-            onErrorMessage (message) {
+            async onErrorMessage (message) {
                 /* Handler method for all incoming error message stanzas
                 */
                 const from_jid =  Strophe.getBareJidFromJid(message.getAttribute('from'));
@@ -708,8 +691,8 @@ converse.plugins.add('converse-chatboxes', {
                     _converse.log('Received an error message without id attribute!', Strophe.LogLevel.ERROR);
                     _converse.log(message, Strophe.LogLevel.ERROR);
                 }
-                chatbox.createMessage(message, message);
-                return true;
+                const attrs = await chatbox.getMessageAttributesFromStanza(message, message);
+                chatbox.messages.create(attrs);
             },
 
             getMessageBody (stanza) {
@@ -736,7 +719,7 @@ converse.plugins.add('converse-chatboxes', {
                 _converse.api.send(receipt_stanza);
             },
 
-            onMessage (stanza) {
+            async onMessage (stanza) {
                 /* Handler method for all incoming single-user chat "message"
                  * stanzas.
                  *
@@ -816,13 +799,12 @@ converse.plugins.add('converse-chatboxes', {
                           message = msgid && chatbox.messages.findWhere({msgid});
                     if (!message) {
                         // Only create the message when we're sure it's not a duplicate
-                        chatbox.createMessage(stanza, original_stanza)
-                            .then(msg => chatbox.incrementUnreadMsgCounter(msg))
-                            .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                        const attrs = await chatbox.getMessageAttributesFromStanza(stanza, original_stanza);
+                        const msg = chatbox.messages.create(attrs);
+                        chatbox.incrementUnreadMsgCounter(msg);
                     }
                 }
                 _converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox});
-                return true;
             },
 
             getChatBox (jid, attrs={}, create) {

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

@@ -218,6 +218,8 @@ converse.plugins.add('converse-mam', {
                         if (!results.length) { return; }
                         this.addSpinner();
                         _converse.api.archive.query(
+                            // TODO: only query from the last message we have
+                            // in our history
                             _.extend({
                                 'groupchat': is_groupchat,
                                 'before': '', // Page backwards from the most recent message

+ 19 - 12
src/headless/converse-muc.js

@@ -987,33 +987,40 @@ converse.plugins.add('converse-muc', {
 
                 const original_stanza = stanza,
                       forwarded = sizzle(`forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).pop();
-
                 if (forwarded) {
                     stanza = forwarded.querySelector('message');
                 }
                 if (this.isDuplicate(stanza, original_stanza)) {
                     return;
                 }
-                const jid = stanza.getAttribute('from'),
-                      resource = Strophe.getResourceFromJid(jid),
-                      sender = resource && Strophe.unescapeNode(resource) || '';
-
+                const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
+                if (!attrs.nick) {
+                    return;
+                }
                 if (!this.handleMessageCorrection(stanza)) {
-                    if (sender === '') {
+                    if (attrs.subject && !attrs.thread && !attrs.message) {
+                        // https://xmpp.org/extensions/xep-0045.html#subject-mod
+                        // -----------------------------------------------------
+                        // The subject is changed by sending a message of type "groupchat" to the <room@service>,
+                        // where the <message/> MUST contain a <subject/> element that specifies the new subject but
+                        // MUST NOT contain a <body/> element (or a <thread/> element).
+                        u.safeSave(this, {'subject': {'author': attrs.nick, 'text': attrs.subject || ''}});
                         return;
                     }
-                    const subject_el = stanza.querySelector('subject');
-                    if (subject_el) {
-                        const subject = _.propertyOf(subject_el)('textContent') || '';
-                        u.safeSave(this, {'subject': {'author': sender, 'text': subject}});
+
+                    const is_csn = u.isOnlyChatStateNotification(attrs),
+                          own_message = Strophe.getResourceFromJid(attrs.from) == this.get('nick');
+                    if (is_csn && (attrs.is_delayed || own_message)) {
+                        // No need showing delayed or our own CSN messages
+                        return;
                     }
-                    const msg = await this.createMessage(stanza, original_stanza);
+                    const msg = await this.messages.create(attrs);
                     if (forwarded && msg && msg.get('sender')  === 'me') {
                         msg.save({'received': moment().format()});
                     }
                     this.incrementUnreadMsgCounter(msg);
                 }
-                if (sender !== this.get('nick')) {
+                if (attrs.nick !== this.get('nick')) {
                     // We only emit an event if it's not our own message
                     _converse.emit('message', {'stanza': original_stanza, 'chatbox': this});
                 }

+ 1 - 1
tests/runner.js

@@ -112,7 +112,7 @@ var specs = [
     "spec/chatbox",
     "spec/user-details-modal",
     "spec/messages",
-    "spec/chatroom",
+    "spec/muc",
     "spec/room_registration",
     "spec/autocomplete",
     "spec/minchats",