Commit 271c79ea authored by JC Brand's avatar JC Brand

Refactor message corrections

- Save date for older message versions and display it in the modal
- Properly handle the correction being received *before* the corrected message
parent 53520317
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
- New API method [\_converse.api.disco.features.get](https://conversejs.org/docs/html/api/-_converse.api.disco.features.html#.get) - New API method [\_converse.api.disco.features.get](https://conversejs.org/docs/html/api/-_converse.api.disco.features.html#.get)
- New config setting [muc_show_join_leave_status](https://conversejs.org/docs/html/configuration.html#muc-show-join-leave-status) - New config setting [muc_show_join_leave_status](https://conversejs.org/docs/html/configuration.html#muc-show-join-leave-status)
- New event: `chatBoxBlurred`. - New event: `chatBoxBlurred`.
- Properly handle message correction being received before the corrected message
- #1296: `embedded` view mode shows `chatbox-navback` arrow in header - #1296: `embedded` view mode shows `chatbox-navback` arrow in header
- #1465: When highlighting a roster contact, they're incorrectly shown as online - #1465: When highlighting a roster contact, they're incorrectly shown as online
- #1532: Converse reloads on enter pressed in the filter box - #1532: Converse reloads on enter pressed in the filter box
......
#conversejs { #conversejs {
.older-msg {
time {
font-weight: bold;
}
}
.message { .message {
.mention { .mention {
font-weight: bold; font-weight: bold;
......
...@@ -76,8 +76,11 @@ ...@@ -76,8 +76,11 @@
const corrected_message = view.model.messages.at(0); const corrected_message = view.model.messages.at(0);
expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid')); expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid'));
expect(corrected_message.get('correcting')).toBe(false); expect(corrected_message.get('correcting')).toBe(false);
expect(corrected_message.get('older_versions').length).toBe(1);
expect(corrected_message.get('older_versions')[0]).toBe('But soft, what light through yonder airlock breaks?'); const older_versions = corrected_message.get('older_versions');
const keys = Object.keys(older_versions);
expect(keys.length).toBe(1);
expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false); expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false);
...@@ -181,8 +184,11 @@ ...@@ -181,8 +184,11 @@
const corrected_message = view.model.messages.at(0); const corrected_message = view.model.messages.at(0);
expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid')); expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid'));
expect(corrected_message.get('correcting')).toBe(false); expect(corrected_message.get('correcting')).toBe(false);
expect(corrected_message.get('older_versions').length).toBe(1);
expect(corrected_message.get('older_versions')[0]).toBe('But soft, what light through yonder airlock breaks?'); const older_versions = corrected_message.get('older_versions');
const keys = Object.keys(older_versions);
expect(keys.length).toBe(1);
expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
await test_utils.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500); await test_utils.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500);
...@@ -1472,8 +1478,8 @@ ...@@ -1472,8 +1478,8 @@
await test_utils.waitUntil(() => u.isVisible(modal.el), 1000); await test_utils.waitUntil(() => u.isVisible(modal.el), 1000);
const older_msgs = modal.el.querySelectorAll('.older-msg'); const older_msgs = modal.el.querySelectorAll('.older-msg');
expect(older_msgs.length).toBe(2); expect(older_msgs.length).toBe(2);
expect(older_msgs[0].textContent).toBe('But soft, what light through yonder airlock breaks?'); expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME');
expect(older_msgs[1].textContent).toBe('But soft, what light through yonder chimney breaks?'); expect(older_msgs[0].childNodes[1].textContent).toBe(': But soft, what light through yonder airlock breaks?');
done(); done();
})); }));
...@@ -2425,8 +2431,10 @@ ...@@ -2425,8 +2431,10 @@
await test_utils.waitUntil(() => u.isVisible(modal.el), 1000); await test_utils.waitUntil(() => u.isVisible(modal.el), 1000);
const older_msgs = modal.el.querySelectorAll('.older-msg'); const older_msgs = modal.el.querySelectorAll('.older-msg');
expect(older_msgs.length).toBe(2); expect(older_msgs.length).toBe(2);
expect(older_msgs[0].textContent).toBe('But soft, what light through yonder airlock breaks?'); expect(older_msgs[0].childNodes[1].textContent).toBe(': But soft, what light through yonder airlock breaks?');
expect(older_msgs[1].textContent).toBe('But soft, what light through yonder chimney breaks?'); expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME');
expect(older_msgs[1].childNodes[0].nodeName).toBe('TIME');
expect(older_msgs[1].childNodes[1].textContent).toBe(': But soft, what light through yonder chimney breaks?');
done(); done();
})); }));
...@@ -2495,8 +2503,11 @@ ...@@ -2495,8 +2503,11 @@
const corrected_message = view.model.messages.at(0); const corrected_message = view.model.messages.at(0);
expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid')); expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid'));
expect(corrected_message.get('correcting')).toBe(false); expect(corrected_message.get('correcting')).toBe(false);
expect(corrected_message.get('older_versions').length).toBe(1);
expect(corrected_message.get('older_versions')[0]).toBe('But soft, what light through yonder airlock breaks?'); const older_versions = corrected_message.get('older_versions');
const keys = Object.keys(older_versions);
expect(keys.length).toBe(1);
expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false); expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false);
......
...@@ -69,7 +69,8 @@ converse.plugins.add('converse-message-view', { ...@@ -69,7 +69,8 @@ converse.plugins.add('converse-message-view', {
toHTML () { toHTML () {
return tpl_message_versions_modal(Object.assign( return tpl_message_versions_modal(Object.assign(
this.model.toJSON(), { this.model.toJSON(), {
'__': __ '__': __,
'dayjs': dayjs
})); }));
} }
}); });
......
...@@ -382,30 +382,52 @@ converse.plugins.add('converse-chatboxes', { ...@@ -382,30 +382,52 @@ converse.plugins.add('converse-chatboxes', {
} }
}, },
handleMessageCorrection (stanza) { /**
const replace = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop(); * If the passed in `message` stanza contains an
if (replace) { * [XEP-0308](https://xmpp.org/extensions/xep-0308.html#usecase)
const msgid = replace && replace.getAttribute('id') || stanza.getAttribute('id'), * `<replace>` element, return its `id` attribute.
message = msgid && this.messages.findWhere({msgid}); * @private
* @method _converse.ChatBox#getReplaceId
if (!message) { * @param { XMLElement } stanza
// XXX: Looks like we received a correction for a */
// non-existing message, probably due to MAM. getReplaceId (stanza) {
// Not clear what can be done about this... we'll const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
// just create it as a separate message for now. if (el) {
return false; return el.getAttribute('id');
}
const older_versions = message.get('older_versions') || [];
older_versions.push(message.get('message'));
message.save({
'message': this.getMessageBody(stanza),
'references': this.getReferencesFromStanza(stanza),
'older_versions': older_versions,
'edited': (new Date()).toISOString()
});
return true;
} }
return false; },
/**
* Determine whether the passed in message attributes represent a
* message which corrects a previously received message, or an
* older message which has already been corrected.
* In both cases, update the corrected message accordingly.
* @private
* @method _converse.ChatBox#correctMessage
* @param { object } attrs - Attributes representing a received
* message, as returned by
* {@link _converse.ChatBox.getMessageAttributesFromStanza}
*/
correctMessage (attrs) {
if (!attrs.msgid || !attrs.from) {
return;
}
const message = this.messages.findWhere({'msgid': attrs.msgid, 'from': attrs.from});
if (!message) {
return;
}
const older_versions = message.get('older_versions') || {};
if ((attrs.time < message.get('time')) && message.get('edited')) {
// This is an older message which has been corrected already
older_versions[attrs.time] = attrs['message'];
message.save({'older_versions': older_versions});
} else {
// This is a correction of an earlier message we already received
older_versions[message.get('time')] = message.get('message');
attrs = Object.assign(attrs, {'older_versions': older_versions});
message.save(attrs);
}
return message;
}, },
async getDuplicateMessage (stanza) { async getDuplicateMessage (stanza) {
...@@ -619,8 +641,8 @@ converse.plugins.add('converse-chatboxes', { ...@@ -619,8 +641,8 @@ converse.plugins.add('converse-chatboxes', {
const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint); const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint);
let message = this.messages.findWhere('correcting') let message = this.messages.findWhere('correcting')
if (message) { if (message) {
const older_versions = message.get('older_versions') || []; const older_versions = message.get('older_versions') || {};
older_versions.push(message.get('message')); older_versions[message.get('time')] = message.get('message');
message.save({ message.save({
'correcting': false, 'correcting': false,
'edited': (new Date()).toISOString(), 'edited': (new Date()).toISOString(),
...@@ -772,15 +794,17 @@ converse.plugins.add('converse-chatboxes', { ...@@ -772,15 +794,17 @@ converse.plugins.add('converse-chatboxes', {
* message stanza, if it was contained, otherwise it's the message stanza itself. * message stanza, if it was contained, otherwise it's the message stanza itself.
*/ */
getMessageAttributesFromStanza (stanza, original_stanza) { getMessageAttributesFromStanza (stanza, original_stanza) {
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, original_stanza).pop(), const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, original_stanza).pop();
delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop(), const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
text = this.getMessageBody(stanza) || undefined, const text = this.getMessageBody(stanza) || undefined;
chat_state = stanza.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING || const chat_state = stanza.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING ||
stanza.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED || stanza.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED ||
stanza.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE || stanza.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE ||
stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE || stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE ||
stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE; stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE;
const replaced_id = this.getReplaceId(stanza)
const msgid = replaced_id || stanza.getAttribute('id') || original_stanza.getAttribute('id');
const attrs = Object.assign({ const attrs = Object.assign({
'chat_state': chat_state, 'chat_state': chat_state,
'is_archived': this.isArchived(original_stanza), 'is_archived': this.isArchived(original_stanza),
...@@ -788,7 +812,7 @@ converse.plugins.add('converse-chatboxes', { ...@@ -788,7 +812,7 @@ converse.plugins.add('converse-chatboxes', {
'is_spoiler': !_.isNil(spoiler), 'is_spoiler': !_.isNil(spoiler),
'is_single_emoji': text ? u.isSingleEmoji(text) : false, 'is_single_emoji': text ? u.isSingleEmoji(text) : false,
'message': text, 'message': text,
'msgid': stanza.getAttribute('id'), 'msgid': msgid,
'references': this.getReferencesFromStanza(stanza), 'references': this.getReferencesFromStanza(stanza),
'subject': _.propertyOf(stanza.querySelector('subject'))('textContent'), 'subject': _.propertyOf(stanza.querySelector('subject'))('textContent'),
'thread': _.propertyOf(stanza.querySelector('thread'))('textContent'), 'thread': _.propertyOf(stanza.querySelector('thread'))('textContent'),
...@@ -796,7 +820,6 @@ converse.plugins.add('converse-chatboxes', { ...@@ -796,7 +820,6 @@ converse.plugins.add('converse-chatboxes', {
'type': stanza.getAttribute('type') 'type': stanza.getAttribute('type')
}, this.getStanzaIDs(original_stanza)); }, this.getStanzaIDs(original_stanza));
if (attrs.type === 'groupchat') { if (attrs.type === 'groupchat') {
attrs.from = stanza.getAttribute('from'); attrs.from = stanza.getAttribute('from');
attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from)); attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from));
...@@ -818,8 +841,13 @@ converse.plugins.add('converse-chatboxes', { ...@@ -818,8 +841,13 @@ converse.plugins.add('converse-chatboxes', {
if (spoiler) { if (spoiler) {
attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : ''; attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : '';
} }
if (replaced_id) {
attrs['edited'] = (new Date()).toISOString();
}
// We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates. // We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${attrs.from}`] || _converse.connection.getUniqueId(); attrs['id'] = attrs['origin_id'] ||
attrs[`stanza_id ${attrs.from}`] ||
_converse.connection.getUniqueId();
return attrs; return attrs;
}, },
...@@ -1021,13 +1049,12 @@ converse.plugins.add('converse-chatboxes', { ...@@ -1021,13 +1049,12 @@ converse.plugins.add('converse-chatboxes', {
chatbox.updateMessage(message, original_stanza); chatbox.updateMessage(message, original_stanza);
} }
if (!message && if (!message &&
!chatbox.handleMessageCorrection(stanza) &&
!chatbox.handleReceipt (stanza, from_jid, is_carbon, is_me, is_mam) && !chatbox.handleReceipt (stanza, from_jid, is_carbon, is_me, is_mam) &&
!chatbox.handleChatMarker(stanza, from_jid, is_carbon, is_roster_contact, is_mam)) { !chatbox.handleChatMarker(stanza, from_jid, is_carbon, is_roster_contact, is_mam)) {
const attrs = await chatbox.getMessageAttributesFromStanza(stanza, original_stanza); const attrs = await chatbox.getMessageAttributesFromStanza(stanza, original_stanza);
if (attrs['chat_state'] || !u.isEmptyMessage(attrs)) { if (attrs['chat_state'] || !u.isEmptyMessage(attrs)) {
const msg = chatbox.messages.create(attrs); const msg = chatbox.correctMessage(attrs) || chatbox.messages.create(attrs);
chatbox.incrementUnreadMsgCounter(msg); chatbox.incrementUnreadMsgCounter(msg);
} }
} }
......
...@@ -9,7 +9,7 @@ import Backbone from "backbone"; ...@@ -9,7 +9,7 @@ import Backbone from "backbone";
import BrowserStorage from "backbone.browserStorage"; import BrowserStorage from "backbone.browserStorage";
import Promise from "es6-promise/dist/es6-promise.auto"; import Promise from "es6-promise/dist/es6-promise.auto";
import _ from "./lodash.noconflict"; import _ from "./lodash.noconflict";
import advancedFormat from 'dayjs/plugin/advancedFormat' import advancedFormat from 'dayjs/plugin/advancedFormat';
import dayjs from "dayjs"; import dayjs from "dayjs";
import i18n from "./i18n"; import i18n from "./i18n";
import pluggable from "pluggable.js/src/pluggable"; import pluggable from "pluggable.js/src/pluggable";
...@@ -19,7 +19,7 @@ import u from "@converse/headless/utils/core"; ...@@ -19,7 +19,7 @@ import u from "@converse/headless/utils/core";
Backbone = Backbone.noConflict(); Backbone = Backbone.noConflict();
dayjs.extend(advancedFormat) dayjs.extend(advancedFormat);
// Strophe globals // Strophe globals
const b64_sha1 = SHA1.b64_sha1; const b64_sha1 = SHA1.b64_sha1;
......
...@@ -1158,19 +1158,19 @@ converse.plugins.add('converse-muc', { ...@@ -1158,19 +1158,19 @@ converse.plugins.add('converse-muc', {
this.updateMessage(message, original_stanza); this.updateMessage(message, original_stanza);
} }
if (message || if (message ||
this.handleMessageCorrection(stanza) ||
this.isReceipt(stanza) || this.isReceipt(stanza) ||
this.isChatMarker(stanza)) { this.isChatMarker(stanza)) {
return _converse.api.trigger('message', {'stanza': original_stanza}); return _converse.api.trigger('message', {'stanza': original_stanza});
} }
const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza); let attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
if (attrs.nick && if (attrs.nick &&
!this.subjectChangeHandled(attrs) && !this.subjectChangeHandled(attrs) &&
!this.ignorableCSN(attrs) && !this.ignorableCSN(attrs) &&
(attrs['chat_state'] || !u.isEmptyMessage(attrs))) { (attrs['chat_state'] || !u.isEmptyMessage(attrs))) {
const msg = this.messages.create(this.addOccupantData(attrs)); attrs = this.addOccupantData(attrs);
const msg = this.correctMessage(attrs) || this.messages.create(attrs);
this.incrementUnreadMsgCounter(msg); this.incrementUnreadMsgCounter(msg);
if (forwarded && msg && msg.get('sender') === 'me') { if (forwarded && msg && msg.get('sender') === 'me') {
msg.save({'received': (new Date()).toISOString()}); msg.save({'received': (new Date()).toISOString()});
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<h4>Older versions</h4> <h4>Older versions</h4>
{[o.older_versions.forEach(function (text) { ]} <p class="older-msg">{{{text}}}</p> {[ }); ]} {[Object.keys(o.older_versions).forEach(function (k) { ]} <p class="older-msg"><time>{{{o.dayjs(k).format('MMM D, YYYY, HH:mm:ss')}}}</time>: {{{o.older_versions[k]}}}</p> {[ }); ]}
<hr/> <hr/>
<h4>Current version</h4> <h4>Current version</h4>
<p>{{{o.message}}}</p> <p>{{{o.message}}}</p>
......
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