From 584f293d05f48eb860ac1738bd202b4a9911cd25 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 14 Apr 2018 21:13:02 +0200 Subject: [PATCH] Updated and refactored the work from @worlword * Use Promises instead of callbacks * Update to latest (Last Call) version of XEP-0363 * Move non-view specific methods to models instead * Add more tests updates #161 --- .eslintrc.json | 2 +- spec/http-file-upload.js | 87 +++++++++++ src/config.js | 1 + src/converse-chatboxes.js | 204 +++++++++++++++----------- src/converse-chatview.js | 61 +------- src/converse-http-file-upload.js | 34 ++--- src/converse-otr.js | 2 +- src/templates/help_message.html | 2 +- src/templates/toolbar.html | 3 +- src/templates/toolbar_fileupload.html | 2 +- 10 files changed, 236 insertions(+), 162 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 0c61c2b72..ab65dcb55 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -91,7 +91,7 @@ "max-depth": "error", "max-len": "off", "max-lines": "off", - "max-nested-callbacks": "error", + "max-nested-callbacks": "off", "max-params": "off", "max-statements": "off", "max-statements-per-line": "off", diff --git a/spec/http-file-upload.js b/spec/http-file-upload.js index ae6df92ef..8c5d789fd 100644 --- a/spec/http-file-upload.js +++ b/spec/http-file-upload.js @@ -173,6 +173,7 @@ describe("When supported", function () { + describe("A file upload toolbar button", function () { it("appears in private chats", mock.initConverseWithAsync(function (done, _converse) { @@ -201,6 +202,92 @@ it("appears in MUC chats", mock.initConverseWithAsync(function (done, _converse) { done(); })); + + describe("when clicked", function () { + it("a file upload slot is requested", mock.initConverseWithAsync(function (done, _converse) { + test_utils.waitUntilDiscoConfirmed( + _converse, _converse.domain, + [{'category': 'server', 'type':'IM'}], + ['http://jabber.org/protocol/disco#items'], [], 'info').then(function () { + + var IQ_stanzas = _converse.connection.IQ_stanzas; + + test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items').then(function () { + test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []).then(function () { + test_utils.createContacts(_converse, 'current'); + var contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost'; + test_utils.openChatBoxFor(_converse, contact_jid); + var view = _converse.chatboxviews.get(contact_jid); + var file = { + 'type': 'image/jpeg', + 'size': '23456' , + 'lastModifiedDate': "", + 'name': "my-juliet.jpg" + }; + view.model.sendFile(file); + return test_utils.waitUntil(function () { + return _.filter(IQ_stanzas, function (iq) { + return iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request'); + }); + }).then(function () { + var iq = IQ_stanzas.pop(); + expect(iq.toLocaleString()).toBe( + ""+ + ""+ + ""); + + var stanza = Strophe.xmlHtmlNode( + ""+ + ""+ + " "+ + "
Basic Base64String==
"+ + "
foo=bar; user=romeo
"+ + "
"+ + " "+ + "
"+ + "
").firstElementChild; + spyOn(view.model, 'uploadFile').and.callFake(function () { + return new window.Promise((resolve, reject) => { resolve(); }); + }); + var sent_stanza; + spyOn(_converse.connection, 'send').and.callFake(function (stanza) { + sent_stanza = stanza; + }); + _converse.connection._dataRecv(test_utils.createRequest(stanza)); + + return test_utils.waitUntil(function () { + return sent_stanza; + }).then(function () { + expect(view.model.uploadFile).toHaveBeenCalled(); + expect(sent_stanza.toLocaleString()).toBe( + ""+ + "https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg"+ + ""+ + ""+ + "https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg"+ + ""+ + ""); + done(); + }); + }); + }); + }); + }); + })); + }); }); }); }); diff --git a/src/config.js b/src/config.js index 18f4073fa..aef8d3c47 100644 --- a/src/config.js +++ b/src/config.js @@ -37,6 +37,7 @@ require.config({ "lodash.converter": "3rdparty/lodash.fp", "lodash.fp": "src/lodash.fp", "lodash.noconflict": "src/lodash.noconflict", + "message-utils": "src/utils/message", "muc-utils": "src/utils/muc", "pluggable": "node_modules/pluggable.js/dist/pluggable", "polyfill": "src/polyfill", diff --git a/src/converse-chatboxes.js b/src/converse-chatboxes.js index 054b95fca..16753f977 100644 --- a/src/converse-chatboxes.js +++ b/src/converse-chatboxes.js @@ -60,7 +60,7 @@ function openChat (jid) { if (!utils.isValidJID(jid)) { - return converse.log( + return _converse.log( `Invalid JID "${jid}" provided in URL fragment`, Strophe.LogLevel.WARN ); @@ -116,99 +116,137 @@ }); }, - createFileMessageStanza (message, to) { + createMessageStanza (message) { + /* Given a _converse.Message Backbone.Model, return the XML + * stanza that represents it. + * + * Parameters: + * (Object) message - The Backbone.Model representing the message + */ const stanza = $msg({ - 'from': _converse.connection.jid, - 'to': to, - 'type': 'chat', - 'id': message.get('msgid') - }).c('body').t(message.get('message')).up() - .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up() - .c('x', {'xmlns': Strophe.NS.OUTOFBAND}).c('url').t(message.get('message')).up(); + 'from': _converse.connection.jid, + 'to': this.get('jid'), + 'type': 'chat', + 'id': message.get('msgid') + }).c('body').t(message.get('message')).up() + .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up(); + if (message.get('is_spoiler')) { + if (message.get('spoiler_hint')) { + stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER }, message.get('spoiler_hint')).up(); + } else { + stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER }).up(); + } + } + if (message.get('file')) { + stanza.c('x', {'xmlns': Strophe.NS.OUTOFBAND}).c('url').t(message.get('message')).up(); + } return stanza; }, - sendFile (file, chatbox) { - const self = this; - const request_slot_url = 'upload.' + _converse.domain; - _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, request_slot_url) - .then((result) => { - chatbox.showHelpMessages([__('The file upload starts now')],'info'); - self.requestSlot(file, request_slot_url, function(data) { - if (!data) { - alert(__('File upload failed. Please check the log.')); - } else if (data.error) { - alert(__('File upload failed. Please check the log.')); - } else if (data.get && data.put) { - self.uploadFile(data.put, file, function() { - console.log(data.put); - chatbox.onMessageSubmitted(data.put, null, file); - }); - } - }); - }); + sendMessageStanza (message, file) { + const messageStanza = this.createMessageStanza(message, file); + _converse.connection.send(messageStanza); + if (_converse.forward_messages) { + // Forward the message, so that other connected resources are also aware of it. + _converse.connection.send( + $msg({ to: _converse.bare_jid, type: 'chat', id: message.get('msgid') }) + .c('forwarded', {'xmlns': Strophe.NS.FORWARD}) + .c('delay', { + 'xmns': Strophe.NS.DELAY, + 'stamp': moment().format() + }).up() + .cnode(messageStanza.tree()) + ); + } }, - requestSlot (file, request_slot_url, cb) { - const self = this; - console.log("try sending file to: " + request_slot_url); - const iq = converse.env.$iq({ - to: request_slot_url, - type: 'get' - }).c('request', { - xmlns: Strophe.NS.HTTPUPLOAD - }).c('filename').t(file.name) - .up() - .c('size').t(file.size); - - _converse.connection.sendIQ(iq, function(stanza) { - self.successfulRequestSlotCB(stanza, cb); - }, function(stanza) { - self.failedRequestSlotCB(stanza, cb); + sendMessage (attrs) { + /* Responsible for sending off a text message. + * + * Parameters: + * (Message) message - The chat message + */ + this.sendMessageStanza(this.messages.create(attrs)); + }, + + notifyUploadFailure (err_msg, error) { + err_msg = err_msg || __("Sorry, failed to upload the file"); + this.trigger('showHelpMessages', [err_msg], 'error'); + if (error instanceof Error) { + _converse.log(error, Strophe.LogLevel.ERROR); + } + }, + + sendFile (file) { + _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then((result) => { + if (!result.length) { + this.notifyUploadFailure(__("Sorry, file upload is not supported by your server.")); + } + const request_slot_url = result[0].id; + if (!request_slot_url) { + return this.notifyUploadFailure(__("Could not determine request slot URL for file upload")); + } + this.trigger('showHelpMessages', [__('The file upload starts now')], 'info'); + this.requestSlot(file, request_slot_url).then((stanza) => { + const slot = stanza.querySelector('slot'); + if (slot) { + const put = slot.querySelector('put').getAttribute('url'); + const get = slot.querySelector('get').getAttribute('url'); + this.uploadFile(put, file) + .then(_.bind(this.sendMessage, this, {'message': get, 'file': true})) + .catch(this.notifyUploadFailure.bind(this, null)); + } else { + this.notifyUploadFailure(); + } + }).catch(this.notifyUploadFailure.bind(this, null)); + }); + }, + + sendFiles (files) { + _.each(files, this.sendFile.bind(this)); + }, + + requestSlot (file, request_slot_url) { + /* Send out an IQ stanza to request a file upload slot. + * + * https://xmpp.org/extensions/xep-0363.html#request + */ + return new Promise((resolve, reject) => { + const iq = converse.env.$iq({ + 'from': _converse.jid, + 'to': request_slot_url, + 'type': 'get' + }).c('request', { + 'xmlns': Strophe.NS.HTTPUPLOAD, + 'filename': file.name, + 'size': file.size, + 'content-type': file.type + }) + _converse.connection.sendIQ(iq, resolve, reject); }); }, - uploadFile (url, file, callback) { - console.log("uploadFile start"); - const xmlhttp = new XMLHttpRequest(); - const contentType = 'application/octet-stream'; - xmlhttp.onreadystatechange = function() { - if (xmlhttp.readyState === XMLHttpRequest.DONE) { - console.log("Status: " + xmlhttp.status); - if (xmlhttp.status === 200 || xmlhttp.status === 201) { - if (callback) { - callback(); - } + uploadFile (url, file) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + _converse.log("Status: " + xhr.status, Strophe.LogLevel.INFO); + if (xhr.status === 200 || xhr.status === 201) { + resolve(url, file); + } else { + xhr.onerror(); + } } - else { - alert(__('Could not upload File please try again.')); - } - } - }; - - xmlhttp.open('PUT', url, true); - xmlhttp.setRequestHeader("Content-type", contentType); - xmlhttp.send(file); - }, - - successfulRequestSlotCB (stanza, cb) { - const slot = stanza.getElementsByTagName('slot')[0]; - - if (slot != undefined) { - var put = slot.getElementsByTagName('put')[0].textContent; - var get = slot.getElementsByTagName('get')[0].textContent; - cb({ - put: put, - get: get - }); - } else { - this.failedRequestSlotCB(stanza, cb); - } - }, - - failedRequestSlotCB (stanza, cb) { - alert(__('Could not upload File please try again.')); + }; + xhr.onerror = function () { + reject(xhr.responseText); + }; + xhr.open('PUT', url, true); + xhr.setRequestHeader("Content-type", 'application/octet-stream'); + xhr.send(file); + }); }, getMessageBody (message) { diff --git a/src/converse-chatview.js b/src/converse-chatview.js index f2e1f9c11..3f6b08595 100644 --- a/src/converse-chatview.js +++ b/src/converse-chatview.js @@ -268,7 +268,6 @@ this.model.on('change:chat_state', this.sendChatState, this); this.model.on('change:chat_status', this.onChatStatusChanged, this); this.model.on('showHelpMessages', this.showHelpMessages, this); - this.model.on('sendMessage', this.sendMessage, this); this.render(); this.fetchMessages(); _converse.emit('chatBoxOpened', this); @@ -372,8 +371,8 @@ } return _.extend(options || {}, { 'label_clear': __('Clear all messages'), - 'label_insert_smiley': __('Insert a smiley'), - 'label_start_call': __('Start a call'), + 'tooltip_insert_smiley': __('Insert emojis'), + 'tooltip_start_call': __('Start a call'), 'label_toggle_spoiler': label_toggle_spoiler, 'show_call_button': _converse.visible_toolbar_buttons.call, 'show_spoiler_button': _converse.visible_toolbar_buttons.spoiler, @@ -666,7 +665,7 @@ 'beforeend', tpl_help_message({ 'isodate': moment().format(), - 'type': type||'info', + 'type': type, 'message': xss.filterXSS(msg, {'whiteList': {'strong': []}}) }) ); @@ -796,55 +795,6 @@ }); }, - createMessageStanza (message) { - const stanza = $msg({ - 'from': _converse.connection.jid, - 'to': this.model.get('jid'), - 'type': 'chat', - 'id': message.get('msgid') - }).c('body').t(message.get('message')).up() - .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up(); - - if (message.get('is_spoiler')) { - if (message.get('spoiler_hint')) { - stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER }, message.get('spoiler_hint')); - } else { - stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER }); - } - } - return stanza; - }, - - sendMessage (message, file = null) { - /* Responsible for sending off a text message. - * - * Parameters: - * (Message) message - The chat message - */ - // TODO: We might want to send to specfic resources. - // Especially in the OTR case. - var messageStanza; - if (file !== null) { - messageStanza = this.model.createFileMessageStanza(message, this.model.get('jid')); - } - else { - messageStanza = this.createMessageStanza(message); - } - _converse.connection.send(messageStanza); - if (_converse.forward_messages) { - // Forward the message, so that other connected resources are also aware of it. - _converse.connection.send( - $msg({ to: _converse.bare_jid, type: 'chat', id: message.get('msgid') }) - .c('forwarded', {'xmlns': Strophe.NS.FORWARD}) - .c('delay', { - 'xmns': Strophe.NS.DELAY, - 'stamp': moment().format() - }).up() - .cnode(messageStanza.tree()) - ); - } - }, - parseMessageForCommands (text) { const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/); if (match) { @@ -864,7 +814,7 @@ } }, - onMessageSubmitted (text, spoiler_hint, file = null) { + onMessageSubmitted (text, spoiler_hint, file=null) { /* This method gets called once the user has typed a message * and then pressed enter in a chat box. * @@ -884,8 +834,7 @@ return; } const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint); - const message = this.model.messages.create(attrs); - this.sendMessage(message, file); + this.model.sendMessage(attrs, file); }, getOutgoingMessageAttributes (text, spoiler_hint) { diff --git a/src/converse-http-file-upload.js b/src/converse-http-file-upload.js index cfa96c949..c408188cc 100644 --- a/src/converse-http-file-upload.js +++ b/src/converse-http-file-upload.js @@ -32,7 +32,16 @@ ChatBoxView: { events: { 'click .upload-file': 'toggleFileUpload', - 'change input.fileupload': 'handleFileSelect' + 'change input.fileupload': 'onFileSelection' + }, + + + toggleFileUpload (ev) { + this.el.querySelector('input.fileupload').click(); + }, + + onFileSelection (evt) { + this.model.sendFiles(evt.target.files); }, addFileUploadButton (options) { @@ -45,30 +54,19 @@ renderToolbar (toolbar, options) { const { _converse } = this.__super__; const result = this.__super__.renderToolbar.apply(this, arguments); - _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain) - .then((result) => { - if (result.length) { - this.addFileUploadButton(); - } - }); + _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then((result) => { + if (result.length) { + this.addFileUploadButton(); + } + }); return result; - }, - - toggleFileUpload (ev) { - this.el.querySelector('.input.fileupload').click(); - }, - - handleFileSelect (evt) { - var files = evt.target.files; - var file = files[0]; - this.model.sendFile(file, this); } }, ChatRoomView: { events: { 'click .upload-file': 'toggleFileUpload', - 'change .input.fileupload': 'handleFileSelect' + 'change .input.fileupload': 'onFileSelection' } } } diff --git a/src/converse-otr.js b/src/converse-otr.js index bc60731b9..d6ec4b690 100644 --- a/src/converse-otr.js +++ b/src/converse-otr.js @@ -224,7 +224,7 @@ this.trigger('showReceivedOTRMessage', msg); }); this.otr.on('io', (msg) => { - this.trigger('sendMessage', new _converse.Message({ message: msg })); + this.sendMessage(new _converse.Message({'message':msg})); }); this.otr.on('error', (msg) => { this.trigger('showOTRError', msg); diff --git a/src/templates/help_message.html b/src/templates/help_message.html index 0ced3d29d..5424340d4 100644 --- a/src/templates/help_message.html +++ b/src/templates/help_message.html @@ -1 +1 @@ -
{{o.message}}
+
{{o.message}}
diff --git a/src/templates/toolbar.html b/src/templates/toolbar.html index 3dca527ff..1b87e4150 100644 --- a/src/templates/toolbar.html +++ b/src/templates/toolbar.html @@ -1,5 +1,6 @@ {[ if (o.use_emoji) { ]} -
  • +
  • +
  • {[ } ]} diff --git a/src/templates/toolbar_fileupload.html b/src/templates/toolbar_fileupload.html index 206c53203..2aaff6b0e 100644 --- a/src/templates/toolbar_fileupload.html +++ b/src/templates/toolbar_fileupload.html @@ -1,4 +1,4 @@ - +