Browse Source

Initial support for chat markers. Updates #324

JC Brand 6 years ago
parent
commit
e1f8d53c46
4 changed files with 316 additions and 97 deletions
  1. 104 41
      dist/converse.js
  2. 41 8
      spec/chatbox.js
  3. 88 12
      spec/messages.js
  4. 83 36
      src/headless/converse-chatboxes.js

+ 104 - 41
dist/converse.js

@@ -61304,6 +61304,7 @@ const u = _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].env.utils;
 Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
 Strophe.addNamespace('RECEIPTS', 'urn:xmpp:receipts');
 Strophe.addNamespace('REFERENCE', 'urn:xmpp:reference:0');
+Strophe.addNamespace('MARKERS', 'urn:xmpp:chat-markers:0');
 _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-chatboxes', {
   dependencies: ["converse-roster", "converse-vcard"],
 
@@ -61634,7 +61635,84 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         return false;
       },
 
-      handleReceipt(stanza) {
+      sendMarker(to_jid, id, type) {
+        const stanza = $msg({
+          'from': _converse.connection.jid,
+          'id': _converse.connection.getUniqueId(),
+          'to': to_jid,
+          'type': 'chat'
+        }).c(type, {
+          'xmlns': Strophe.NS.MARKERS,
+          'id': id
+        });
+
+        _converse.api.send(stanza);
+      },
+
+      handleChatMarker(stanza, from_jid, is_carbon) {
+        const to_bare_jid = Strophe.getBareJidFromJid(stanza.getAttribute('to'));
+
+        if (to_bare_jid !== _converse.bare_jid) {
+          return false;
+        }
+
+        const markers = sizzle(`[xmlns="${Strophe.NS.MARKERS}"]`, stanza);
+
+        if (markers.length === 0) {
+          return false;
+        } else if (markers.length > 1) {
+          _converse.log('onMessage: Ignoring incoming stanza with multiple message markers', Strophe.LogLevel.ERROR);
+
+          _converse.log(stanza, Strophe.LogLevel.ERROR);
+
+          return false;
+        } else {
+          const marker = markers.pop();
+
+          if (marker.nodeName === 'markable' && !is_carbon) {
+            this.sendMarker(from_jid, stanza.getAttribute('id'), 'received');
+            return false;
+          } else {
+            const msgid = marker && marker.getAttribute('id'),
+                  message = msgid && this.messages.findWhere({
+              msgid
+            }),
+                  field_name = `marker_${marker.nodeName}`;
+
+            if (message && !message.get(field_name)) {
+              message.save({
+                field_name: moment().format()
+              });
+            }
+
+            return true;
+          }
+        }
+      },
+
+      sendReceiptStanza(to_jid, id) {
+        const receipt_stanza = $msg({
+          'from': _converse.connection.jid,
+          'id': _converse.connection.getUniqueId(),
+          'to': to_jid,
+          'type': 'chat'
+        }).c('received', {
+          'xmlns': Strophe.NS.RECEIPTS,
+          'id': id
+        }).up().c('store', {
+          'xmlns': Strophe.NS.HINTS
+        }).up();
+
+        _converse.api.send(receipt_stanza);
+      },
+
+      handleReceipt(stanza, from_jid, is_carbon, is_me) {
+        const requests_receipt = !_.isUndefined(sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop());
+
+        if (requests_receipt && !is_carbon && !is_me) {
+          this.sendReceiptStanza(from_jid, stanza.getAttribute('id'));
+        }
+
         const to_bare_jid = Strophe.getBareJidFromJid(stanza.getAttribute('to'));
 
         if (to_bare_jid === _converse.bare_jid) {
@@ -61927,6 +62005,23 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         return attrs;
       },
 
+      async createMessage(stanza, original_stanza) {
+        const msgid = stanza.getAttribute('id'),
+              message = msgid && this.messages.findWhere({
+          msgid
+        });
+
+        if (!message) {
+          // Only create the message when we're sure it's not a duplicate
+          const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
+
+          if (attrs['chat_state'] || !u.isEmptyMessage(attrs)) {
+            const msg = this.messages.create(attrs);
+            this.incrementUnreadMsgCounter(msg);
+          }
+        }
+      },
+
       isHidden() {
         /* Returns a boolean to indicate whether a newly received
          * message will be visible to the user or not.
@@ -62078,22 +62173,6 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         }
       },
 
-      sendReceiptStanza(to_jid, id) {
-        const receipt_stanza = $msg({
-          'from': _converse.connection.jid,
-          'id': _converse.connection.getUniqueId(),
-          'to': to_jid,
-          'type': 'chat'
-        }).c('received', {
-          'xmlns': Strophe.NS.RECEIPTS,
-          'id': id
-        }).up().c('store', {
-          'xmlns': Strophe.NS.HINTS
-        }).up();
-
-        _converse.api.send(receipt_stanza);
-      },
-
       async onMessage(stanza) {
         /* Handler method for all incoming single-user chat "message"
          * stanzas.
@@ -62141,12 +62220,6 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
         const from_bare_jid = Strophe.getBareJidFromJid(from_jid),
               from_resource = Strophe.getResourceFromJid(from_jid),
               is_me = from_bare_jid === _converse.bare_jid;
-        const requests_receipt = !_.isUndefined(sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop());
-
-        if (requests_receipt && !is_carbon && !is_me) {
-          this.sendReceiptStanza(from_jid, stanza.getAttribute('id'));
-        }
-
         let contact_jid;
 
         if (is_me) {
@@ -62158,27 +62231,17 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
           contact_jid = Strophe.getBareJidFromJid(to_jid);
         } else {
           contact_jid = from_bare_jid;
-        }
-
-        const attrs = {
-          'fullname': _.get(_converse.api.contacts.get(contact_jid), 'attributes.fullname') // Get chat box, but only create a new one when the message has a body.
+        } // Get chat box, but only create when the message has something to show to the user
 
-        };
-        const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0;
-        const chatbox = this.getChatBox(contact_jid, attrs, has_body);
 
-        if (chatbox && !chatbox.handleMessageCorrection(stanza) && !chatbox.handleReceipt(stanza)) {
-          const msgid = stanza.getAttribute('id'),
-                message = msgid && chatbox.messages.findWhere({
-            msgid
-          });
+        const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0,
+              chatbox_attrs = {
+          'fullname': _.get(_converse.api.contacts.get(contact_jid), 'attributes.fullname')
+        },
+              chatbox = this.getChatBox(contact_jid, chatbox_attrs, has_body);
 
-          if (!message) {
-            // Only create the message when we're sure it's not a duplicate
-            const attrs = await chatbox.getMessageAttributesFromStanza(stanza, original_stanza);
-            const msg = chatbox.messages.create(attrs);
-            chatbox.incrementUnreadMsgCounter(msg);
-          }
+        if (chatbox && !chatbox.handleMessageCorrection(stanza) && !chatbox.handleReceipt(stanza, from_jid, is_carbon, is_me) && !chatbox.handleChatMarker(stanza, from_jid, is_carbon)) {
+          await chatbox.createMessage(stanza, original_stanza);
         }
 
         _converse.emit('message', {

+ 41 - 8
spec/chatbox.js

@@ -530,6 +530,38 @@
 
             describe("A Chat Status Notification", function () {
 
+                it("is ignored when it's a carbon copy of one of my own",
+                    mock.initConverseWithPromises(
+                        null, ['rosterGroupsFetched'], {},
+                        async function (done, _converse) {
+
+                    test_utils.createContacts(_converse, 'current');
+                    test_utils.openControlBox();
+
+                    const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+                    await test_utils.openChatBoxFor(_converse, sender_jid);
+                    let stanza = Strophe.xmlHtmlNode(
+                        `<message from="${sender_jid}"
+                                 type="chat"
+                                 to="dummy@localhost/resource">
+                            <composing xmlns="http://jabber.org/protocol/chatstates"/>
+                            <no-store xmlns="urn:xmpp:hints"/>
+                            <no-permanent-store xmlns="urn:xmpp:hints"/>
+                        </message>`).firstChild;
+                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                    stanza = Strophe.xmlHtmlNode(
+                        `<message from="${sender_jid}"
+                                 type="chat"
+                                 to="dummy@localhost/resource">
+                            <paused xmlns="http://jabber.org/protocol/chatstates"/>
+                            <no-store xmlns="urn:xmpp:hints"/>
+                            <no-permanent-store xmlns="urn:xmpp:hints"/>
+                        </message>`).firstChild;
+                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                    done();
+                }));
+
                 it("does not open a new chatbox",
                     mock.initConverseWithPromises(
                         null, ['rosterGroupsFetched'], {},
@@ -656,12 +688,14 @@
                             async function (done, _converse) {
 
                         test_utils.createContacts(_converse, 'current');
+                        _converse.emit('rosterContactsFetched');
                         test_utils.openControlBox();
 
                         // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
                         spyOn(_converse, 'emit');
                         const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
-                        test_utils.openChatBoxFor(_converse, sender_jid);
+                        await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
+                        await test_utils.openChatBoxFor(_converse, sender_jid);
 
                         // <composing> state
                         let msg = $msg({
@@ -678,7 +712,6 @@
                         await test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
                         // Check that the notification appears inside the chatbox in the DOM
                         let events = view.el.querySelectorAll('.chat-state-notification');
-                        expect(events.length).toBe(1);
                         expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
 
                         // Check that it doesn't appear twice
@@ -687,7 +720,7 @@
                                 to: _converse.connection.jid,
                                 type: 'chat',
                                 id: (new Date()).getTime()
-                            }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+                            }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
                         await _converse.chatboxes.onMessage(msg);
                         events = view.el.querySelectorAll('.chat-state-notification');
                         expect(events.length).toBe(1);
@@ -802,24 +835,24 @@
                                 null, ['rosterGroupsFetched'], {},
                                 async function (done, _converse) {
 
-                        test_utils.createContacts(_converse, 'current');
+                        test_utils.createContacts(_converse, 'current', 2);
+                        _converse.emit('rosterContactsFetched');
                         test_utils.openControlBox();
                         await test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
                         // TODO: only show paused state if the previous state was composing
                         // See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
-                        spyOn(_converse, 'emit');
+                        spyOn(_converse, 'emit').and.callThrough();
                         const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
+                        const view = await test_utils.openChatBoxFor(_converse, sender_jid);
                         // <paused> state
                         const msg = $msg({
                                 from: sender_jid,
                                 to: _converse.connection.jid,
                                 type: 'chat',
                                 id: (new Date()).getTime()
-                            }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+                            }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
                         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));
                         await test_utils.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
                         var event = view.el.querySelector('.chat-info.chat-state-notification');
                         expect(event.textContent).toEqual(mock.cur_names[1] + ' has stopped typing');

+ 88 - 12
spec/messages.js

@@ -313,8 +313,6 @@
             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));
-
             msg = $msg({'id': 'aeb214', 'to': _converse.bare_jid})
                 .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
                     .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'}).up()
@@ -392,7 +390,6 @@
                 .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
                 .tree();
             await _converse.chatboxes.onMessage(msg);
-            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
             msg = $msg({
                     'id': 'aeb220',
@@ -527,7 +524,6 @@
             await test_utils.waitUntil(() => _converse.api.chats.get().length)
             const chatbox = _converse.chatboxes.get(sender_jid);
             const view = _converse.chatboxviews.get(sender_jid);
-            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
                 
             expect(chatbox).toBeDefined();
             expect(view).toBeDefined();
@@ -579,7 +575,6 @@
             // Check that the chatbox and its view now exist
             const chatbox = _converse.chatboxes.get(recipient_jid);
             const view = _converse.chatboxviews.get(recipient_jid);
-            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
             expect(chatbox).toBeDefined();
             expect(view).toBeDefined();
             // Check that the message was received and check the message parameters
@@ -1225,7 +1220,8 @@
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             const msg_id = u.getUniqueId();
             const sent_stanzas = [];
-            spyOn(_converse.chatboxes, 'sendReceiptStanza').and.callThrough();
+            const view = await test_utils.openChatBoxFor(_converse, sender_jid);
+            spyOn(view.model, 'sendReceiptStanza').and.callThrough();
             const msg = $msg({
                     'from': sender_jid,
                     'to': _converse.connection.jid,
@@ -1243,7 +1239,7 @@
                 .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
             await _converse.chatboxes.onMessage(msg);
             await test_utils.waitUntil(() => _converse.api.chats.get().length);
-            expect(_converse.chatboxes.sendReceiptStanza).not.toHaveBeenCalled();
+            expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
             done();
         }));
 
@@ -1255,7 +1251,8 @@
             const recipient_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             const msg_id = u.getUniqueId();
             const sent_stanzas = [];
-            spyOn(_converse.chatboxes, 'sendReceiptStanza').and.callThrough();
+            const view = await test_utils.openChatBoxFor(_converse, recipient_jid);
+            spyOn(view.model, 'sendReceiptStanza').and.callThrough();
             const msg = $msg({
                     'from': converse.bare_jid,
                     'to': _converse.connection.jid,
@@ -1272,7 +1269,7 @@
                 .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
             await _converse.chatboxes.onMessage(msg);
             await test_utils.waitUntil(() => _converse.api.chats.get().length);
-            expect(_converse.chatboxes.sendReceiptStanza).not.toHaveBeenCalled();
+            expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
             done();
         }));
 
@@ -1482,7 +1479,6 @@
                     // Check that the chatbox and its view now exist
                     const chatbox = _converse.chatboxes.get(sender_jid);
                     const view = _converse.chatboxviews.get(sender_jid);
-                    await new Promise((resolve, reject) => view.once('messageInserted', resolve));
 
                     expect(chatbox).toBeDefined();
                     expect(view).toBeDefined();
@@ -1531,7 +1527,6 @@
 
                     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));
 
                     expect(_converse.emit).toHaveBeenCalledWith('message', jasmine.any(Object));
                     // onMessage is a handler for received XMPP messages
@@ -1843,7 +1838,6 @@
                 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));
                 await test_utils.waitUntil(() => view.model.messages.length);
                 expect(_converse.chatboxes.getChatBox).toHaveBeenCalled();
                 var chat_content = $(view.el).find('.chat-content:last')[0];
@@ -2023,6 +2017,88 @@
         });
     });
 
+    describe("A XEP-0333 Chat Marker", function () {
+
+        it("is sent when a markable message is received",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {},
+                async function (done, _converse) {
+
+            test_utils.createContacts(_converse, 'current', 1);
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            await test_utils.openChatBoxFor(_converse, contact_jid);
+            const view = await _converse.api.chatviews.get(contact_jid);
+            const msgid = u.getUniqueId();
+            const stanza = Strophe.xmlHtmlNode(`
+                <message from='${contact_jid}'
+                    id='${msgid}'
+                    type="chat"
+                    to='${_converse.jid}'>
+                  <body>My lord, dispatch; read o'er these articles.</body>
+                  <markable xmlns='urn:xmpp:chat-markers:0'/>
+                </message>`).firstElementChild;
+
+            const sent_stanzas = [];
+            spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
+            spyOn(view.model, 'sendMarker').and.callThrough();
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+            await test_utils.waitUntil(() => view.model.sendMarker.calls.count() === 1);
+            expect(Strophe.serialize(sent_stanzas[0])).toBe(
+                `<message from="dummy@localhost/resource" `+
+                        `id="${sent_stanzas[0].nodeTree.getAttribute('id')}" `+
+                        `to="${contact_jid}" type="chat" xmlns="jabber:client">`+
+                `<received id="${msgid}" xmlns="urn:xmpp:chat-markers:0"/>`+
+                `</message>`);
+            done();
+        }));
+
+        it("is ignored if it's a carbon copy of one that I sent from a different client",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {},
+                async function (done, _converse) {
+
+            test_utils.createContacts(_converse, 'current', 1);
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            await test_utils.openChatBoxFor(_converse, contact_jid);
+            const view = await _converse.api.chatviews.get(contact_jid);
+
+            let stanza = Strophe.xmlHtmlNode(`
+                <message xmlns="jabber:client"
+                         to="${_converse.bare_jid}"
+                         type="chat"
+                         id="2e972ea0-0050-44b7-a830-f6638a2595b3"
+                         from="${contact_jid}">
+                    <body>😊</body>
+                    <markable xmlns="urn:xmpp:chat-markers:0"/>
+                    <origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
+                    <stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
+                </message>`).firstElementChild;
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+            expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+            expect(view.model.messages.length).toBe(1);
+
+            stanza = Strophe.xmlHtmlNode(
+                `<message xmlns="jabber:client" to="${_converse.bare_jid}" type="chat" from="${contact_jid}">
+                    <sent xmlns="urn:xmpp:carbons:2">
+                        <forwarded xmlns="urn:xmpp:forward:0">
+                            <message xmlns="jabber:client" to="${contact_jid}" type="chat" from="${_converse.bare_jid}/other-resource">
+                                <received xmlns="urn:xmpp:chat-markers:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
+                                <store xmlns="urn:xmpp:hints"/>
+                                <stanza-id xmlns="urn:xmpp:sid:0" id="F4TC6CvHwzqRbeHb" by="jc@opkode.com"/>
+                            </message>
+                        </forwarded>
+                    </sent>
+                </message>`).firstElementChild;
+            spyOn(_converse, 'emit').and.callThrough();
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+            await test_utils.waitUntil(() => _converse.emit.calls.count() === 1);
+            expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
+            expect(view.model.messages.length).toBe(1);
+            done();
+        }));
+    });
+
 
     describe("A Groupchat Message", function () {
 

+ 83 - 36
src/headless/converse-chatboxes.js

@@ -15,6 +15,7 @@ const u = converse.env.utils;
 Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
 Strophe.addNamespace('RECEIPTS', 'urn:xmpp:receipts');
 Strophe.addNamespace('REFERENCE', 'urn:xmpp:reference:0');
+Strophe.addNamespace('MARKERS', 'urn:xmpp:chat-markers:0');
 
 
 converse.plugins.add('converse-chatboxes', {
@@ -312,8 +313,66 @@ converse.plugins.add('converse-chatboxes', {
                 }
                 return false;
             },
+            
+            sendMarker(to_jid, id, type) {
+                const stanza = $msg({
+                    'from': _converse.connection.jid,
+                    'id': _converse.connection.getUniqueId(),
+                    'to': to_jid,
+                    'type': 'chat',
+                }).c(type, {'xmlns': Strophe.NS.MARKERS, 'id': id});
+                _converse.api.send(stanza);
+            },
 
-            handleReceipt (stanza) {
+            handleChatMarker (stanza, from_jid, is_carbon) {
+                const to_bare_jid = Strophe.getBareJidFromJid(stanza.getAttribute('to'));
+                if (to_bare_jid !== _converse.bare_jid) {
+                    return false;
+                }
+                const markers = sizzle(`[xmlns="${Strophe.NS.MARKERS}"]`, stanza);
+                if (markers.length === 0) {
+                    return false;
+                } else if (markers.length > 1) {
+                    _converse.log(
+                        'onMessage: Ignoring incoming stanza with multiple message markers',
+                        Strophe.LogLevel.ERROR
+                    );
+                    _converse.log(stanza, Strophe.LogLevel.ERROR);
+                    return false;
+                } else {
+                    const marker = markers.pop();
+                    if (marker.nodeName === 'markable' && !is_carbon) {
+                        this.sendMarker(from_jid, stanza.getAttribute('id'), 'received');
+                        return false;
+                    } else {
+                        const msgid = marker && marker.getAttribute('id'),
+                            message = msgid && this.messages.findWhere({msgid}),
+                            field_name = `marker_${marker.nodeName}`;
+
+                        if (message && !message.get(field_name)) {
+                            message.save({field_name: moment().format()});
+                        }
+                        return true;
+                    }
+                }
+            },
+
+            sendReceiptStanza (to_jid, id) {
+                const receipt_stanza = $msg({
+                    'from': _converse.connection.jid,
+                    'id': _converse.connection.getUniqueId(),
+                    'to': to_jid,
+                    'type': 'chat',
+                }).c('received', {'xmlns': Strophe.NS.RECEIPTS, 'id': id}).up()
+                .c('store', {'xmlns': Strophe.NS.HINTS}).up();
+                _converse.api.send(receipt_stanza);
+            },
+
+            handleReceipt (stanza, from_jid, is_carbon, is_me) {
+                const requests_receipt = !_.isUndefined(sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop());
+                if (requests_receipt && !is_carbon && !is_me) {
+                    this.sendReceiptStanza(from_jid, stanza.getAttribute('id'));
+                }
                 const to_bare_jid = Strophe.getBareJidFromJid(stanza.getAttribute('to'));
                 if (to_bare_jid === _converse.bare_jid) {
                     const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop();
@@ -321,9 +380,7 @@ converse.plugins.add('converse-chatboxes', {
                         const msgid = receipt && receipt.getAttribute('id'),
                             message = msgid && this.messages.findWhere({msgid});
                         if (message && !message.get('received')) {
-                            message.save({
-                                'received': moment().format()
-                            });
+                            message.save({'received': moment().format()});
                         }
                         return true;
                     }
@@ -575,6 +632,19 @@ converse.plugins.add('converse-chatboxes', {
                 return attrs;
             },
 
+            async createMessage (stanza, original_stanza) {
+                const msgid = stanza.getAttribute('id'),
+                      message = msgid && this.messages.findWhere({msgid});
+                if (!message) {
+                    // Only create the message when we're sure it's not a duplicate
+                    const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
+                    if (attrs['chat_state'] || !u.isEmptyMessage(attrs)) {
+                        const msg = this.messages.create(attrs);
+                        this.incrementUnreadMsgCounter(msg);
+                    }
+                }
+            },
+
             isHidden () {
                 /* Returns a boolean to indicate whether a newly received
                  * message will be visible to the user or not.
@@ -708,17 +778,6 @@ converse.plugins.add('converse-chatboxes', {
                 }
             },
 
-            sendReceiptStanza (to_jid, id) {
-                const receipt_stanza = $msg({
-                    'from': _converse.connection.jid,
-                    'id': _converse.connection.getUniqueId(),
-                    'to': to_jid,
-                    'type': 'chat',
-                }).c('received', {'xmlns': Strophe.NS.RECEIPTS, 'id': id}).up()
-                .c('store', {'xmlns': Strophe.NS.HINTS}).up();
-                _converse.api.send(receipt_stanza);
-            },
-
             async onMessage (stanza) {
                 /* Handler method for all incoming single-user chat "message"
                  * stanzas.
@@ -765,16 +824,10 @@ converse.plugins.add('converse-chatboxes', {
                     from_jid = stanza.getAttribute('from');
                     to_jid = stanza.getAttribute('to');
                 }
-
                 const from_bare_jid = Strophe.getBareJidFromJid(from_jid),
                       from_resource = Strophe.getResourceFromJid(from_jid),
                       is_me = from_bare_jid === _converse.bare_jid;
 
-                const requests_receipt = !_.isUndefined(sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop());
-                if (requests_receipt && !is_carbon && !is_me) {
-                    this.sendReceiptStanza(from_jid, stanza.getAttribute('id'));
-                }
-
                 let contact_jid;
                 if (is_me) {
                     // I am the sender, so this must be a forwarded message...
@@ -788,21 +841,15 @@ converse.plugins.add('converse-chatboxes', {
                 } else {
                     contact_jid = from_bare_jid;
                 }
-                const attrs = {
-                    'fullname': _.get(_converse.api.contacts.get(contact_jid), 'attributes.fullname')
-                }
-                // Get chat box, but only create a new one when the message has a body.
-                const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0;
-                const chatbox = this.getChatBox(contact_jid, attrs, has_body);
-                if (chatbox && !chatbox.handleMessageCorrection(stanza) && !chatbox.handleReceipt(stanza)) {
-                    const msgid = stanza.getAttribute('id'),
-                          message = msgid && chatbox.messages.findWhere({msgid});
-                    if (!message) {
-                        // Only create the message when we're sure it's not a duplicate
-                        const attrs = await chatbox.getMessageAttributesFromStanza(stanza, original_stanza);
-                        const msg = chatbox.messages.create(attrs);
-                        chatbox.incrementUnreadMsgCounter(msg);
-                    }
+                // Get chat box, but only create when the message has something to show to the user
+                const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0,
+                      chatbox_attrs = {'fullname': _.get(_converse.api.contacts.get(contact_jid), 'attributes.fullname')},
+                      chatbox = this.getChatBox(contact_jid, chatbox_attrs, has_body);
+                if (chatbox &&
+                        !chatbox.handleMessageCorrection(stanza) &&
+                        !chatbox.handleReceipt (stanza, from_jid, is_carbon, is_me) &&
+                        !chatbox.handleChatMarker(stanza, from_jid, is_carbon)) {
+                    await chatbox.createMessage(stanza, original_stanza);
                 }
                 _converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox});
             },