implement XEP-0184: Message Delivery Receipts

This commit is contained in:
Christoph Scholz 2018-11-03 14:37:57 +01:00 committed by JC Brand
parent e3a5bf7e23
commit da5ca0b585
13 changed files with 226 additions and 13 deletions

View File

@ -5,6 +5,7 @@
- Error `FATAL: TypeError: Cannot read property 'extend' of undefined` when using `embedded` view mode. - Error `FATAL: TypeError: Cannot read property 'extend' of undefined` when using `embedded` view mode.
- Default paths in converse-notifications.js are now relative - Default paths in converse-notifications.js are now relative
- Add a button to regenerate OMEMO keys - Add a button to regenerate OMEMO keys
- #141 XEP-0184: Message Delivery Receipts
- #1188 Feature request: drag and drop file to HTTP Upload - #1188 Feature request: drag and drop file to HTTP Upload
- #1268 Switch from SASS variables to CSS custom properties - #1268 Switch from SASS variables to CSS custom properties
- #1278 Replace the default avatar with a SVG version - #1278 Replace the default avatar with a SVG version

View File

@ -9303,6 +9303,7 @@ readers do not read off random characters that represent icons */
--text-color: #666; --text-color: #666;
--text-color-lighten-15-percent: #8c8c8c; --text-color-lighten-15-percent: #8c8c8c;
--message-text-color: #555; --message-text-color: #555;
--message-receipt-color: #3AA569;
--save-button-color: #3AA569; --save-button-color: #3AA569;
--chat-textarea-color: #666; --chat-textarea-color: #666;
--chat-textarea-height: 60px; --chat-textarea-height: 60px;
@ -11796,6 +11797,8 @@ body.reset {
display: none; } display: none; }
#conversejs .message.chat-msg.chat-msg--followup .chat-msg__content { #conversejs .message.chat-msg.chat-msg--followup .chat-msg__content {
margin-left: 2.75rem; } margin-left: 2.75rem; }
#conversejs .message.chat-msg .chat-msg__receipt {
color: var(--message-receipt-color); }
#conversejs .chatroom-body .message.onload { #conversejs .chatroom-body .message.onload {
animation: colorchange-chatmessage-muc 1s; animation: colorchange-chatmessage-muc 1s;

75
dist/converse.js vendored
View File

