Browse Source

Add support for the XEP-0333 `displayed` chat marker

Credit for this work goes to @deleolajide
JC Brand 5 years ago
parent
commit
5a57ded243
7 changed files with 102 additions and 37 deletions
  1. 1 0
      CHANGES.md
  2. 14 7
      package-lock.json
  3. 36 1
      spec/chatbox.js
  4. 2 2
      spec/messages.js
  5. 1 0
      spec/mock.js
  6. 27 13
      src/headless/converse-chat.js
  7. 21 14
      src/headless/converse-muc.js

+ 1 - 0
CHANGES.md

@@ -24,6 +24,7 @@ Soon we'll deprecate the latter, so prepare now.
 - #1999: Demarcate first unread message
 - #2002: fix rendering of `muc_roomid_policy_hint`
 - #2006: fix rendering of emojis in case `use_system_emojis == false`
+- #2028: Implement XEP-0333 `displayed` chat marker
 - Filter roster contacts via all available information (JID, nickname and VCard full name).
 - Allow ignoring of bootstrap modules at build using environment variable. For xample: `export BOOTSTRAP_IGNORE_MODULES="Modal,Dropdown" && make dist`
 - Bugfix. Handle stanza that clears the MUC subject

+ 14 - 7
package-lock.json

@@ -2244,7 +2244,8 @@
 			"dependencies": {
 				"filesize": {
 					"version": "6.1.0",
-					"resolved": false
+					"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
+					"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg=="
 				},
 				"fs-extra": {
 					"version": "8.1.0",
@@ -2278,7 +2279,8 @@
 				},
 				"jed": {
 					"version": "1.1.1",
-					"resolved": false
+					"resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz",
+					"integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ="
 				},
 				"jsonfile": {
 					"version": "5.0.0",
@@ -2299,18 +2301,21 @@
 				},
 				"localforage": {
 					"version": "1.7.3",
-					"resolved": false,
+					"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
+					"integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
 					"requires": {
 						"lie": "3.1.1"
 					}
 				},
 				"lodash": {
 					"version": "4.17.15",
-					"resolved": false
+					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+					"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
 				},
 				"pluggable.js": {
 					"version": "2.0.1",
-					"resolved": false,
+					"resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-2.0.1.tgz",
+					"integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==",
 					"requires": {
 						"lodash": "^4.17.11"
 					}
@@ -2324,11 +2329,13 @@
 				},
 				"strophe.js": {
 					"version": "1.3.4",
-					"resolved": false
+					"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.3.4.tgz",
+					"integrity": "sha512-jSLDG8jolhAwGOSgiJ7DTMSYK3wVoEJHKtpVRyEacQZ6CWA6z2WRPJpcFMjsIweq5aP9/XIvKUQqHBu/ZhvESA=="
 				},
 				"twemoji": {
 					"version": "12.1.5",
-					"resolved": false,
+					"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-12.1.5.tgz",
+					"integrity": "sha512-B0PBVy5xomwb1M/WZxf/IqPZfnoIYy1skXnlHjMwLwTNfZ9ljh8VgWQktAPcJXu8080WoEh6YwQGPVhDVqvrVQ==",
 					"requires": {
 						"fs-extra": "^8.0.1",
 						"jsonfile": "^5.0.0",

+ 36 - 1
spec/chatbox.js

@@ -1326,13 +1326,19 @@ describe("Chatboxes", function () {
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
                   msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
 
+            const sent_stanzas = [];
+            spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
+
             const view = await mock.openChatBoxFor(_converse, sender_jid)
+            spyOn(view.model, 'sendMarker').and.callThrough();
             view.model.save('scrolled', true);
             await _converse.handleMessageStanza(msg);
             await u.waitUntil(() => view.model.messages.length);
             expect(view.model.get('num_unread')).toBe(1);
             const msgid = view.model.messages.last().get('id');
             expect(view.model.get('first_unread_id')).toBe(msgid);
+            await u.waitUntil(() => view.model.sendMarker.calls.count() === 1);
+            expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
             done();
         }));
 
@@ -1345,11 +1351,15 @@ describe("Chatboxes", function () {
 
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
                   msg = mock.createChatMessage(_converse, sender_jid, 'This message will be read');
-
+            const sent_stanzas = [];
+            spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
+            spyOn(chatbox, 'sendMarker').and.callThrough();
             await _converse.handleMessageStanza(msg);
             expect(chatbox.get('num_unread')).toBe(0);
+            await u.waitUntil(() => chatbox.sendMarker.calls.count() === 2);
+            expect(sent_stanzas[1].nodeTree.querySelector('displayed')).toBeDefined();
             done();
         }));
 
@@ -1363,8 +1373,12 @@ describe("Chatboxes", function () {
             const msgFactory = function () {
                 return mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
             };
+
+            const sent_stanzas = [];
+            spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
+            spyOn(chatbox, 'sendMarker').and.callThrough();
             _converse.windowState = 'hidden';
             const msg = msgFactory();
             _converse.handleMessageStanza(msg);
@@ -1372,6 +1386,8 @@ describe("Chatboxes", function () {
             expect(chatbox.get('num_unread')).toBe(1);
             const msgid = chatbox.messages.last().get('id');
             expect(chatbox.get('first_unread_id')).toBe(msgid);
+            await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1);
+            expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
             done();
         }));
 
@@ -1383,8 +1399,11 @@ describe("Chatboxes", function () {
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
+            const sent_stanzas = [];
+            spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
+            spyOn(chatbox, 'sendMarker').and.callThrough();
             chatbox.save('scrolled', true);
             _converse.windowState = 'hidden';
             const msg = msgFactory();
@@ -1393,6 +1412,8 @@ describe("Chatboxes", function () {
             expect(chatbox.get('num_unread')).toBe(1);
             const msgid = chatbox.messages.last().get('id');
             expect(chatbox.get('first_unread_id')).toBe(msgid);
+            await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1);
+            expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
             done();
         }));
 
@@ -1404,8 +1425,11 @@ describe("Chatboxes", function () {
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
+            const sent_stanzas = [];
+            spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
+            spyOn(chatbox, 'sendMarker').and.callThrough();
             _converse.windowState = 'hidden';
             const msg = msgFactory();
             _converse.handleMessageStanza(msg);
@@ -1413,8 +1437,12 @@ describe("Chatboxes", function () {
             expect(chatbox.get('num_unread')).toBe(1);
             const msgid = chatbox.messages.last().get('id');
             expect(chatbox.get('first_unread_id')).toBe(msgid);
+            await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1);
+            expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
             _converse.saveWindowState(null, 'focus');
             expect(chatbox.get('num_unread')).toBe(0);
+            await u.waitUntil(() => chatbox.sendMarker.calls.count() === 2);
+            expect(sent_stanzas[1].nodeTree.querySelector('displayed')).toBeDefined();
             done();
         }));
 
@@ -1426,8 +1454,11 @@ describe("Chatboxes", function () {
             await mock.waitForRoster(_converse, 'current', 1);
             const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
+            const sent_stanzas = [];
+            spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
             await mock.openChatBoxFor(_converse, sender_jid);
             const chatbox = _converse.chatboxes.get(sender_jid);
+            spyOn(chatbox, 'sendMarker').and.callThrough();
             chatbox.save('scrolled', true);
             _converse.windowState = 'hidden';
             const msg = msgFactory();
@@ -1436,9 +1467,13 @@ describe("Chatboxes", function () {
             expect(chatbox.get('num_unread')).toBe(1);
             const msgid = chatbox.messages.last().get('id');
             expect(chatbox.get('first_unread_id')).toBe(msgid);
+            await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1);
+            expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
             _converse.saveWindowState(null, 'focus');
             expect(chatbox.get('num_unread')).toBe(1);
             expect(chatbox.get('first_unread_id')).toBe(msgid);
+            await u.waitUntil(() => chatbox.sendMarker.calls.count() === 1);
+            expect(sent_stanzas[0].nodeTree.querySelector('received')).toBeDefined();
             done();
         }));
     });

