Add support for the XEP-0333 displayed chat marker

Credit for this work goes to @deleolajide
This commit is contained in:
JC Brand 2020-06-01 17:30:20 +02:00
parent ac36adddfe
commit 5a57ded243
7 changed files with 102 additions and 37 deletions

View File

@ -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

21
package-lock.json generated
View File

@ -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",

View File

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

View File

@ -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"/>`+

View File

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

View File

@ -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});
},

View File

@ -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 (!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);
}
if (this.isUserMentioned(message)) {
settings.num_unread = this.get('num_unread') + 1;
_converse.incrementMsgCounter();
}
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