Commit 2929647e authored by JC Brand's avatar JC Brand

Add support for correcting the last message sent

fixes #421
parent be58e2b9
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
- #161 XEP-0363: HTTP File Upload - #161 XEP-0363: HTTP File Upload
- #194 Include entity capabilities in outgoing presence stanzas - #194 Include entity capabilities in outgoing presence stanzas
- #337 API call to update a VCard - #337 API call to update a VCard
- #421 XEP-0308: Last Message Correction
- #968 Use nickname from VCard when joining a room - #968 Use nickname from VCard when joining a room
- #1091 There's now only one CSS file for all view modes. - #1091 There's now only one CSS file for all view modes.
- #1094 Show room members who aren't currently online - #1094 Show room members who aren't currently online
...@@ -22,7 +23,6 @@ ...@@ -22,7 +23,6 @@
If the device is trusted, localStorage is used and user data is cached indefinitely. If the device is trusted, localStorage is used and user data is cached indefinitely.
- Initial support for XEP-0357 Push Notifications, specifically registering an "App Server". - Initial support for XEP-0357 Push Notifications, specifically registering an "App Server".
- Add support for logging in via OAuth (see the [oauth_providers](https://conversejs.org/docs/html/configurations.html#oauth-providers) setting) - Add support for logging in via OAuth (see the [oauth_providers](https://conversejs.org/docs/html/configurations.html#oauth-providers) setting)
- XEP-0308: Render message corrections
### Bugfixes ### Bugfixes
......
...@@ -60,6 +60,7 @@ which shows you how to use the CDN (content delivery network) to quickly get a d ...@@ -60,6 +60,7 @@ which shows you how to use the CDN (content delivery network) to quickly get a d
- Server-side archiving of messages [XEP 313](http://xmpp.org/extensions/xep-0313.html) - Server-side archiving of messages [XEP 313](http://xmpp.org/extensions/xep-0313.html)
- Hidden Messages (aka Spoilers) [XEP 382](http://xmpp.org/extensions/xep-0382.html) - Hidden Messages (aka Spoilers) [XEP 382](http://xmpp.org/extensions/xep-0382.html)
- Client state indication [XEP 352](http://xmpp.org/extensions/xep-0352.html) - Client state indication [XEP 352](http://xmpp.org/extensions/xep-0352.html)
- Last Message Correction [XEP 308](http://xmpp.org/extensions/xep-0308.html)
- Off-the-record encryption - Off-the-record encryption
- Translated into 16 languages - Translated into 16 languages
......
...@@ -8051,36 +8051,6 @@ body.reset { ...@@ -8051,36 +8051,6 @@ body.reset {
#conversejs .toggle-controlbox span { #conversejs .toggle-controlbox span {
color: white; } color: white; }
@media (max-width: 767.98px) {
#conversejs:not(.converse-embedded) {
left: 0;
right: 0;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right); }
#conversejs:not(.converse-embedded) .converse-chatboxes {
margin: 0 !important;
flex-direction: row !important;
justify-content: space-between; }
#conversejs:not(.converse-embedded) .converse-chatboxes .converse-chatroom {
font-size: 14px; }
#conversejs:not(.converse-embedded) .converse-chatboxes .chatbox .box-flyout {
margin-left: 15px;
left: 0;
bottom: 0;
border-radius: 0;
width: 100vw !important;
height: 100vh !important; }
#conversejs:not(.converse-embedded) .converse-chatboxes #controlbox {
width: 100vw !important; }
#conversejs:not(.converse-embedded) .converse-chatboxes #controlbox .box-flyout {
width: 100vw !important;
height: 100vh !important; }
#conversejs:not(.converse-embedded) .converse-chatboxes #controlbox .sidebar {
display: block; }
#conversejs:not(.converse-embedded) .converse-chatboxes.sidebar-open .chatbox:not(#controlbox) {
display: none; }
#conversejs:not(.converse-embedded) .converse-chatboxes.sidebar-open #controlbox .controlbox-pane {
display: block; } }
#conversejs.converse-overlayed #controlbox { #conversejs.converse-overlayed #controlbox {
order: -1; order: -1;
min-width: 250px !important; min-width: 250px !important;
...@@ -8257,6 +8227,39 @@ body.reset { ...@@ -8257,6 +8227,39 @@ body.reset {
#conversejs.converse-mobile #controlbox #converse-login input[type=button] { #conversejs.converse-mobile #controlbox #converse-login input[type=button] {
width: auto; } width: auto; }
@media (max-width: 767.98px) {
#conversejs:not(.converse-embedded) {
left: 0;
right: 0;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right); }
#conversejs:not(.converse-embedded) .converse-chatboxes {
margin: 0 !important;
flex-direction: row !important;
justify-content: space-between; }
#conversejs:not(.converse-embedded) .converse-chatboxes .converse-chatroom {
font-size: 14px; }
#conversejs:not(.converse-embedded) .converse-chatboxes .chatbox .box-flyout {
margin-left: 15px;
left: 0;
bottom: 0;
border-radius: 0;
width: 100vw !important;
height: 100vh !important; }
#conversejs:not(.converse-embedded) .converse-chatboxes #controlbox {
width: 100vw !important; }
#conversejs:not(.converse-embedded) .converse-chatboxes #controlbox .box-flyout {
width: 100vw !important;
height: 100vh !important; }
#conversejs:not(.converse-embedded) .converse-chatboxes #controlbox .sidebar {
display: block; }
#conversejs:not(.converse-embedded) .converse-chatboxes.sidebar-open .chatbox:not(#controlbox) {
display: none; }
#conversejs:not(.converse-embedded) .converse-chatboxes.sidebar-open #controlbox .controlbox-pane {
display: block; }
#conversejs.converse-overlayed .converse-chatboxes .chatbox .box-flyout {
margin-left: 30px; } }
#conversejs #converse-roster { #conversejs #converse-roster {
text-align: left; text-align: left;
width: 100%; width: 100%;
......
...@@ -68305,6 +68305,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -68305,6 +68305,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
utils = _converse$env.utils, utils = _converse$env.utils,
_ = _converse$env._; _ = _converse$env._;
const u = converse.env.utils; const u = converse.env.utils;
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
converse.plugins.add('converse-chatboxes', { converse.plugins.add('converse-chatboxes', {
dependencies: ["converse-roster", "converse-vcard"], dependencies: ["converse-roster", "converse-vcard"],
overrides: { overrides: {
...@@ -68615,7 +68616,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -68615,7 +68616,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
'from': _converse.connection.jid, 'from': _converse.connection.jid,
'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('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(); }).up();
...@@ -68638,6 +68639,13 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -68638,6 +68639,13 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}).c('url').t(message.get('message')).up(); }).c('url').t(message.get('message')).up();
} }
if (message.get('edited')) {
stanza.c('replace', {
'xmlns': Strophe.NS.MESSAGE_CORRECT,
'id': message.get('msgid')
}).up();
}
return stanza; return stanza;
}, },
...@@ -68667,6 +68675,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -68667,6 +68675,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
return { return {
'fullname': fullname, 'fullname': fullname,
'replace': this.correction,
'from': _converse.bare_jid, 'from': _converse.bare_jid,
'sender': 'me', 'sender': 'me',
'time': moment().format(), 'time': moment().format(),
...@@ -68682,7 +68691,24 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -68682,7 +68691,24 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
* Parameters: * Parameters:
* (Message) message - The chat message * (Message) message - The chat message
*/ */
this.sendMessageStanza(this.messages.create(attrs)); if (attrs.replace) {
const message = this.messages.findWhere({
'id': attrs.replace
});
if (message) {
const older_versions = message.get('older_versions') || [];
older_versions.push(message.get('message'));
message.save({
'message': attrs.message,
'older_versions': older_versions,
'edited': true
});
return this.sendMessageStanza(message);
}
}
return this.sendMessageStanza(this.messages.create(attrs));
}, },
sendChatState() { sendChatState() {
...@@ -69177,6 +69203,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69177,6 +69203,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}); });
_converse.on('addClientFeatures', () => { _converse.on('addClientFeatures', () => {
_converse.api.disco.own.features.add(Strophe.NS.MESSAGE_CORRECT);
_converse.api.disco.own.features.add(Strophe.NS.HTTPUPLOAD); _converse.api.disco.own.features.add(Strophe.NS.HTTPUPLOAD);
_converse.api.disco.own.features.add(Strophe.NS.OUTOFBAND); _converse.api.disco.own.features.add(Strophe.NS.OUTOFBAND);
...@@ -69315,6 +69343,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69315,6 +69343,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
const u = converse.env.utils; const u = converse.env.utils;
const KEY = { const KEY = {
ENTER: 13, ENTER: 13,
UP_ARROW: 38,
FORWARD_SLASH: 47 FORWARD_SLASH: 47
}; };
converse.plugins.add('converse-chatview', { converse.plugins.add('converse-chatview', {
...@@ -69592,7 +69621,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69592,7 +69621,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
'click .toggle-smiley ul.emoji-picker li': 'insertEmoji', 'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
'click .toggle-smiley': 'toggleEmojiMenu', 'click .toggle-smiley': 'toggleEmojiMenu',
'click .upload-file': 'toggleFileUpload', 'click .upload-file': 'toggleFileUpload',
'keypress .chat-textarea': 'keyPressed', 'keyup .chat-textarea': 'keyPressed',
'input .chat-textarea': 'inputChanged' 'input .chat-textarea': 'inputChanged'
}, },
...@@ -70113,6 +70142,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70113,6 +70142,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
} }
const attrs = this.model.getOutgoingMessageAttributes(text, spoiler_hint); const attrs = this.model.getOutgoingMessageAttributes(text, spoiler_hint);
delete this.model.correction;
this.model.sendMessage(attrs); this.model.sendMessage(attrs);
}, },
...@@ -70175,6 +70205,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70175,6 +70205,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
*/ */
if (ev.keyCode === KEY.ENTER && !ev.shiftKey) { if (ev.keyCode === KEY.ENTER && !ev.shiftKey) {
this.onFormSubmitted(ev); this.onFormSubmitted(ev);
} else if (ev.keyCode === KEY.UP_ARROW && !ev.shiftKey) {
this.editPreviousMessage();
} else if (ev.keyCode !== KEY.FORWARD_SLASH && this.model.get('chat_state') !== _converse.COMPOSING) { } else if (ev.keyCode !== KEY.FORWARD_SLASH && this.model.get('chat_state') !== _converse.COMPOSING) {
// Set chat state to composing if keyCode is not a forward-slash // Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message). // (which would imply an internal command and not a message).
...@@ -70182,6 +70214,19 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70182,6 +70214,19 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
} }
}, },
editPreviousMessage() {
const msg = _.findLast(this.model.messages.models, msg => msg.get('message'));
if (msg) {
const textbox_el = this.el.querySelector('.chat-textarea');
textbox_el.value = msg.get('message');
textbox_el.focus(); // We don't set "correcting" the Backbone-way, because
// we don't want it to persist to storage.
this.model.correction = msg.get('id');
}
},
inputChanged(ev) { inputChanged(ev) {
ev.target.style.height = 'auto'; // Fixes weirdness ev.target.style.height = 'auto'; // Fixes weirdness
...@@ -71193,7 +71238,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71193,7 +71238,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
Strophe.addNamespace('HINTS', 'urn:xmpp:hints'); Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0'); Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0');
Strophe.addNamespace('MAM', 'urn:xmpp:mam:2'); Strophe.addNamespace('MAM', 'urn:xmpp:mam:2');
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick'); Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob'); Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob');
Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub'); Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
...@@ -87771,6 +87815,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -87771,6 +87815,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
return result; return result;
}; };
u.getUniqueId = function () {
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c === 'x' ? r : r & 0x3 | 0x8;
return v.toString(16);
});
};
return u; return u;
}); });
...@@ -87839,14 +87891,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -87839,14 +87891,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
})); }));
}; };
u.getUniqueId = function () {
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c === 'x' ? r : r & 0x3 | 0x8;
return v.toString(16);
});
};
u.xForm2webForm = function (field, stanza, domain) { u.xForm2webForm = function (field, stanza, domain) {
/* Takes a field in XMPP XForm (XEP-004: Data Forms) format /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
* and turns it into an HTML field. * and turns it into an HTML field.
...@@ -174,6 +174,7 @@ ...@@ -174,6 +174,7 @@
<li>Server-side archiving of messages (<a href="http://xmpp.org/extensions/xep-0313.html" target="_blank" rel="noopener">XEP 313</a>)</li> <li>Server-side archiving of messages (<a href="http://xmpp.org/extensions/xep-0313.html" target="_blank" rel="noopener">XEP 313</a>)</li>
<li>Hidden messages (aka Spoilers) (<a href="http://xmpp.org/extensions/xep-0382.html" target="_blank" rel="noopener">XEP 382</a>)</li> <li>Hidden messages (aka Spoilers) (<a href="http://xmpp.org/extensions/xep-0382.html" target="_blank" rel="noopener">XEP 382</a>)</li>
<li>Client state indication (<a href="http://xmpp.org/extensions/xep-0352.html" target="_blank" rel="noopener">XEP 352</a>)</li> <li>Client state indication (<a href="http://xmpp.org/extensions/xep-0352.html" target="_blank" rel="noopener">XEP 352</a>)</li>
<li>Last Message Correction (<a href="http://xmpp.org/extensions/xep-0308.html" target="_blank" rel="noopener">XEP 308</a>)</li>
<li>Off-the-record encryption</li> <li>Off-the-record encryption</li>
<li>Supports anonymous logins, see the <a href="https://conversejs.org/demo/anonymous.html" target="_blank" rel="noopener">anonymous login demo</a>.</li> <li>Supports anonymous logins, see the <a href="https://conversejs.org/demo/anonymous.html" target="_blank" rel="noopener">anonymous login demo</a>.</li>
<li>Translated into 17 languages</li> <li>Translated into 17 languages</li>
......
...@@ -350,59 +350,6 @@ ...@@ -350,59 +350,6 @@
} }
} }
@include media-breakpoint-down(sm) {
#conversejs:not(.converse-embedded) {
left: 0;
right: 0;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
.converse-chatboxes {
margin: 0 !important;
flex-direction: row !important;
justify-content: space-between;
.converse-chatroom {
font-size: 14px;
}
.chatbox {
.box-flyout {
margin-left: 15px; // Counteracts Bootstrap margins, but
// not clear why needed...
left: 0;
bottom: 0;
border-radius: 0;
width: 100vw !important;
height: 100vh !important;
}
}
#controlbox {
width: 100vw !important;
.box-flyout {
width: 100vw !important;
height: 100vh !important;
}
.sidebar {
display: block;
}
}
&.sidebar-open {
.chatbox:not(#controlbox) {
display: none;
}
#controlbox {
.controlbox-pane {
display: block;
}
}
}
}
}
}
#conversejs.converse-overlayed { #conversejs.converse-overlayed {
#controlbox { #controlbox {
...@@ -563,3 +510,67 @@ ...@@ -563,3 +510,67 @@
} }
} }
} }
@include media-breakpoint-down(sm) {
#conversejs:not(.converse-embedded) {
left: 0;
right: 0;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
.converse-chatboxes {
margin: 0 !important;
flex-direction: row !important;
justify-content: space-between;
.converse-chatroom {
font-size: 14px;
}
.chatbox {
.box-flyout {
margin-left: 15px; // Counteracts Bootstrap margins, but
// not clear why needed...
left: 0;
bottom: 0;
border-radius: 0;
width: 100vw !important;
height: 100vh !important;
}
}
#controlbox {
width: 100vw !important;
.box-flyout {
width: 100vw !important;
height: 100vh !important;
}
.sidebar {
display: block;
}
}
&.sidebar-open {
.chatbox:not(#controlbox) {
display: none;
}
#controlbox {
.controlbox-pane {
display: block;
}
}
}
}
}
#conversejs.converse-overlayed {
.converse-chatboxes {
.chatbox {
.box-flyout {
margin-left: 30px; // Counteracts Bootstrap margins, but
// not clear why needed...
}
}
}
}
}
...@@ -635,7 +635,7 @@ ...@@ -635,7 +635,7 @@
spyOn(_converse.connection, 'send'); spyOn(_converse.connection, 'send');
spyOn(_converse, 'emit'); spyOn(_converse, 'emit');
view.keyPressed({ view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'), target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
expect(view.model.get('chat_state')).toBe('composing'); expect(view.model.get('chat_state')).toBe('composing');
...@@ -648,7 +648,7 @@ ...@@ -648,7 +648,7 @@
// The notification is not sent again // The notification is not sent again
view.keyPressed({ view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'), target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
expect(view.model.get('chat_state')).toBe('composing'); expect(view.model.get('chat_state')).toBe('composing');
...@@ -776,7 +776,7 @@ ...@@ -776,7 +776,7 @@
spyOn(view, 'setChatState').and.callThrough(); spyOn(view, 'setChatState').and.callThrough();
expect(view.model.get('chat_state')).toBe('active'); expect(view.model.get('chat_state')).toBe('active');
view.keyPressed({ view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'), target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
expect(view.model.get('chat_state')).toBe('composing'); expect(view.model.get('chat_state')).toBe('composing');
...@@ -803,14 +803,14 @@ ...@@ -803,14 +803,14 @@
// out if the user simply types longer than the // out if the user simply types longer than the
// timeout. // timeout.
view.keyPressed({ view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'), target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
expect(view.setChatState).toHaveBeenCalled(); expect(view.setChatState).toHaveBeenCalled();
expect(view.model.get('chat_state')).toBe('composing'); expect(view.model.get('chat_state')).toBe('composing');
view.keyPressed({ view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'), target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
expect(view.model.get('chat_state')).toBe('composing'); expect(view.model.get('chat_state')).toBe('composing');
...@@ -921,33 +921,25 @@ ...@@ -921,33 +921,25 @@
contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(_converse, contact_jid); test_utils.openChatBoxFor(_converse, contact_jid);
view = _converse.chatboxviews.get(contact_jid); view = _converse.chatboxviews.get(contact_jid);
return test_utils.waitUntil(function () { return test_utils.waitUntil(() => view.model.get('chat_state') === 'active', 500);
return view.model.get('chat_state') === 'active';
}, 500);
}).then(function () { }).then(function () {
console.log('chat_state set to active'); console.log('chat_state set to active');
view = _converse.chatboxviews.get(contact_jid); view = _converse.chatboxviews.get(contact_jid);
expect(view.model.get('chat_state')).toBe('active'); expect(view.model.get('chat_state')).toBe('active');
view.keyPressed({ view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'), target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
return test_utils.waitUntil(function () { return test_utils.waitUntil(() => view.model.get('chat_state') === 'composing', 500);
return view.model.get('chat_state') === 'composing';
}, 500);
}).then(function () { }).then(function () {
console.log('chat_state set to composing'); console.log('chat_state set to composing');
view = _converse.chatboxviews.get(contact_jid); view = _converse.chatboxviews.get(contact_jid);
expect(view.model.get('chat_state')).toBe('composing'); expect(view.model.get('chat_state')).toBe('composing');
spyOn(_converse.connection, 'send'); spyOn(_converse.connection, 'send');
return test_utils.waitUntil(function () { return test_utils.waitUntil(() => view.model.get('chat_state') === 'paused', 500);
return view.model.get('chat_state') === 'paused';
}, 500);
}).then(function () { }).then(function () {
console.log('chat_state set to paused'); console.log('chat_state set to paused');
return test_utils.waitUntil(function () { return test_utils.waitUntil(() => view.model.get('chat_state') === 'inactive', 500);
return view.model.get('chat_state') === 'inactive';
}, 500);
}).then(function () { }).then(function () {
console.log('chat_state set to inactive'); console.log('chat_state set to inactive');
expect(_converse.connection.send).toHaveBeenCalled(); expect(_converse.connection.send).toHaveBeenCalled();
......
...@@ -136,6 +136,70 @@ ...@@ -136,6 +136,70 @@
}); });
})); }));
it("can be sent as a correction",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
test_utils.createContacts(_converse, 'current', 1);
test_utils.openControlBox();
const message = 'This is a received message';
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
const textarea = view.el.querySelector('textarea.chat-textarea');
expect(textarea.value).toBe('');
view.keyPressed({
target: textarea,
keyCode: 38
});
expect(textarea.value).toBe('');
textarea.value = 'But soft, what light through yonder airlock breaks?';
view.keyPressed({
target: textarea,
preventDefault: _.noop,
keyCode: 13
});
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.el.querySelector('.chat-msg-text').textContent)
.toBe('But soft, what light through yonder airlock breaks?');
const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
expect(textarea.value).toBe('');
view.keyPressed({
target: textarea,
keyCode: 38
});
expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
spyOn(_converse.connection, 'send');
textarea.value = 'But soft, what light through yonder window breaks?';
view.keyPressed({
target: textarea,
preventDefault: _.noop,
keyCode: 13
});
expect(_converse.connection.send).toHaveBeenCalled();
const msg = _converse.connection.send.calls.all()[0].args[0];
expect(msg.toLocaleString())
.toBe(`<message from='dummy@localhost/resource' `+
`to='max.frankfurter@localhost' type='chat' id='${msg.nodeTree.getAttribute('id')}' `+
`xmlns='jabber:client'>`+
`<body>But soft, what light through yonder window breaks?</body>`+
`<active xmlns='http://jabber.org/protocol/chatstates'/>`+
`<replace xmlns='urn:xmpp:message-correct:0' id='${first_msg.get('msgid')}'/>`+
`</message>`);
expect(view.model.messages.models.length).toBe(1);
const corrected_message = view.model.messages.at(0);
expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid'));
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?');
done();
}));
describe("when a chatbox is opened for someone who is not in the roster", function () { describe("when a chatbox is opened for someone who is not in the roster", function () {
it("the VCard for that user is fetched and the chatbox updated with the results", it("the VCard for that user is fetched and the chatbox updated with the results",
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
"<presence xmlns='jabber:client'>"+ "<presence xmlns='jabber:client'>"+
"<status>Hello world</status>"+ "<status>Hello world</status>"+
"<priority>0</priority>"+ "<priority>0</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='1J7kq1MEvnB6ea6vKcgCsSE37gw='/>"+ "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='wmJWAEmiBuDhg0VUoDmqHp3qXJ0='/>"+
"</presence>" "</presence>"
); );
_converse.priority = 2; _converse.priority = 2;
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
"<show>away</show>"+ "<show>away</show>"+
"<status>Going jogging</status>"+ "<status>Going jogging</status>"+
"<priority>2</priority>"+ "<priority>2</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='1J7kq1MEvnB6ea6vKcgCsSE37gw='/>"+ "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='wmJWAEmiBuDhg0VUoDmqHp3qXJ0='/>"+
"</presence>" "</presence>"
); );
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
"<show>dnd</show>"+ "<show>dnd</show>"+
"<status>Doing taxes</status>"+ "<status>Doing taxes</status>"+
"<priority>0</priority>"+ "<priority>0</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='1J7kq1MEvnB6ea6vKcgCsSE37gw='/>"+ "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='wmJWAEmiBuDhg0VUoDmqHp3qXJ0='/>"+
"</presence>" "</presence>"
); );
})); }));
...@@ -97,7 +97,7 @@ ...@@ -97,7 +97,7 @@
.toBe("<presence xmlns='jabber:client'>"+ .toBe("<presence xmlns='jabber:client'>"+
"<status>My custom status</status>"+ "<status>My custom status</status>"+
"<priority>0</priority>"+ "<priority>0</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='1J7kq1MEvnB6ea6vKcgCsSE37gw='/>"+ "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='wmJWAEmiBuDhg0VUoDmqHp3qXJ0='/>"+
"</presence>") "</presence>")
return test_utils.waitUntil(function () { return test_utils.waitUntil(function () {
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
modal.el.querySelector('[type="submit"]').click(); modal.el.querySelector('[type="submit"]').click();
expect(_converse.connection.send.calls.mostRecent().args[0].toLocaleString()) expect(_converse.connection.send.calls.mostRecent().args[0].toLocaleString())
.toBe("<presence xmlns='jabber:client'><show>dnd</show><status>My custom status</status><priority>0</priority>"+ .toBe("<presence xmlns='jabber:client'><show>dnd</show><status>My custom status</status><priority>0</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='1J7kq1MEvnB6ea6vKcgCsSE37gw='/>"+ "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='wmJWAEmiBuDhg0VUoDmqHp3qXJ0='/>"+
"</presence>") "</presence>")
done(); done();
}); });
......
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
const { $msg, Backbone, Promise, Strophe, b64_sha1, moment, sizzle, utils, _ } = converse.env; const { $msg, Backbone, Promise, Strophe, b64_sha1, moment, sizzle, utils, _ } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
converse.plugins.add('converse-chatboxes', { converse.plugins.add('converse-chatboxes', {
...@@ -43,7 +45,7 @@ ...@@ -43,7 +45,7 @@
* loaded by converse.js's plugin machinery. * loaded by converse.js's plugin machinery.
*/ */
const { _converse } = this, const { _converse } = this,
{ __ } = _converse; { __ } = _converse;
// Configuration values for this plugin // Configuration values for this plugin
// ==================================== // ====================================
...@@ -314,7 +316,7 @@ ...@@ -314,7 +316,7 @@
'from': _converse.connection.jid, 'from': _converse.connection.jid,
'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('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();
...@@ -328,6 +330,12 @@ ...@@ -328,6 +330,12 @@
if (message.get('file')) { if (message.get('file')) {
stanza.c('x', {'xmlns': Strophe.NS.OUTOFBAND}).c('url').t(message.get('message')).up(); stanza.c('x', {'xmlns': Strophe.NS.OUTOFBAND}).c('url').t(message.get('message')).up();
} }
if (message.get('edited')) {
stanza.c('replace', {
'xmlns': Strophe.NS.MESSAGE_CORRECT,
'id': message.get('msgid')
}).up();
}
return stanza; return stanza;
}, },
...@@ -357,6 +365,7 @@ ...@@ -357,6 +365,7 @@
return { return {
'fullname': fullname, 'fullname': fullname,
'replace': this.correction,
'from': _converse.bare_jid, 'from': _converse.bare_jid,
'sender': 'me', 'sender': 'me',
'time': moment().format(), 'time': moment().format(),
...@@ -372,7 +381,20 @@ ...@@ -372,7 +381,20 @@
* Parameters: * Parameters:
* (Message) message - The chat message * (Message) message - The chat message
*/ */
this.sendMessageStanza(this.messages.create(attrs)); if (attrs.replace) {
const message = this.messages.findWhere({'id': attrs.replace})
if (message) {
const older_versions = message.get('older_versions') || [];
older_versions.push(message.get('message'));
message.save({
'message': attrs.message,
'older_versions': older_versions,
'edited': true
});
return this.sendMessageStanza(message);
}
}
return this.sendMessageStanza(this.messages.create(attrs));
}, },
sendChatState () { sendChatState () {
...@@ -826,6 +848,7 @@ ...@@ -826,6 +848,7 @@
_converse.on('addClientFeatures', () => { _converse.on('addClientFeatures', () => {
_converse.api.disco.own.features.add(Strophe.NS.MESSAGE_CORRECT);
_converse.api.disco.own.features.add(Strophe.NS.HTTPUPLOAD); _converse.api.disco.own.features.add(Strophe.NS.HTTPUPLOAD);
_converse.api.disco.own.features.add(Strophe.NS.OUTOFBAND); _converse.api.disco.own.features.add(Strophe.NS.OUTOFBAND);
}); });
......
...@@ -54,6 +54,7 @@ ...@@ -54,6 +54,7 @@
const u = converse.env.utils; const u = converse.env.utils;
const KEY = { const KEY = {
ENTER: 13, ENTER: 13,
UP_ARROW: 38,
FORWARD_SLASH: 47 FORWARD_SLASH: 47
}; };
...@@ -333,7 +334,7 @@ ...@@ -333,7 +334,7 @@
'click .toggle-smiley ul.emoji-picker li': 'insertEmoji', 'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
'click .toggle-smiley': 'toggleEmojiMenu', 'click .toggle-smiley': 'toggleEmojiMenu',
'click .upload-file': 'toggleFileUpload', 'click .upload-file': 'toggleFileUpload',
'keypress .chat-textarea': 'keyPressed', 'keyup .chat-textarea': 'keyPressed',
'input .chat-textarea': 'inputChanged' 'input .chat-textarea': 'inputChanged'
}, },
...@@ -847,6 +848,7 @@ ...@@ -847,6 +848,7 @@
return; return;
} }
const attrs = this.model.getOutgoingMessageAttributes(text, spoiler_hint); const attrs = this.model.getOutgoingMessageAttributes(text, spoiler_hint);
delete this.model.correction;
this.model.sendMessage(attrs); this.model.sendMessage(attrs);
}, },
...@@ -912,6 +914,8 @@ ...@@ -912,6 +914,8 @@
*/ */
if (ev.keyCode === KEY.ENTER && !ev.shiftKey) { if (ev.keyCode === KEY.ENTER && !ev.shiftKey) {
this.onFormSubmitted(ev); this.onFormSubmitted(ev);
} else if (ev.keyCode === KEY.UP_ARROW && !ev.shiftKey) {
this.editPreviousMessage();
} else if (ev.keyCode !== KEY.FORWARD_SLASH && this.model.get('chat_state') !== _converse.COMPOSING) { } else if (ev.keyCode !== KEY.FORWARD_SLASH && this.model.get('chat_state') !== _converse.COMPOSING) {
// Set chat state to composing if keyCode is not a forward-slash // Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message). // (which would imply an internal command and not a message).
...@@ -919,6 +923,18 @@ ...@@ -919,6 +923,18 @@
} }
}, },
editPreviousMessage () {
const msg = _.findLast(this.model.messages.models, (msg) => msg.get('message'));
if (msg) {
const textbox_el = this.el.querySelector('.chat-textarea');
textbox_el.value = msg.get('message');
textbox_el.focus()
// We don't set "correcting" the Backbone-way, because
// we don't want it to persist to storage.
this.model.correction = msg.get('id');
}
},
inputChanged (ev) { inputChanged (ev) {
ev.target.style.height = 'auto'; // Fixes weirdness ev.target.style.height = 'auto'; // Fixes weirdness
ev.target.style.height = (ev.target.scrollHeight) + 'px'; ev.target.style.height = (ev.target.scrollHeight) + 'px';
......
...@@ -36,7 +36,6 @@ ...@@ -36,7 +36,6 @@
Strophe.addNamespace('HINTS', 'urn:xmpp:hints'); Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0'); Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0');
Strophe.addNamespace('MAM', 'urn:xmpp:mam:2'); Strophe.addNamespace('MAM', 'urn:xmpp:mam:2');
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick'); Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob'); Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob');
Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub'); Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
......
...@@ -844,5 +844,12 @@ ...@@ -844,5 +844,12 @@
return result; return result;
}; };
u.getUniqueId = function () {
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c === 'x' ? r : r & 0x3 | 0x8;
return v.toString(16);
});
};
return u; return u;
})); }));
...@@ -73,14 +73,6 @@ ...@@ -73,14 +73,6 @@
); );
}; };
u.getUniqueId = function () {
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c === 'x' ? r : r & 0x3 | 0x8;
return v.toString(16);
});
};
u.xForm2webForm = function (field, stanza, domain) { u.xForm2webForm = function (field, stanza, domain) {
/* Takes a field in XMPP XForm (XEP-004: Data Forms) format /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
* and turns it into an HTML field. * and turns it into an HTML field.
......
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