Commit 5a57ded2 authored by JC Brand's avatar JC Brand

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

Credit for this work goes to @deleolajide
parent ac36addd
...@@ -24,6 +24,7 @@ Soon we'll deprecate the latter, so prepare now. ...@@ -24,6 +24,7 @@ Soon we'll deprecate the latter, so prepare now.
- #1999: Demarcate first unread message - #1999: Demarcate first unread message
- #2002: fix rendering of `muc_roomid_policy_hint` - #2002: fix rendering of `muc_roomid_policy_hint`
- #2006: fix rendering of emojis in case `use_system_emojis == false` - #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). - 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` - 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 - Bugfix. Handle stanza that clears the MUC subject
......
...@@ -2244,7 +2244,8 @@ ...@@ -2244,7 +2244,8 @@
"dependencies": { "dependencies": {
"filesize": { "filesize": {
"version": "6.1.0", "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": { "fs-extra": {
"version": "8.1.0", "version": "8.1.0",
...@@ -2278,7 +2279,8 @@ ...@@ -2278,7 +2279,8 @@
}, },
"jed": { "jed": {
"version": "1.1.1", "version": "1.1.1",
"resolved": false "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz",
"integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ="
}, },
"jsonfile": { "jsonfile": {
"version": "5.0.0", "version": "5.0.0",
...@@ -2299,18 +2301,21 @@ ...@@ -2299,18 +2301,21 @@
}, },
"localforage": { "localforage": {
"version": "1.7.3", "version": "1.7.3",
"resolved": false, "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
"integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
"requires": { "requires": {
"lie": "3.1.1" "lie": "3.1.1"
} }
}, },
"lodash": { "lodash": {
"version": "4.17.15", "version": "4.17.15",
"resolved": false "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
}, },
"pluggable.js": { "pluggable.js": {
"version": "2.0.1", "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": { "requires": {
"lodash": "^4.17.11" "lodash": "^4.17.11"
} }
...@@ -2324,11 +2329,13 @@ ...@@ -2324,11 +2329,13 @@
}, },
"strophe.js": { "strophe.js": {
"version": "1.3.4", "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": { "twemoji": {
"version": "12.1.5", "version": "12.1.5",
"resolved": false, "resolved": "https://registry.npmjs.org/twemoji/-/twemoji-12.1.5.tgz",
"integrity": "sha512-B0PBVy5xomwb1M/WZxf/IqPZfnoIYy1skXnlHjMwLwTNfZ9ljh8VgWQktAPcJXu8080WoEh6YwQGPVhDVqvrVQ==",
"requires": { "requires": {
"fs-extra": "^8.0.1", "fs-extra": "^8.0.1",
"jsonfile": "^5.0.0", "jsonfile": "^5.0.0",
......
...@@ -1326,13 +1326,19 @@ describe("Chatboxes", function () { ...@@ -1326,13 +1326,19 @@ describe("Chatboxes", function () {
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); 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) const view = await mock.openChatBoxFor(_converse, sender_jid)
spyOn(view.model, 'sendMarker').and.callThrough();
view.model.save('scrolled', true); view.model.save('scrolled', true);
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length); await u.waitUntil(() => view.model.messages.length);
expect(view.model.get('num_unread')).toBe(1); expect(view.model.get('num_unread')).toBe(1);
const msgid = view.model.messages.last().get('id'); const msgid = view.model.messages.last().get('id');
expect(view.model.get('first_unread_id')).toBe(msgid); 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(); done();
})); }));
...@@ -1345,11 +1351,15 @@ describe("Chatboxes", function () { ...@@ -1345,11 +1351,15 @@ describe("Chatboxes", function () {
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
msg = mock.createChatMessage(_converse, sender_jid, 'This message will be read'); 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); await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid); const chatbox = _converse.chatboxes.get(sender_jid);
spyOn(chatbox, 'sendMarker').and.callThrough();
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
expect(chatbox.get('num_unread')).toBe(0); expect(chatbox.get('num_unread')).toBe(0);
await u.waitUntil(() => chatbox.sendMarker.calls.count() === 2);
expect(sent_stanzas[1].nodeTree.querySelector('displayed')).toBeDefined();
done(); done();
})); }));
...@@ -1363,8 +1373,12 @@ describe("Chatboxes", function () { ...@@ -1363,8 +1373,12 @@ describe("Chatboxes", function () {
const msgFactory = function () { const msgFactory = function () {
return mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); 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); await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid); const chatbox = _converse.chatboxes.get(sender_jid);
spyOn(chatbox, 'sendMarker').and.callThrough();
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
const msg = msgFactory(); const msg = msgFactory();
_converse.handleMessageStanza(msg); _converse.handleMessageStanza(msg);
...@@ -1372,6 +1386,8 @@ describe("Chatboxes", function () { ...@@ -1372,6 +1386,8 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1); expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id'); const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid); 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(); done();
})); }));
...@@ -1383,8 +1399,11 @@ describe("Chatboxes", function () { ...@@ -1383,8 +1399,11 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; 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 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); await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid); const chatbox = _converse.chatboxes.get(sender_jid);
spyOn(chatbox, 'sendMarker').and.callThrough();
chatbox.save('scrolled', true); chatbox.save('scrolled', true);
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
const msg = msgFactory(); const msg = msgFactory();
...@@ -1393,6 +1412,8 @@ describe("Chatboxes", function () { ...@@ -1393,6 +1412,8 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1); expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id'); const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid); 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(); done();
})); }));
...@@ -1404,8 +1425,11 @@ describe("Chatboxes", function () { ...@@ -1404,8 +1425,11 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; 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 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); await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid); const chatbox = _converse.chatboxes.get(sender_jid);
spyOn(chatbox, 'sendMarker').and.callThrough();
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
const msg = msgFactory(); const msg = msgFactory();
_converse.handleMessageStanza(msg); _converse.handleMessageStanza(msg);
...@@ -1413,8 +1437,12 @@ describe("Chatboxes", function () { ...@@ -1413,8 +1437,12 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1); expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id'); const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid); 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'); _converse.saveWindowState(null, 'focus');
expect(chatbox.get('num_unread')).toBe(0); expect(chatbox.get('num_unread')).toBe(0);
await u.waitUntil(() => chatbox.sendMarker.calls.count() === 2);
expect(sent_stanzas[1].nodeTree.querySelector('displayed')).toBeDefined();
done(); done();
})); }));
...@@ -1426,8 +1454,11 @@ describe("Chatboxes", function () { ...@@ -1426,8 +1454,11 @@ describe("Chatboxes", function () {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; 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 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); await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid); const chatbox = _converse.chatboxes.get(sender_jid);
spyOn(chatbox, 'sendMarker').and.callThrough();
chatbox.save('scrolled', true); chatbox.save('scrolled', true);
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
const msg = msgFactory(); const msg = msgFactory();
...@@ -1436,9 +1467,13 @@ describe("Chatboxes", function () { ...@@ -1436,9 +1467,13 @@ describe("Chatboxes", function () {
expect(chatbox.get('num_unread')).toBe(1); expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id'); const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid); 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'); _converse.saveWindowState(null, 'focus');
expect(chatbox.get('num_unread')).toBe(1); expect(chatbox.get('num_unread')).toBe(1);
expect(chatbox.get('first_unread_id')).toBe(msgid); 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(); done();
})); }));
}); });
......
...@@ -2010,7 +2010,7 @@ describe("A XEP-0333 Chat Marker", function () { ...@@ -2010,7 +2010,7 @@ describe("A XEP-0333 Chat Marker", function () {
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s)); spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
spyOn(view.model, 'sendMarker').and.callThrough(); spyOn(view.model, 'sendMarker').and.callThrough();
_converse.connection._dataRecv(mock.createRequest(stanza)); _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( expect(Strophe.serialize(sent_stanzas[0])).toBe(
`<message from="romeo@montague.lit/orchard" `+ `<message from="romeo@montague.lit/orchard" `+
`id="${sent_stanzas[0].nodeTree.getAttribute('id')}" `+ `id="${sent_stanzas[0].nodeTree.getAttribute('id')}" `+
...@@ -2044,7 +2044,7 @@ describe("A XEP-0333 Chat Marker", function () { ...@@ -2044,7 +2044,7 @@ describe("A XEP-0333 Chat Marker", function () {
.map(s => _.isElement(s) ? s : s.nodeTree) .map(s => _.isElement(s) ? s : s.nodeTree)
.filter(e => e.nodeName === 'message'); .filter(e => e.nodeName === 'message');
expect(sent_messages.length).toBe(1); expect(sent_messages.length).toBe(2);
expect(Strophe.serialize(sent_messages[0])).toBe( expect(Strophe.serialize(sent_messages[0])).toBe(
`<message id="${sent_messages[0].getAttribute('id')}" to="${contact_jid}" type="chat" xmlns="jabber:client">`+ `<message id="${sent_messages[0].getAttribute('id')}" to="${contact_jid}" type="chat" xmlns="jabber:client">`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+ `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
......
...@@ -441,6 +441,7 @@ window.addEventListener('converse-loaded', () => { ...@@ -441,6 +441,7 @@ window.addEventListener('converse-loaded', () => {
id: (new Date()).getTime() id: (new Date()).getTime()
}) })
.c('body').t(message).up() .c('body').t(message).up()
.c('markable', {'xmlns': Strophe.NS.MARKERS}).up()
.c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree(); .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
} }
......
...@@ -831,12 +831,19 @@ converse.plugins.add('converse-chat', { ...@@ -831,12 +831,19 @@ converse.plugins.add('converse-chat', {
return _converse.connection.send(msg); 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({ const stanza = $msg({
'from': _converse.connection.jid, 'from': _converse.connection.jid,
'id': u.getUniqueId(), 'id': u.getUniqueId(),
'to': to_jid, 'to': to_jid,
'type': 'chat', 'type': msg_type ? msg_type : 'chat'
}).c(type, {'xmlns': Strophe.NS.MARKERS, 'id': id}); }).c(type, {'xmlns': Strophe.NS.MARKERS, 'id': id});
api.send(stanza); api.send(stanza);
}, },
...@@ -1141,10 +1148,11 @@ converse.plugins.add('converse-chat', { ...@@ -1141,10 +1148,11 @@ converse.plugins.add('converse-chat', {
* @param {_converse.Message} message * @param {_converse.Message} message
*/ */
incrementUnreadMsgCounter (message) { incrementUnreadMsgCounter (message) {
if (!message || !message.get('message')) { if (!message?.get('body')) {
return; return
} }
if (utils.isNewMessage(message) && this.isHidden()) { if (utils.isNewMessage(message)) {
if (this.isHidden()) {
const settings = { const settings = {
'num_unread': this.get('num_unread') + 1 'num_unread': this.get('num_unread') + 1
}; };
...@@ -1153,10 +1161,16 @@ converse.plugins.add('converse-chat', { ...@@ -1153,10 +1161,16 @@ converse.plugins.add('converse-chat', {
} }
this.save(settings); this.save(settings);
_converse.incrementMsgCounter(); _converse.incrementMsgCounter();
} else {
this.sendMarkerForMessage(message);
}
} }
}, },
clearUnreadMsgCounter () { clearUnreadMsgCounter() {
if (this.get('num_unread') > 0) {
this.sendMarkerForMessage(this.messages.last());
}
u.safeSave(this, {'num_unread': 0}); u.safeSave(this, {'num_unread': 0});
}, },
......
...@@ -2404,14 +2404,15 @@ converse.plugins.add('converse-muc', { ...@@ -2404,14 +2404,15 @@ converse.plugins.add('converse-muc', {
* @param { XMLElement } - The <messsage> stanza * @param { XMLElement } - The <messsage> stanza
*/ */
incrementUnreadMsgCounter (message) { incrementUnreadMsgCounter (message) {
if (!message) { return; } if (!message?.get('body')) {
const body = message.get('message'); return
if (!body) { return; } }
if (u.isNewMessage(message) && this.isHidden()) { if (u.isNewMessage(message)) {
if (this.isHidden()) {
const settings = { const settings = {
'num_unread_general': this.get('num_unread_general') + 1 'num_unread_general': this.get('num_unread_general') + 1
}; };
if (this.get('num_unread') === 0) { if (this.get('num_unread_general') === 0) {
settings['first_unread_id'] = message.get('id'); settings['first_unread_id'] = message.get('id');
} }
if (this.isUserMentioned(message)) { if (this.isUserMentioned(message)) {
...@@ -2419,10 +2420,16 @@ converse.plugins.add('converse-muc', { ...@@ -2419,10 +2420,16 @@ converse.plugins.add('converse-muc', {
_converse.incrementMsgCounter(); _converse.incrementMsgCounter();
} }
this.save(settings); this.save(settings);
} else {
this.sendMarkerForMessage(message);
}
} }
}, },
clearUnreadMsgCounter() { clearUnreadMsgCounter() {
if (this.get('num_unread_general') > 0 || this.get('num_unread') > 0) {
this.sendMarkerForMessage(this.messages.last());
}
u.safeSave(this, { u.safeSave(this, {
'num_unread': 0, 'num_unread': 0,
'num_unread_general': 0 'num_unread_general': 0
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment