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 @@
- #161 XEP-0363: HTTP File Upload
- #194 Include entity capabilities in outgoing presence stanzas
- #337 API call to update a VCard
- #421 XEP-0308: Last Message Correction
- #968 Use nickname from VCard when joining a room
- #1091 There's now only one CSS file for all view modes.
- #1094 Show room members who aren't currently online
......@@ -22,7 +23,6 @@
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".
- 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
......
......@@ -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)
- 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)
- Last Message Correction [XEP 308](http://xmpp.org/extensions/xep-0308.html)
- Off-the-record encryption
- Translated into 16 languages
......
......@@ -8051,36 +8051,6 @@ body.reset {
#conversejs .toggle-controlbox span {
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 {
order: -1;
min-width: 250px !important;
......@@ -8257,6 +8227,39 @@ body.reset {
#conversejs.converse-mobile #controlbox #converse-login input[type=button] {
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 {
text-align: left;
width: 100%;
......
......@@ -68305,6 +68305,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
utils = _converse$env.utils,
_ = _converse$env._;
const u = converse.env.utils;
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
converse.plugins.add('converse-chatboxes', {
dependencies: ["converse-roster", "converse-vcard"],
overrides: {
......@@ -68615,7 +68616,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
'from': _converse.connection.jid,
'to': this.get('jid'),
'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, {
'xmlns': Strophe.NS.CHATSTATES
}).up();
......@@ -68638,6 +68639,13 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}).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;
},
......@@ -68667,6 +68675,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
return {
'fullname': fullname,
'replace': this.correction,
'from': _converse.bare_jid,
'sender': 'me',
'time': moment().format(),
......@@ -68682,7 +68691,24 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
* Parameters:
* (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() {
......@@ -69177,6 +69203,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
});
_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.OUTOFBAND);
......@@ -69315,6 +69343,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
const u = converse.env.utils;
const KEY = {
ENTER: 13,
UP_ARROW: 38,
FORWARD_SLASH: 47
};
converse.plugins.add('converse-chatview', {
......@@ -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': 'toggleEmojiMenu',
'click .upload-file': 'toggleFileUpload',
'keypress .chat-textarea': 'keyPressed',
'keyup .chat-textarea': 'keyPressed',
'input .chat-textarea': 'inputChanged'
},
......@@ -70113,6 +70142,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}
const attrs = this.model.getOutgoingMessageAttributes(text, spoiler_hint);
delete this.model.correction;
this.model.sendMessage(attrs);
},
......@@ -70175,6 +70205,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
*/
if (ev.keyCode === KEY.ENTER && !ev.shiftKey) {
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) {
// Set chat state to composing if keyCode is not a forward-slash
// (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_
}
},
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) {
ev.target.style.height = 'auto'; // Fixes weirdness
......@@ -71193,7 +71238,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0');
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('OUTOFBAND', 'jabber:x:oob');
Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
......@@ -87771,6 +87815,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
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;
});
......@@ -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) {
/* Takes a field in XMPP XForm (XEP-004: Data Forms) format
* and turns it into an HTML field.
......@@ -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>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>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>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>
......
......@@ -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 {
#controlbox {
......@@ -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 @@
spyOn(_converse.connection, 'send');
spyOn(_converse, 'emit');
view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'),
target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1
});
expect(view.model.get('chat_state')).toBe('composing');
......@@ -648,7 +648,7 @@
// The notification is not sent again
view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'),
target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1
});
expect(view.model.get('chat_state')).toBe('composing');
......@@ -776,7 +776,7 @@
spyOn(view, 'setChatState').and.callThrough();
expect(view.model.get('chat_state')).toBe('active');
view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'),
target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1
});
expect(view.model.get('chat_state')).toBe('composing');
......@@ -803,14 +803,14 @@
// out if the user simply types longer than the
// timeout.
view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'),
target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1
});
expect(view.setChatState).toHaveBeenCalled();
expect(view.model.get('chat_state')).toBe('composing');
view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'),
target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1
});
expect(view.model.get('chat_state')).toBe('composing');
......@@ -921,33 +921,25 @@
contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(_converse, contact_jid);
view = _converse.chatboxviews.get(contact_jid);
return test_utils.waitUntil(function () {
return view.model.get('chat_state') === 'active';
}, 500);
return test_utils.waitUntil(() => view.model.get('chat_state') === 'active', 500);
}).then(function () {
console.log('chat_state set to active');
view = _converse.chatboxviews.get(contact_jid);
expect(view.model.get('chat_state')).toBe('active');
view.keyPressed({
target: $(view.el).find('textarea.chat-textarea'),
target: view.el.querySelector('textarea.chat-textarea'),
keyCode: 1
});
return test_utils.waitUntil(function () {
return view.model.get('chat_state') === 'composing';
}, 500);
return test_utils.waitUntil(() => view.model.get('chat_state') === 'composing', 500);
}).then(function () {
console.log('chat_state set to composing');
view = _converse.chatboxviews.get(contact_jid);
expect(view.model.get('chat_state')).toBe('composing');
spyOn(_converse.connection, 'send');
return test_utils.waitUntil(function () {
return view.model.get('chat_state') === 'paused';
}, 500);
return test_utils.waitUntil(() => view.model.get('chat_state') === 'paused', 500);
}).then(function () {
console.log('chat_state set to paused');
return test_utils.waitUntil(function () {
return view.model.get('chat_state') === 'inactive';
}, 500);
return test_utils.waitUntil(() => view.model.get('chat_state') === 'inactive', 500);
}).then(function () {
console.log('chat_state set to inactive');
expect(_converse.connection.send).toHaveBeenCalled();
......
......@@ -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 () {
it("the VCard for that user is fetched and the chatbox updated with the results",
......
......@@ -47,7 +47,7 @@
"<presence xmlns='jabber:client'>"+
"<status>Hello world</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>"
);
_converse.priority = 2;
......@@ -57,7 +57,7 @@
"<show>away</show>"+
"<status>Going jogging</status>"+
"<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>"
);
......@@ -68,7 +68,7 @@
"<show>dnd</show>"+
"<status>Doing taxes</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>"
);
}));
......@@ -97,7 +97,7 @@
.toBe("<presence xmlns='jabber:client'>"+
"<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>")
return test_utils.waitUntil(function () {
......@@ -113,7 +113,7 @@
modal.el.querySelector('[type="submit"]').click();
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>"+
"<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>")
done();
});
......
......@@ -19,6 +19,8 @@
const { $msg, Backbone, Promise, Strophe, b64_sha1, moment, sizzle, utils, _ } = converse.env;
const u = converse.env.utils;
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
converse.plugins.add('converse-chatboxes', {
......@@ -314,7 +316,7 @@
'from': _converse.connection.jid,
'to': this.get('jid'),
'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, {'xmlns': Strophe.NS.CHATSTATES}).up();
......@@ -328,6 +330,12 @@
if (message.get('file')) {
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;
},
......@@ -357,6 +365,7 @@
return {
'fullname': fullname,
'replace': this.correction,
'from': _converse.bare_jid,
'sender': 'me',
'time': moment().format(),
......@@ -372,7 +381,20 @@
* Parameters:
* (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 () {
......@@ -826,6 +848,7 @@
_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.OUTOFBAND);
});
......
......@@ -54,6 +54,7 @@
const u = converse.env.utils;
const KEY = {
ENTER: 13,
UP_ARROW: 38,
FORWARD_SLASH: 47
};
......@@ -333,7 +334,7 @@
'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
'click .toggle-smiley': 'toggleEmojiMenu',
'click .upload-file': 'toggleFileUpload',
'keypress .chat-textarea': 'keyPressed',
'keyup .chat-textarea': 'keyPressed',
'input .chat-textarea': 'inputChanged'
},
......@@ -847,6 +848,7 @@
return;
}
const attrs = this.model.getOutgoingMessageAttributes(text, spoiler_hint);
delete this.model.correction;
this.model.sendMessage(attrs);
},
......@@ -912,6 +914,8 @@
*/
if (ev.keyCode === KEY.ENTER && !ev.shiftKey) {
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) {
// Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message).
......@@ -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) {
ev.target.style.height = 'auto'; // Fixes weirdness
ev.target.style.height = (ev.target.scrollHeight) + 'px';
......
......@@ -36,7 +36,6 @@
Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0');
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('OUTOFBAND', 'jabber:x:oob');
Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
......
......@@ -844,5 +844,12 @@
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;
}));
......@@ -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) {
/* Takes a field in XMPP XForm (XEP-004: Data Forms) format
* 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