@ -61682,7 +61682,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
return this.renderFileUploadProgresBar(); return this.renderFileUploadProgresBar();
} }
if (_.filter(['correcting', 'message', 'type', 'upload'], prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop)).length) { if (_.filter(['correcting', 'message', 'type', 'upload', 'received'], prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop)).length) {
await this.render(); await this.render();
} }
@ -65704,7 +65704,9 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
'to': this.get('jid'), 'to': this.get('jid'),
'type': this.get('message_type'), 'type': this.get('message_type'),
'id': message.get('msgid') 'id': message.get('msgid')
}).c('body').t(body).up() // An encrypted header is added to the message for }).c('body').t(body).up().c('request', {
'xmlns': Strophe.NS.RECEIPTS
}).up() // An encrypted header is added to the message for
// each device that is supposed to receive it. // each device that is supposed to receive it.
// These headers simply contain the key that the // These headers simply contain the key that the
// payload message is encrypted with, // payload message is encrypted with,
@ -70630,6 +70632,7 @@ const _converse$env = _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].env
_ = _converse$env._; _ = _converse$env._;
const u = _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].env.utils; const u = _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].env.utils;
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0'); Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
Strophe.addNamespace('RECEIPTS', 'urn:xmpp:receipts');
Strophe.addNamespace('REFERENCE', 'urn:xmpp:reference:0'); Strophe.addNamespace('REFERENCE', 'urn:xmpp:reference:0');
_converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-chatboxes', { _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-chatboxes', {
dependencies: ["converse-roster", "converse-vcard"], dependencies: ["converse-roster", "converse-vcard"],
@ -70940,6 +70943,31 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
return false; return false;
}, },
handleReceipt(stanza) {
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();
if (receipt) {
const msgid = receipt && receipt.getAttribute('id'),
message = msgid && this.messages.findWhere({
msgid
});
if (message && !message.get('received')) {
message.save({
'received': moment().format()
});
}
return true;
}
}
return false;
},
createMessageStanza(message) { createMessageStanza(message) {
/* Given a _converse.Message Backbone.Model, return the XML /* Given a _converse.Message Backbone.Model, return the XML
* stanza that represents it. * stanza that represents it.
@ -70954,6 +70982,8 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
'id': message.get('edited') && _converse.connection.getUniqueId() || message.get('msgid') 'id': message.get('edited') && _converse.connection.getUniqueId() || message.get('msgid')
}).c('body').t(message.get('message')).up().c(_converse.ACTIVE, { }).c('body').t(message.get('message')).up().c(_converse.ACTIVE, {
'xmlns': Strophe.NS.CHATSTATES 'xmlns': Strophe.NS.CHATSTATES
}).up().c('request', {
'xmlns': Strophe.NS.RECEIPTS
}).up(); }).up();
if (message.get('is_spoiler')) { if (message.get('is_spoiler')) {
@ -71344,6 +71374,19 @@ _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
}).c('received', {
'xmlns': Strophe.NS.RECEIPTS,
'id': id
}).up();
_converse.api.send(receipt_stanza);
},
onMessage(stanza) { onMessage(stanza) {
/* Handler method for all incoming single-user chat "message" /* Handler method for all incoming single-user chat "message"
* stanzas. * stanzas.
@ -71387,6 +71430,12 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
to_jid = stanza.getAttribute('to'); to_jid = stanza.getAttribute('to');
} }
const requests_receipt = !_.isUndefined(sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop());
if (requests_receipt) {
this.sendReceiptStanza(from_jid, stanza.getAttribute('id'));
}
const from_bare_jid = Strophe.getBareJidFromJid(from_jid), const from_bare_jid = Strophe.getBareJidFromJid(from_jid),
from_resource = Strophe.getResourceFromJid(from_jid), from_resource = Strophe.getResourceFromJid(from_jid),
is_me = from_bare_jid === _converse.bare_jid; is_me = from_bare_jid === _converse.bare_jid;
@ -71410,7 +71459,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`).length > 0; const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`).length > 0;
const chatbox = this.getChatBox(contact_jid, attrs, has_body); const chatbox = this.getChatBox(contact_jid, attrs, has_body);
if (chatbox && !chatbox.handleMessageCorrection(stanza)) { if (chatbox && !chatbox.handleMessageCorrection(stanza) && !chatbox.handleReceipt(stanza)) {
const msgid = stanza.getAttribute('id'), const msgid = stanza.getAttribute('id'),
message = msgid && chatbox.messages.findWhere({ message = msgid && chatbox.messages.findWhere({
msgid msgid
@ -76065,15 +76114,23 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
return data; return data;
}, },
isDuplicate(message, original_stanza) { isDuplicate(message) {
const msgid = message.getAttribute('id'), const msgid = message.getAttribute('id'),
jid = message.getAttribute('from'); jid = message.getAttribute('from');
if (msgid) { if (msgid) {
return this.messages.where({ const msg = this.messages.findWhere({
'msgid': msgid, 'msgid': msgid,
'from': jid 'from': jid
}).length; });
if (msg && msg.get('sender') === 'me' && !msg.get('received')) {
msg.save({
'received': moment().format()
});
}
return msg;
} }
return false; return false;
@ -76106,7 +76163,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
stanza = forwarded.querySelector('message'); stanza = forwarded.querySelector('message');
} }
if (this.isDuplicate(stanza, original_stanza)) { if (this.isDuplicate(stanza)) {
return; return;
} }
@ -102447,6 +102504,10 @@ __p += '\n </span>\n ';
if (!o.is_me_message) { ; if (!o.is_me_message) { ;
__p += '<div class="chat-msg__body">'; __p += '<div class="chat-msg__body">';
} ; } ;
__p += '\n ';
if (o.received) { ;
__p += ' <span class="fa fa-check chat-msg__receipt">&nbsp;</span> ';
} ;
__p += '\n '; __p += '\n ';
if (o.edited) { ; if (o.edited) { ;
__p += ' <i title="' + __p += ' <i title="' +

View File

@ -256,6 +256,10 @@
margin-left: 2.75rem; margin-left: 2.75rem;
} }
} }
.chat-msg__receipt {
color: var(--message-receipt-color);
}
} }
} }

View File

@ -28,6 +28,7 @@ $font-path: "webfonts/icomoon/fonts/" !default;
--text-color: #666; --text-color: #666;
--text-color-lighten-15-percent: #8c8c8c; // lighten(#666, 15%) --text-color-lighten-15-percent: #8c8c8c; // lighten(#666, 15%)
--message-text-color: #555; --message-text-color: #555;
--message-receipt-color: #3AA569; // $green
--save-button-color: #3AA569; // $green --save-button-color: #3AA569; // $green
--chat-textarea-color: #666; --chat-textarea-color: #666;

View File

@ -357,6 +357,7 @@
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>${message}</body>`+ `<body>${message}</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+ `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<x xmlns="jabber:x:oob">`+ `<x xmlns="jabber:x:oob">`+
`<url>${message}</url>`+ `<url>${message}</url>`+
`</x>`+ `</x>`+
@ -459,6 +460,7 @@
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>${message}</body>`+ `<body>${message}</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+ `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<x xmlns="jabber:x:oob">`+ `<x xmlns="jabber:x:oob">`+
`<url>${message}</url>`+ `<url>${message}</url>`+
`</x>`+ `</x>`+

View File

@ -77,6 +77,7 @@
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>But soft, what light through yonder window breaks?</body>`+ `<body>But soft, what light through yonder window breaks?</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+ `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+ `<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+
`</message>`); `</message>`);
expect(view.model.messages.models.length).toBe(1); expect(view.model.messages.models.length).toBe(1);
@ -181,6 +182,7 @@
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>But soft, what light through yonder window breaks?</body>`+ `<body>But soft, what light through yonder window breaks?</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+ `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+ `<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+
`</message>`); `</message>`);
expect(view.model.messages.models.length).toBe(1); expect(view.model.messages.models.length).toBe(1);
@ -1200,6 +1202,64 @@
done(); done();
})); }));
it("received may emit a message delivery receipt",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
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();
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
sent_stanzas.push(stanza);
});
const msg = $msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
'id': msg_id,
}).c('body').t('Message!').up()
.c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
_converse.chatboxes.onMessage(msg);
const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, sent_stanzas[0].tree()).pop();
expect(receipt.outerHTML).toBe(`<received xmlns="${Strophe.NS.RECEIPTS}" id="${msg_id}"/>`);
done();
}));
it("delivery can be acknowledged by a receipt",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
test_utils.createContacts(_converse, 'current', 1);
_converse.emit('rosterContactsFetched');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
await test_utils.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
const textarea = view.el.querySelector('textarea.chat-textarea');
textarea.value = 'But soft, what light through yonder airlock breaks?';
view.keyPressed({
target: textarea,
preventDefault: _.noop,
keyCode: 13 // Enter
});
await test_utils.waitUntil(() => _converse.api.chats.get().length);
const chatbox = _converse.chatboxes.get(contact_jid);
expect(chatbox).toBeDefined();
await new Promise((resolve, reject) => view.once('messageInserted', resolve));
const msg_obj = chatbox.messages.models[0];
const msg_id = msg_obj.get('msgid');
const msg = $msg({
'from': contact_jid,
'to': _converse.connection.jid,
'id': u.getUniqueId(),
}).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
_converse.chatboxes.onMessage(msg);
await new Promise((resolve, reject) => view.model.messages.once('rendered', resolve));
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(1);
done();
}));
describe("when received from someone else", function () { describe("when received from someone else", function () {
@ -2010,6 +2070,7 @@
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>But soft, what light through yonder window breaks?</body>`+ `<body>But soft, what light through yonder window breaks?</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+ `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+ `<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+
`</message>`); `</message>`);
@ -2056,6 +2117,38 @@
done(); done();
})); }));
it("delivery can be acknowledged by a receipt",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
async function (done, _converse) {
test_utils.createContacts(_converse, 'current');
await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy');
const view = _converse.chatboxviews.get('lounge@localhost');
const textarea = view.el.querySelector('textarea.chat-textarea');
textarea.value = 'But soft, what light through yonder airlock breaks?';
view.keyPressed({
target: textarea,
preventDefault: _.noop,
keyCode: 13 // Enter
});
await new Promise((resolve, reject) => view.once('messageInserted', resolve));
const msg_obj = view.model.messages.at(0);
const msg_id = msg_obj.get('msgid');
const from = msg_obj.get('from');
const body = msg_obj.get('message');
const msg = $msg({
'from': from,
'id': msg_id,
'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));
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(1);
done();
}));
describe("when received", function () { describe("when received", function () {
it("highlights all users mentioned via XEP-0372 references", it("highlights all users mentioned via XEP-0372 references",
@ -2201,6 +2294,7 @@
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>hello z3r0 gibson mr.robot, how are you?</body>`+ `<body>hello z3r0 gibson mr.robot, how are you?</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+ `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<reference begin="18" end="26" type="mention" uri="xmpp:mr.robot@localhost" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="18" end="26" type="mention" uri="xmpp:mr.robot@localhost" xmlns="urn:xmpp:reference:0"/>`+
`<reference begin="11" end="17" type="mention" uri="xmpp:gibson@localhost" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="11" end="17" type="mention" uri="xmpp:gibson@localhost" xmlns="urn:xmpp:reference:0"/>`+
`<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@localhost" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@localhost" xmlns="urn:xmpp:reference:0"/>`+
@ -2226,6 +2320,7 @@
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>hello z3r0 gibson sw0rdf1sh, how are you?</body>`+ `<body>hello z3r0 gibson sw0rdf1sh, how are you?</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+ `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<reference begin="18" end="27" type="mention" uri="xmpp:sw0rdf1sh@localhost" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="18" end="27" type="mention" uri="xmpp:sw0rdf1sh@localhost" xmlns="urn:xmpp:reference:0"/>`+
`<reference begin="11" end="17" type="mention" uri="xmpp:gibson@localhost" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="11" end="17" type="mention" uri="xmpp:gibson@localhost" xmlns="urn:xmpp:reference:0"/>`+
`<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@localhost" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@localhost" xmlns="urn:xmpp:reference:0"/>`+
@ -2274,6 +2369,7 @@
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>hello z3r0 gibson mr.robot, how are you?</body>`+ `<body>hello z3r0 gibson mr.robot, how are you?</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+ `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<reference begin="18" end="26" type="mention" uri="xmpp:mr.robot@localhost" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="18" end="26" type="mention" uri="xmpp:mr.robot@localhost" xmlns="urn:xmpp:reference:0"/>`+
`<reference begin="11" end="17" type="mention" uri="xmpp:gibson@localhost" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="11" end="17" type="mention" uri="xmpp:gibson@localhost" xmlns="urn:xmpp:reference:0"/>`+
`<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@localhost" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@localhost" xmlns="urn:xmpp:reference:0"/>`+

View File

@ -172,6 +172,7 @@
`to="max.frankfurter@localhost" `+ `to="max.frankfurter@localhost" `+
`type="chat" xmlns="jabber:client">`+ `type="chat" xmlns="jabber:client">`+
`<body>This is an OMEMO encrypted message which your client doesnt seem to support. Find more information on https://conversations.im/omemo</body>`+ `<body>This is an OMEMO encrypted message which your client doesnt seem to support. Find more information on https://conversations.im/omemo</body>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<encrypted xmlns="eu.siacs.conversations.axolotl">`+ `<encrypted xmlns="eu.siacs.conversations.axolotl">`+
`<header sid="123456789">`+ `<header sid="123456789">`+
`<key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key>`+ `<key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key>`+

View File

@ -86,7 +86,7 @@ converse.plugins.add('converse-message-view', {
if (this.model.changed.progress) { if (this.model.changed.progress) {
return this.renderFileUploadProgresBar(); return this.renderFileUploadProgresBar();
} }
if (_.filter(['correcting', 'message', 'type', 'upload'], if (_.filter(['correcting', 'message', 'type', 'upload', 'received'],
prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop)).length) { prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop)).length) {
await this.render(); await this.render();
} }

View File

@ -394,6 +394,7 @@ converse.plugins.add('converse-omemo', {
'type': this.get('message_type'), 'type': this.get('message_type'),
'id': message.get('msgid') 'id': message.get('msgid')
}).c('body').t(body).up() }).c('body').t(body).up()
.c('request', {'xmlns': Strophe.NS.RECEIPTS}).up()
// An encrypted header is added to the message for // An encrypted header is added to the message for
// each device that is supposed to receive it. // each device that is supposed to receive it.
// These headers simply contain the key that the // These headers simply contain the key that the

View File

@ -13,6 +13,7 @@ const { $msg, Backbone, Promise, Strophe, b64_sha1, moment, sizzle, utils, _ } =
const u = converse.env.utils; const u = converse.env.utils;
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0'); Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
Strophe.addNamespace('RECEIPTS', 'urn:xmpp:receipts');
Strophe.addNamespace('REFERENCE', 'urn:xmpp:reference:0'); Strophe.addNamespace('REFERENCE', 'urn:xmpp:reference:0');
@ -297,6 +298,24 @@ converse.plugins.add('converse-chatboxes', {
return false; return false;
}, },
handleReceipt (stanza) {
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();
if (receipt) {
const msgid = receipt && receipt.getAttribute('id'),
message = msgid && this.messages.findWhere({msgid});
if (message && !message.get('received')) {
message.save({
'received': moment().format()
});
}
return true;
}
}
return false;
},
createMessageStanza (message) { createMessageStanza (message) {
/* Given a _converse.Message Backbone.Model, return the XML /* Given a _converse.Message Backbone.Model, return the XML
* stanza that represents it. * stanza that represents it.
@ -310,7 +329,8 @@ converse.plugins.add('converse-chatboxes', {
'type': this.get('message_type'), 'type': this.get('message_type'),
'id': message.get('edited') && _converse.connection.getUniqueId() || message.get('msgid'), 'id': message.get('edited') && _converse.connection.getUniqueId() || message.get('msgid'),
}).c('body').t(message.get('message')).up() }).c('body').t(message.get('message')).up()
.c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up(); .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up()
.c('request', {'xmlns': Strophe.NS.RECEIPTS}).up();
if (message.get('is_spoiler')) { if (message.get('is_spoiler')) {
if (message.get('spoiler_hint')) { if (message.get('spoiler_hint')) {
@ -663,6 +683,15 @@ converse.plugins.add('converse-chatboxes', {
} }
}, },
sendReceiptStanza (to_jid, id) {
const receipt_stanza = $msg({
'from': _converse.connection.jid,
'id': _converse.connection.getUniqueId(),
'to': to_jid,
}).c('received', {'xmlns': Strophe.NS.RECEIPTS, 'id': id}).up();
_converse.api.send(receipt_stanza);
},
onMessage (stanza) { onMessage (stanza) {
/* Handler method for all incoming single-user chat "message" /* Handler method for all incoming single-user chat "message"
* stanzas. * stanzas.
@ -709,6 +738,11 @@ converse.plugins.add('converse-chatboxes', {
to_jid = stanza.getAttribute('to'); to_jid = stanza.getAttribute('to');
} }
const requests_receipt = !_.isUndefined(sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop());
if (requests_receipt) {
this.sendReceiptStanza(from_jid, stanza.getAttribute('id'));
}
const from_bare_jid = Strophe.getBareJidFromJid(from_jid), const from_bare_jid = Strophe.getBareJidFromJid(from_jid),
from_resource = Strophe.getResourceFromJid(from_jid), from_resource = Strophe.getResourceFromJid(from_jid),
is_me = from_bare_jid === _converse.bare_jid; is_me = from_bare_jid === _converse.bare_jid;
@ -732,7 +766,7 @@ converse.plugins.add('converse-chatboxes', {
// Get chat box, but only create a new one when the message has a body. // 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}"]`).length > 0; const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`).length > 0;
const chatbox = this.getChatBox(contact_jid, attrs, has_body); const chatbox = this.getChatBox(contact_jid, attrs, has_body);
if (chatbox && !chatbox.handleMessageCorrection(stanza)) { if (chatbox && !chatbox.handleMessageCorrection(stanza) && !chatbox.handleReceipt(stanza)) {
const msgid = stanza.getAttribute('id'), const msgid = stanza.getAttribute('id'),
message = msgid && chatbox.messages.findWhere({msgid}); message = msgid && chatbox.messages.findWhere({msgid});
if (!message) { if (!message) {

View File

@ -913,13 +913,21 @@ converse.plugins.add('converse-muc', {
return data; return data;
}, },
isDuplicate (message, original_stanza) { isDuplicate (message) {
const msgid = message.getAttribute('id'), const msgid = message.getAttribute('id'),
jid = message.getAttribute('from'); jid = message.getAttribute('from');
if (msgid) { if (msgid) {
return this.messages.where({'msgid': msgid, 'from': jid}).length; const msg = this.messages.findWhere({'msgid': msgid, 'from': jid});
if (msg && msg.get('sender') === 'me' && !msg.get('received')) {
msg.save({
'received': moment().format()
});
}
return msg;
} }
return false; return false;
}, },
fetchFeaturesIfConfigurationChanged (stanza) { fetchFeaturesIfConfigurationChanged (stanza) {
@ -949,7 +957,7 @@ converse.plugins.add('converse-muc', {
if (!_.isNull(forwarded)) { if (!_.isNull(forwarded)) {
stanza = forwarded.querySelector('message'); stanza = forwarded.querySelector('message');
} }
if (this.isDuplicate(stanza, original_stanza)) { if (this.isDuplicate(stanza)) {
return; return;
} }
const jid = stanza.getAttribute('from'), const jid = stanza.getAttribute('from'),

View File

@ -12,6 +12,7 @@
{[ if (o.is_encrypted) { ]}<span class="fa fa-lock"></span>{[ } ]} {[ if (o.is_encrypted) { ]}<span class="fa fa-lock"></span>{[ } ]}
</span> </span>
{[ if (!o.is_me_message) { ]}<div class="chat-msg__body">{[ } ]} {[ if (!o.is_me_message) { ]}<div class="chat-msg__body">{[ } ]}
{[ if (o.received) { ]} <span class="fa fa-check chat-msg__receipt">&nbsp;</span> {[ } ]}
{[ if (o.edited) { ]} <i title="{{{o.__('This message has been edited')}}}" class="fa fa-edit chat-msg__edit-modal"></i> {[ } ]} {[ if (o.edited) { ]} <i title="{{{o.__('This message has been edited')}}}" class="fa fa-edit chat-msg__edit-modal"></i> {[ } ]}
{[ if (!o.is_me_message) { ]}<div class="chat-msg__message">{[ } ]} {[ if (!o.is_me_message) { ]}<div class="chat-msg__message">{[ } ]}
{[ if (o.is_spoiler) { ]} {[ if (o.is_spoiler) { ]}