Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XEP-0184: Message Delivery Receipts #1304

Merged
merged 1 commit into from Nov 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Expand Up @@ -5,6 +5,7 @@
- Error `FATAL: TypeError: Cannot read property 'extend' of undefined` when using `embedded` view mode.
- Default paths in converse-notifications.js are now relative
- Add a button to regenerate OMEMO keys
- #141 XEP-0184: Message Delivery Receipts
- #1188 Feature request: drag and drop file to HTTP Upload
- #1268 Switch from SASS variables to CSS custom properties
- #1278 Replace the default avatar with a SVG version
Expand Down
3 changes: 3 additions & 0 deletions css/converse.css
Expand Up @@ -9303,6 +9303,7 @@ readers do not read off random characters that represent icons */
--text-color: #666;
--text-color-lighten-15-percent: #8c8c8c;
--message-text-color: #555;
--message-receipt-color: #3AA569;
--save-button-color: #3AA569;
--chat-textarea-color: #666;
--chat-textarea-height: 60px;
Expand Down Expand Up @@ -11796,6 +11797,8 @@ body.reset {
display: none; }
#conversejs .message.chat-msg.chat-msg--followup .chat-msg__content {
margin-left: 2.75rem; }
#conversejs .message.chat-msg .chat-msg__receipt {
color: var(--message-receipt-color); }

#conversejs .chatroom-body .message.onload {
animation: colorchange-chatmessage-muc 1s;
Expand Down
75 changes: 68 additions & 7 deletions dist/converse.js
Expand Up @@ -61682,7 +61682,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
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();
}

