Quellcode durchsuchen

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
JC Brand vor 7 Jahren
Ursprung
Commit
584f293d05

+ 1 - 1
.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",

+ 87 - 0
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(
+                                            "<iq from='dummy@localhost/resource' "+
+                                                "to='upload.montague.tld' "+
+                                                "type='get' "+
+                                                "xmlns='jabber:client' "+
+                                                "id='"+iq.nodeTree.getAttribute('id')+"'>"+
+                                            "<request xmlns='urn:xmpp:http:upload:0' "+
+                                                "filename='my-juliet.jpg' "+
+                                                "size='23456' "+
+                                                "content-type='image/jpeg'/>"+
+                                            "</iq>");
+
+                                        var stanza = Strophe.xmlHtmlNode(
+                                            "<iq from='upload.montague.tld'"+
+                                            "    id='"+iq.nodeTree.getAttribute('id')+"'"+
+                                            "    to='dummy@localhost/resource'"+
+                                            "    type='result'>"+
+                                            "<slot xmlns='urn:xmpp:http:upload:0'>"+
+                                            "    <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>"+
+                                            "    <header name='Authorization'>Basic Base64String==</header>"+
+                                            "    <header name='Cookie'>foo=bar; user=romeo</header>"+
+                                            "    </put>"+
+                                            "    <get url='https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg' />"+
+                                            "</slot>"+
+                                            "</iq>").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(
+                                                "<message from='dummy@localhost/resource' "+
+                                                    "to='irini.vlastuin@localhost' "+
+                                                    "type='chat' "+
+                                                    "id='"+sent_stanza.nodeTree.getAttribute('id')+"' xmlns='jabber:client'>"+
+                                                        "<body>https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg</body>"+
+                                                        "<active xmlns='http://jabber.org/protocol/chatstates'/>"+
+                                                        "<x xmlns='jabber:x:oob'>"+
+                                                            "<url>https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg</url>"+
+                                                        "</x>"+
+                                                "</message>");
+                                            done();
+                                        });
+                                    });
+                                });
+                            });
+                        });
+                    }));
+                });
             });
         });
     });

+ 1 - 0
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",

+ 120 - 82
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));
                 },
-                
-                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();
-                                }    
-                            }
-                            else {
-                                alert(__('Could not upload File please try again.'));
-                            }
+
+                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."));
                         }
-                    };
-                
-                    xmlhttp.open('PUT', url, true);
-                    xmlhttp.setRequestHeader("Content-type", contentType);
-                    xmlhttp.send(file);
+                        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));
+                    });
                 },
 
-                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);
-                    }
+                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);
+                    });
                 },
                 
-                failedRequestSlotCB (stanza, cb) {
-                    alert(__('Could not upload File please try again.'));
+                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();
+                                }
+                            }
+                        };
+                        xhr.onerror = function () {
+                            reject(xhr.responseText);
+                        };
+                        xhr.open('PUT', url, true);
+                        xhr.setRequestHeader("Content-type", 'application/octet-stream');
+                        xhr.send(file);
+                    });
                 },
 
                 getMessageBody (message) {

+ 5 - 56
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) {

+ 16 - 18
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'
                 }
             }
         }

+ 1 - 1
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);

+ 1 - 1
src/templates/help_message.html

@@ -1 +1 @@
-<div class="message chat-{{{o.type}}}" data-isodate="{{{o.isodate}}}">{{o.message}}</div>
+<div class="message chat-info {[ if (o.type !== 'info') { ]} chat-{{{o.type}}} {[ } ]}" data-isodate="{{{o.isodate}}}">{{o.message}}</div>

+ 2 - 1
src/templates/toolbar.html

@@ -1,5 +1,6 @@
 {[ if (o.use_emoji)  { ]}
-<li class="toggle-toolbar-menu toggle-smiley fa fa-smile-o dropup">
+<li class="toggle-toolbar-menu toggle-smiley dropup">
+    <a class="btn toggle-smiley fa fa-smile-o" title="{{{o.tooltip_insert_smiley}}}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></a> 
     <div class="emoji-picker dropdown-menu toolbar-menu"></div>
 </li>
 {[ } ]}

+ 1 - 1
src/templates/toolbar_fileupload.html

@@ -1,4 +1,4 @@
-<input type="file" class="fileupload" style="display:none"/>
+<input type="file" class="fileupload" multiple style="display:none"/>
 <li class="upload-file">
     <a class="fa fa-paperclip" title="{{{o.tooltip_upload_file}}}"></a>
 </li>