+ 2 - 2
spec/messages.js

@@ -2010,7 +2010,7 @@ describe("A XEP-0333 Chat Marker", function () {
         spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
         spyOn(view.model, 'sendMarker').and.callThrough();
         _converse.connection._dataRecv(mock.createRequest(stanza));
-        await u.waitUntil(() => view.model.sendMarker.calls.count() === 1);
+        await u.waitUntil(() => view.model.sendMarker.calls.count() === 2);
         expect(Strophe.serialize(sent_stanzas[0])).toBe(
             `<message from="romeo@montague.lit/orchard" `+
                     `id="${sent_stanzas[0].nodeTree.getAttribute('id')}" `+
@@ -2044,7 +2044,7 @@ describe("A XEP-0333 Chat Marker", function () {
             .map(s => _.isElement(s) ? s : s.nodeTree)
             .filter(e => e.nodeName === 'message');
 
-        expect(sent_messages.length).toBe(1);
+        expect(sent_messages.length).toBe(2);
         expect(Strophe.serialize(sent_messages[0])).toBe(
             `<message id="${sent_messages[0].getAttribute('id')}" to="${contact_jid}" type="chat" xmlns="jabber:client">`+
                 `<active xmlns="http://jabber.org/protocol/chatstates"/>`+

+ 1 - 0
spec/mock.js

@@ -441,6 +441,7 @@ window.addEventListener('converse-loaded', () => {
                     id: (new Date()).getTime()
                 })
                 .c('body').t(message).up()
+                .c('markable', {'xmlns': Strophe.NS.MARKERS}).up()
                 .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
     }
 

+ 27 - 13
src/headless/converse-chat.js

@@ -831,12 +831,19 @@ converse.plugins.add('converse-chat', {
                 return _converse.connection.send(msg);
             },
 
-            sendMarker(to_jid, id, type) {
+            sendMarkerForMessage (msg) {
+                if (msg?.get('is_markable')) {
+                    const from_jid = Strophe.getBareJidFromJid(msg.get('from'));
+                    this.sendMarker(from_jid, msg.get('msgid'), 'displayed', msg.get('type'));
+                }
+            },
+
+            sendMarker (to_jid, id, type, msg_type) {
                 const stanza = $msg({
                     'from': _converse.connection.jid,
                     'id': u.getUniqueId(),
                     'to': to_jid,
-                    'type': 'chat',
+                    'type': msg_type ? msg_type : 'chat'
                 }).c(type, {'xmlns': Strophe.NS.MARKERS, 'id': id});
                 api.send(stanza);
             },
@@ -1141,22 +1148,29 @@ converse.plugins.add('converse-chat', {
              * @param {_converse.Message} message
              */
             incrementUnreadMsgCounter (message) {
-                if (!message || !message.get('message')) {
-                    return;
+                if (!message?.get('body')) {
+                    return
                 }
-                if (utils.isNewMessage(message) && this.isHidden()) {
-                    const settings = {
-                        'num_unread': this.get('num_unread') + 1
-                    };
-                    if (this.get('num_unread') === 0) {
-                        settings['first_unread_id'] = message.get('id');
+                if (utils.isNewMessage(message)) {
+                    if (this.isHidden()) {
+                        const settings = {
+                            'num_unread': this.get('num_unread') + 1
+                        };
+                        if (this.get('num_unread') === 0) {
+                            settings['first_unread_id'] = message.get('id');
+                        }
+                        this.save(settings);
+                        _converse.incrementMsgCounter();
+                    } else {
+                        this.sendMarkerForMessage(message);
                     }
-                    this.save(settings);
-                    _converse.incrementMsgCounter();
                 }
             },
 
-            clearUnreadMsgCounter () {
+            clearUnreadMsgCounter() {
+                if (this.get('num_unread') > 0) {
+                    this.sendMarkerForMessage(this.messages.last());
+                }
                 u.safeSave(this, {'num_unread': 0});
             },
 

+ 21 - 14
src/headless/converse-muc.js

@@ -2404,25 +2404,32 @@ converse.plugins.add('converse-muc', {
              * @param { XMLElement } - The <messsage> stanza
              */
             incrementUnreadMsgCounter (message) {
-                if (!message) { return; }
-                const body = message.get('message');
-                if (!body) { return; }
-                if (u.isNewMessage(message) && this.isHidden()) {
-                    const settings = {
-                        'num_unread_general': this.get('num_unread_general') + 1
-                    };
-                    if (this.get('num_unread') === 0) {
-                        settings['first_unread_id'] = message.get('id');
-                    }
-                    if (this.isUserMentioned(message)) {
-                        settings.num_unread = this.get('num_unread') + 1;
-                        _converse.incrementMsgCounter();
+                if (!message?.get('body')) {
+                    return
+                }
+                if (u.isNewMessage(message)) {
+                    if (this.isHidden()) {
+                        const settings = {
+                            'num_unread_general': this.get('num_unread_general') + 1
+                        };
+                        if (this.get('num_unread_general') === 0) {
+                            settings['first_unread_id'] = message.get('id');
+                        }
+                        if (this.isUserMentioned(message)) {
+                            settings.num_unread = this.get('num_unread') + 1;
+                            _converse.incrementMsgCounter();
+                        }
+                        this.save(settings);
+                    } else {
+                        this.sendMarkerForMessage(message);
                     }
-                    this.save(settings);
                 }
             },
 
             clearUnreadMsgCounter() {
+                if (this.get('num_unread_general') > 0 || this.get('num_unread') > 0) {
+                    this.sendMarkerForMessage(this.messages.last());
+                }
                 u.safeSave(this, {
                     'num_unread': 0,
                     'num_unread_general': 0