Expand Down Expand Up @@ -65704,7 +65704,9 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
'to': this.get('jid'),
'type': this.get('message_type'),
'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.
// These headers simply contain the key that the
// payload message is encrypted with,
Expand Down Expand Up @@ -70645,6 +70647,7 @@ const _converse$env = _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].env
_ = _converse$env._;
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');
_converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-chatboxes', {
dependencies: ["converse-roster", "converse-vcard"],
Expand Down Expand Up @@ -70955,6 +70958,31 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
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) {
/* Given a _converse.Message Backbone.Model, return the XML
* stanza that represents it.
Expand All @@ -70969,6 +70997,8 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
'id': message.get('edited') && _converse.connection.getUniqueId() || message.get('msgid')
}).c('body').t(message.get('message')).up().c(_converse.ACTIVE, {
'xmlns': Strophe.NS.CHATSTATES
}).up().c('request', {
'xmlns': Strophe.NS.RECEIPTS
}).up();

if (message.get('is_spoiler')) {
Expand Down Expand Up @@ -71359,6 +71389,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) {
/* Handler method for all incoming single-user chat "message"
* stanzas.
Expand Down Expand Up @@ -71402,6 +71445,12 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-cha
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),
from_resource = Strophe.getResourceFromJid(from_jid),
is_me = from_bare_jid === _converse.bare_jid;
Expand All @@ -71425,7 +71474,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 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'),
message = msgid && chatbox.messages.findWhere({
msgid
Expand Down Expand Up @@ -76079,15 +76128,23 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
return data;
},

isDuplicate(message, original_stanza) {
isDuplicate(message) {
const msgid = message.getAttribute('id'),
jid = message.getAttribute('from');

if (msgid) {
return this.messages.where({
const msg = this.messages.findWhere({
'msgid': msgid,
'from': jid
}).length;
});

if (msg && msg.get('sender') === 'me' && !msg.get('received')) {
msg.save({
'received': moment().format()
});
}

return msg;
}

return false;
Expand Down Expand Up @@ -76120,7 +76177,7 @@ _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].plugins.add('converse-muc
stanza = forwarded.querySelector('message');
}

if (this.isDuplicate(stanza, original_stanza)) {
if (this.isDuplicate(stanza)) {
return;
}

Expand Down Expand Up @@ -102461,6 +102518,10 @@ __p += '\n </span>\n ';
if (!o.is_me_message) { ;
__p += '<div class="chat-msg__body">';
} ;
__p += '\n ';
if (o.received) { ;
__p += ' <span class="fa fa-check chat-msg__receipt">&nbsp;</span> ';
} ;
__p += '\n ';
if (o.edited) { ;
__p += ' <i title="' +
Expand Down
4 changes: 4 additions & 0 deletions sass/_messages.scss
Expand Up @@ -256,6 +256,10 @@
margin-left: 2.75rem;
}
}

.chat-msg__receipt {
color: var(--message-receipt-color);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions sass/_variables.scss
Expand Up @@ -28,6 +28,7 @@ $font-path: "webfonts/icomoon/fonts/" !default;
--text-color: #666;
--text-color-lighten-15-percent: #8c8c8c; // lighten(#666, 15%)
--message-text-color: #555;
--message-receipt-color: #3AA569; // $green
--save-button-color: #3AA569; // $green

--chat-textarea-color: #666;
Expand Down
2 changes: 2 additions & 0 deletions spec/http-file-upload.js
Expand Up @@ -357,6 +357,7 @@
`xmlns="jabber:client">`+
`<body>${message}</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<x xmlns="jabber:x:oob">`+
`<url>${message}</url>`+
`</x>`+
Expand Down Expand Up @@ -459,6 +460,7 @@
`xmlns="jabber:client">`+
`<body>${message}</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<x xmlns="jabber:x:oob">`+
`<url>${message}</url>`+
`</x>`+
Expand Down
96 changes: 96 additions & 0 deletions spec/messages.js
Expand Up @@ -77,6 +77,7 @@
`xmlns="jabber:client">`+
`<body>But soft, what light through yonder window breaks?</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+
`</message>`);
expect(view.model.messages.models.length).toBe(1);
Expand Down Expand Up @@ -181,6 +182,7 @@
`xmlns="jabber:client">`+
`<body>But soft, what light through yonder window breaks?</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+
`</message>`);
expect(view.model.messages.models.length).toBe(1);
Expand Down Expand Up @@ -1200,6 +1202,64 @@
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 () {

Expand Down Expand Up @@ -2010,6 +2070,7 @@
`xmlns="jabber:client">`+
`<body>But soft, what light through yonder window breaks?</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+
`</message>`);

Expand Down Expand Up @@ -2056,6 +2117,38 @@
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 () {

it("highlights all users mentioned via XEP-0372 references",
Expand Down Expand Up @@ -2201,6 +2294,7 @@
`xmlns="jabber:client">`+
`<body>hello z3r0 gibson mr.robot, how are you?</body>`+
`<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="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"/>`+
Expand All @@ -2226,6 +2320,7 @@
`xmlns="jabber:client">`+
`<body>hello z3r0 gibson sw0rdf1sh, how are you?</body>`+
`<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="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"/>`+
Expand Down Expand Up @@ -2274,6 +2369,7 @@
`xmlns="jabber:client">`+
`<body>hello z3r0 gibson mr.robot, how are you?</body>`+
`<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="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"/>`+
Expand Down
1 change: 1 addition & 0 deletions spec/omemo.js
Expand Up @@ -172,6 +172,7 @@
`to="max.frankfurter@localhost" `+
`type="chat" xmlns="jabber:client">`+
`<body>This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo</body>`+
`<request xmlns="urn:xmpp:receipts"/>`+
`<encrypted xmlns="eu.siacs.conversations.axolotl">`+
`<header sid="123456789">`+
`<key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key>`+
Expand Down
2 changes: 1 addition & 1 deletion src/converse-message-view.js
Expand Up @@ -86,7 +86,7 @@ converse.plugins.add('converse-message-view', {
if (this.model.changed.progress) {
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) {
await this.render();
}
Expand Down
1 change: 1 addition & 0 deletions src/converse-omemo.js
Expand Up @@ -394,6 +394,7 @@ converse.plugins.add('converse-omemo', {
'type': this.get('message_type'),
'id': message.get('msgid')
}).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.
// These headers simply contain the key that the
Expand Down