ソースを参照

Move methods from chatbox view to message view

Specifically the methods related to requesting an upload slot and uploading a file.
Also show a progress indicator while a file is being uploaded.

Updates #161
JC Brand 7 年 前
コミット
c01e9f8265

+ 0 - 11
docs/source/configuration.rst

@@ -1214,17 +1214,6 @@ the operating system or browser (which might not support emoji).
 
 See also `emojione_image_path`_.
 
-
-show_message_load_animation
----------------------------
-* Default: ``false``
-
-Determines whether a CSS3 background-color fade-out animation is shown when messages
-appear in chats.
-
-Set to ``false`` by default since this option causes performance issues on Firefox.
-
-
 show_only_online_users
 ----------------------
 

+ 2 - 1
spec/chatbox.js

@@ -705,7 +705,7 @@
                             expect(chatbox.messages.length).toEqual(1);
                             var msg_obj = chatbox.messages.models[0];
                             expect(msg_obj.get('message')).toEqual(message);
-                            expect(msg_obj.get('fullname')).toEqual(sender_jid);
+                            expect(msg_obj.get('fullname')).toEqual(undefined);
                             expect(msg_obj.get('sender')).toEqual('them');
                             expect(msg_obj.get('delayed')).toEqual(false);
                             // Now check that the message appears inside the chatbox in the DOM
@@ -714,6 +714,7 @@
                             expect(msg_txt).toEqual(message);
                             var sender_txt = $chat_content.find('span.chat-msg-them').text();
                             expect(sender_txt.match(/^[0-9][0-9]:[0-9][0-9] /)).toBeTruthy();
+                            expect(sender_txt.indexOf('max.frankfurter@localhost')).not.toBe(-1);
                             done();
                         }));
                     });

+ 5 - 4
spec/chatroom.js

@@ -862,10 +862,10 @@
                     var message = '/me is tired';
                     var nick = mock.chatroom_names[0],
                         msg = $msg({
-                            from: 'lounge@localhost/'+nick,
-                            id: (new Date()).getTime(),
-                            to: 'dummy@localhost',
-                            type: 'groupchat'
+                            'from': 'lounge@localhost/'+nick,
+                            'id': (new Date()).getTime(),
+                            'to': 'dummy@localhost',
+                            'type': 'groupchat'
                         }).c('body').t(message).tree();
                     view.model.onMessage(msg);
                     expect(_.includes($(view.el).find('.chat-msg-author').text(), '**Dyon van de Wege')).toBeTruthy();
@@ -3306,6 +3306,7 @@
                                     to: 'dummy@localhost',
                                     type: 'groupchat'
                                 }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
+
                             view.model.onMessage(msg);
 
                             // Check that the notification appears inside the chatbox in the DOM

+ 2 - 0
spec/controlbox.js

@@ -241,6 +241,7 @@
                     xhr.onload();
                 }
             };
+            const XMLHttpRequestBackup = window.XMLHttpRequest;
             window.XMLHttpRequest = jasmine.createSpy('XMLHttpRequest');
             XMLHttpRequest.and.callFake(function () {
                 return xhr;
@@ -288,6 +289,7 @@
                 "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
                     "<query xmlns='jabber:iq:roster'><item jid='marty@mcfly.net' name='Marty McFly'/></query>"+
                 "</iq>");
+                window.XMLHttpRequest = XMLHttpRequestBackup;
                 done();
             });
         }));

+ 32 - 9
spec/http-file-upload.js

@@ -204,12 +204,14 @@
                 }));
 
                 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 send_backup = XMLHttpRequest.prototype.send;
                             var IQ_stanzas = _converse.connection.IQ_stanzas;
 
                             test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items').then(function () {
@@ -224,11 +226,11 @@
                                         'lastModifiedDate': "",
                                         'name': "my-juliet.jpg"
                                     };
-                                    view.model.sendFile(file);
+                                    view.model.sendFiles([file]);
                                     return test_utils.waitUntil(function () {
                                         return _.filter(IQ_stanzas, function (iq) {
                                             return iq.nodeTree.querySelector('iq[to="upload.montague.tld"] request');
-                                        });
+                                        }).length > 0;
                                     }).then(function () {
                                         var iq = IQ_stanzas.pop();
                                         expect(iq.toLocaleString()).toBe(
@@ -243,6 +245,9 @@
                                                 "content-type='image/jpeg'/>"+
                                             "</iq>");
 
+                                        var base_url = document.URL.split(window.location.pathname)[0];
+                                        var message = base_url+"/logo/conversejs-filled.svg";
+
                                         var stanza = Strophe.xmlHtmlNode(
                                             "<iq from='upload.montague.tld'"+
                                             "    id='"+iq.nodeTree.getAttribute('id')+"'"+
@@ -253,11 +258,21 @@
                                             "    <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' />"+
+                                            "    <get url='"+message+"' />"+
                                             "</slot>"+
                                             "</iq>").firstElementChild;
-                                        spyOn(view.model, 'uploadFile').and.callFake(function () {
-                                            return new window.Promise((resolve, reject) => { resolve(); });
+
+                                        spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
+                                            const message = view.model.messages.at(0);
+                                            expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
+                                            message.set('progress', 0.5);
+                                            expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0.5');
+                                            message.set('progress', 1);
+                                            expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('1');
+                                            message.save({
+                                                'upload': _converse.SUCCESS,
+                                                'message': message.get('get')
+                                            });
                                         });
                                         var sent_stanza;
                                         spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
@@ -267,19 +282,27 @@
 
                                         return test_utils.waitUntil(function () {
                                             return sent_stanza;
-                                        }).then(function () {
-                                            expect(view.model.uploadFile).toHaveBeenCalled();
+                                        }, 1000).then(function () {
                                             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>"+
+                                                        "<body>"+message+"</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>"+
+                                                            "<url>"+message+"</url>"+
                                                         "</x>"+
                                                 "</message>");
+                                            return test_utils.waitUntil(function () {
+                                                return view.el.querySelector('.chat-image');
+                                            }, 1000);
+                                        }).then(function () {
+                                            // Check that the image renders
+                                            expect(view.el.querySelector('.chat-message .chat-msg-content').innerHTML).toEqual(
+                                                '<a target="_blank" rel="noopener" href="http://localhost:8000/logo/conversejs-filled.svg">'+
+                                                    '<img class="chat-image" src="http://localhost:8000/logo/conversejs-filled.svg"></a>')
+                                            XMLHttpRequest.prototype.send = send_backup;
                                             done();
                                         });
                                     });

+ 150 - 90
src/converse-chatboxes.js

@@ -7,15 +7,19 @@
 (function (root, factory) {
     define([
         "converse-core",
+        "emojione",
         "tpl!chatboxes",
         "backbone.overview"
     ], factory);
-}(this, function (converse, tpl_chatboxes) {
+}(this, function (converse, emojione, tpl_chatboxes) {
     "use strict";
 
     const { $msg, Backbone, Promise, Strophe, b64_sha1, moment, utils, _ } = converse.env;
+    const u = converse.env.utils;
+
     Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob');
 
+
     converse.plugins.add('converse-chatboxes', {
 
         overrides: {
@@ -74,10 +78,100 @@
 
 
             _converse.Message = Backbone.Model.extend({
-                defaults(){
+
+                defaults () {
                     return {
-                        msgid: _converse.connection.getUniqueId()
+                        'msgid': _converse.connection.getUniqueId(),
+                        'time': moment().format()
+                    };
+                },
+
+                initialize () {
+                    if (this.get('file')) {
+                        this.on('change:put', this.uploadFile, this);
+
+                        if (!_.includes([_converse.SUCCESS, _converse.FAILURE], this.get('upload'))) {
+                            this.getRequestSlotURL();
+                        }
+                    }
+                },
+
+                sendSlotRequestStanza () {
+                    /* Send out an IQ stanza to request a file upload slot.
+                     *
+                     * https://xmpp.org/extensions/xep-0363.html#request
+                     */
+                    const file = this.get('file');
+                    return new Promise((resolve, reject) => {
+                        const iq = converse.env.$iq({
+                            'from': _converse.jid,
+                            'to': this.get('slot_request_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);
+                    });
+                },
+
+                getRequestSlotURL () {
+                    this.sendSlotRequestStanza().then((stanza) => {
+                        const slot = stanza.querySelector('slot');
+                        if (slot) {
+                            this.save({
+                                'get':  slot.querySelector('get').getAttribute('url'),
+                                'put': slot.querySelector('put').getAttribute('url'),
+                            });
+                        } else {
+                            return this.save({
+                                'type': 'error',
+                                'message': __("Sorry, could not determine upload URL.")
+                            });
+                        }
+                    }).catch((e) => {
+                        _converse.log(e, Strophe.LogLevel.ERROR);
+                        return this.save({
+                            'type': 'error',
+                            'message': __("Sorry, could not determine upload URL.")
+                        });
+                    });
+                },
+
+                uploadFile () {
+                    const xhr = new XMLHttpRequest();
+                    xhr.onreadystatechange = () => {
+                        if (xhr.readyState === XMLHttpRequest.DONE) {
+                            _converse.log("Status: " + xhr.status, Strophe.LogLevel.INFO);
+                            if (xhr.status === 200 || xhr.status === 201) {
+                                this.save({
+                                    'upload': _converse.SUCCESS,
+                                    'message': this.get('get')
+                                });
+                            } else {
+                                this.save({
+                                    'upload': _converse.FAILURE,
+                                    'message': __('Sorry, could not succesfully upload your file')
+                                });
+                            }
+                        }
+                    };
+                    xhr.upload.addEventListener("progress", (evt) => {
+                        if (evt.lengthComputable) {
+                            this.set('progress', evt.loaded / evt.total);
+                        }
+                    }, false);
+                    xhr.onerror = () => {
+                        this.save({
+                            'upload': _converse.FAILURE,
+                            'message': __('Sorry, could not succesfully upload your file')
+                        });
                     };
+                    xhr.open('PUT', this.get('put'), true);
+                    xhr.setRequestHeader("Content-type", 'application/octet-stream');
+                    xhr.send(this.get('file'));
                 }
             });
 
@@ -97,6 +191,7 @@
                     'num_unread': 0,
                     'show_avatar': true,
                     'type': 'chatbox',
+                    'message_type': 'chat',
                     'url': ''
                 },
 
@@ -106,6 +201,12 @@
                         b64_sha1(`converse.messages${this.get('jid')}${_converse.bare_jid}`));
                     this.messages.chatbox = this;
 
+                    this.messages.on('change:upload', (message) => {
+                        if (message.get('upload') === _converse.SUCCESS) {
+                            this.sendMessageStanza(message);
+                        }
+                    });
+
                     this.save({
                         // The chat_state will be set to ACTIVE once the chat box is opened
                         // and we listen for change:chat_state, so shouldn't set it to ACTIVE here.
@@ -125,7 +226,7 @@
                     const stanza = $msg({
                             'from': _converse.connection.jid,
                             'to': this.get('jid'),
-                            'type': 'chat',
+                            'type': this.get('message_type'),
                             'id': message.get('msgid')
                         }).c('body').t(message.get('message')).up()
                           .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up();
@@ -149,17 +250,34 @@
                     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())
+                            $msg({
+                                'to': _converse.bare_jid,
+                                'type': this.get('message_type'),
+                                'id': message.get('msgid')
+                            }).c('forwarded', {'xmlns': Strophe.NS.FORWARD})
+                                .c('delay', {
+                                        'xmns': Strophe.NS.DELAY,
+                                        'stamp': moment().format()
+                                }).up()
+                              .cnode(messageStanza.tree())
                         );
                     }
                 },
 
+                getOutgoingMessageAttributes (text, spoiler_hint) {
+                    const fullname = _converse.xmppstatus.get('fullname'),
+                        is_spoiler = this.get('composing_spoiler');
+
+                    return {
+                        'fullname': _.isEmpty(fullname) ? _converse.bare_jid : fullname,
+                        'sender': 'me',
+                        'time': moment().format(),
+                        'message': text ? u.httpToGeoUri(emojione.shortnameToUnicode(text), _converse) : undefined,
+                        'is_spoiler': is_spoiler,
+                        'spoiler_hint': is_spoiler ? spoiler_hint : undefined
+                    };
+                },
+
                 sendMessage (attrs) {
                     /* Responsible for sending off a text message.
                      *
@@ -169,83 +287,25 @@
                     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) {
+                sendFiles (files) {
                     _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"));
+                        const slot_request_url = _.get(result.pop(), 'id');
+                        if (!slot_request_url) {
+                            const err_msg = __("Sorry, looks like file upload is not supported by your server.");
+                            return this.trigger('showHelpMessages', [err_msg], 'error');
                         }
-                        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) {
-                    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);
-                    });
+                        _.each(files, (file) => {
+                            this.messages.create(
+                                _.extend(
+                                    this.getOutgoingMessageAttributes(), {
+                                    'file': file,
+                                    'progress': 0,
+                                    'slot_request_url': slot_request_url,
+                                    'type': this.get('message_type'),
+                                })
+                            );
+                        });
+                    }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                 },
 
                 getMessageBody (message) {
@@ -255,7 +315,7 @@
                             _.propertyOf(message.querySelector('body'))('textContent');
                 },
 
-                getMessageAttributes (message, delay, original_stanza) {
+                getMessageAttributesFromStanza (message, delay, original_stanza) {
                     /* Parses a passed in message stanza and returns an object
                      * of attributes.
                      *
@@ -292,10 +352,10 @@
                     let sender, fullname;
                     if ((is_groupchat && from === this.get('nick')) || (!is_groupchat && from === _converse.bare_jid)) {
                         sender = 'me';
-                        fullname = _converse.xmppstatus.get('fullname') || from;
+                        fullname = _converse.xmppstatus.get('fullname');
                     } else {
                         sender = 'them';
-                        fullname = this.get('fullname') || from;
+                        fullname = this.get('fullname');
                     }
                     const spoiler = message.querySelector(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`);
                     const attrs = {
@@ -320,7 +380,7 @@
                     /* Create a Backbone.Message object inside this chat box
                      * based on the identified message stanza.
                      */
-                    return this.messages.create(this.getMessageAttributes.apply(this, arguments));
+                    return this.messages.create(this.getMessageAttributesFromStanza.apply(this, arguments));
                 },
 
                 newMessageWillBeHidden () {

+ 8 - 27
src/converse-chatview.js

@@ -105,7 +105,6 @@
                 'chatview_avatar_height': 32,
                 'chatview_avatar_width': 32,
                 'show_toolbar': true,
-                'show_message_load_animation': false,
                 'time_format': 'HH:mm',
                 'visible_toolbar_buttons': {
                     'call': false,
@@ -613,24 +612,25 @@
                 showChatStateNotification (message) {
                     /* Support for XEP-0085, Chat State Notifications */
                     let text;
-                    const from = message.get('from');
-                    const data = `data-csn=${from}`;
+                    const from = message.get('from'),
+                          username = message.get('fullname') || from,
+                          data = `data-csn=${from}`;
                     this.clearChatStateNotification(from);
 
                     if (message.get('chat_state') === _converse.COMPOSING) {
                         if (message.get('sender') === 'me') {
                             text = __('Typing from another device');
                         } else {
-                            text = message.get('fullname')+' '+__('is typing');
+                            text = username +' '+__('is typing');
                         }
                     } else if (message.get('chat_state') === _converse.PAUSED) {
                         if (message.get('sender') === 'me') {
                             text = __('Stopped typing on the other device');
                         } else {
-                            text = message.get('fullname')+' '+__('has stopped typing');
+                            text = username +' '+__('has stopped typing');
                         }
                     } else if (message.get('chat_state') === _converse.GONE) {
-                        text = message.get('fullname')+' '+__('has gone away');
+                        text = username +' '+__('has gone away');
                     } else {
                         return;
                     }
@@ -707,7 +707,7 @@
                         if (message.get('chat_state')) {
                             this.showChatStateNotification(message);
                         }
-                        if (message.get('message')) {
+                        if (message.get('file') || message.get('message')) {
                             this.handleTextMessage(message);
                         }
                     }
@@ -755,29 +755,10 @@
                     if (this.parseMessageForCommands(text)) {
                         return;
                     }
-                    const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint);
+                    const attrs = this.model.getOutgoingMessageAttributes(text, spoiler_hint);
                     this.model.sendMessage(attrs);
                 },
 
-                getOutgoingMessageAttributes (text, spoiler_hint) {
-                    /* Overridable method which returns the attributes to be
-                     * passed to Backbone.Message's constructor.
-                     */
-                    const fullname = _converse.xmppstatus.get('fullname'),
-                        is_spoiler = this.model.get('composing_spoiler'),
-                        attrs = {
-                            'fullname': _.isEmpty(fullname) ? _converse.bare_jid : fullname,
-                            'sender': 'me',
-                            'time': moment().format(),
-                            'message': u.httpToGeoUri(emojione.shortnameToUnicode(text), _converse),
-                            'is_spoiler': is_spoiler
-                        };
-                    if (is_spoiler) {
-                        attrs.spoiler_hint = spoiler_hint;
-                    }
-                    return attrs;
-                },
-
                 sendChatState () {
                     /* Sends a message with the status of the user in this chat session
                      * as taken from the 'chat_state' attribute of the chat box.

ファイルの差分が大きいため隠しています
+ 3 - 0
src/converse-core.js


+ 2 - 2
src/converse-mam.js

@@ -128,8 +128,8 @@
             //
             // New functions which don't exist yet can also be added.
             ChatBox: {
-                getMessageAttributes (message, delay, original_stanza) {
-                    const attrs = this.__super__.getMessageAttributes.apply(this, arguments);
+                getMessageAttributesFromStanza (message, delay, original_stanza) {
+                    const attrs = this.__super__.getMessageAttributesFromStanza.apply(this, arguments);
                     const archive_id = getMessageArchiveID(original_stanza);
                     if (archive_id) {
                         attrs.archive_id = archive_id;

+ 75 - 47
src/converse-message-view.js

@@ -10,16 +10,18 @@
         "xss",
         "emojione",
         "tpl!action",
+        "tpl!file",
         "tpl!message",
         "tpl!spoiler_message"
     ], factory);
 }(this, function (
-            converse,
-            xss,
-            emojione,
-            tpl_action,
-            tpl_message,
-            tpl_spoiler_message
+        converse,
+        xss,
+        emojione,
+        tpl_action,
+        tpl_file,
+        tpl_message,
+        tpl_spoiler_message
     ) {
     "use strict";
     const { Backbone, _, moment } = converse.env;
@@ -38,60 +40,66 @@
             _converse.MessageView = Backbone.NativeView.extend({
 
                 initialize () {
-                    this.model.collection.chatbox.on('change:fullname', this.render, this);
+                    const chatbox = this.model.collection.chatbox;
+                    chatbox.on('change:fullname', (chatbox) => this.model.save('fullname', chatbox.get('fullname')));
+
+                    this.model.on('change:fullname', this.render, this);
+                    this.model.on('change:progress', this.renderFileUploadProgresBar, this);
+                    this.model.on('change:type', this.render, this);
+                    this.model.on('change:upload', this.render, this);
                     this.render();
                 },
 
                 render () {
-                    const chatbox = this.model.collection.chatbox;
-
-                    let text = this.model.get('message'),
-                        fullname = chatbox.get('fullname') || chatbox.get('jid'),
-                        template, username;
+                    if (this.model.get('file') && !this.model.get('message')) {
+                        return this.renderFileUploadProgresBar();
+                    }
+                    let template, username,
+                        text = this.model.get('message');
 
-                    const match = text.match(/^\/(.*?)(?: (.*))?$/);
-                    if ((match) && (match[1] === 'me')) {
-                        text = text.replace(/^\/me/, '');
-                        template = tpl_action;
-                        if (this.model.get('sender') === 'me') {
-                            fullname = _converse.xmppstatus.get('fullname') || this.model.get('fullname');
-                            username = _.isNil(fullname)? _converse.bare_jid: fullname;
-                        } else {
-                            username = this.model.get('fullname');
-                        }
+                    // TODO: store proper username on the message itself
+                    if (this.isMeCommand()) {
+                        const arr = this.getValuesForMeCommand();
+                        template = arr[0];
+                        username = arr[1];
+                        text = arr[2];
                     } else {
-                        username = this.model.get('sender') === 'me' && __('me') || fullname;
+                        const fullname = _converse.xmppstatus.get('fullname') || this.model.get('fullname');
+                        username = this.model.get('sender') === 'me' && __('me') || fullname || this.model.get('from');
                         template = this.model.get('is_spoiler') ? tpl_spoiler_message : tpl_message;
                     }
-                    text = u.geoUriToHttp(text, _converse);
-
-                    const msg_time = moment(this.model.get('time')) || moment;
+                    const moment_time = moment(this.model.get('time'));
                     const msg = u.stringToElement(template(
                         _.extend(this.model.toJSON(), {
-                            'time': msg_time.format(_converse.time_format),
-                            'isodate': msg_time.format(),
+                            'pretty_time': moment_time.format(_converse.time_format),
+                            'time': moment_time.format(),
                             'username': username,
                             'extra_classes': this.getExtraMessageClasses(),
                             'label_show': __('Show hidden message')
                         })
                     ));
-                    if (_converse.show_message_load_animation) {
-                        window.setTimeout(_.partial(u.removeClass, 'onload', msg), 2000);
-                    }
                     const msg_content = msg.querySelector('.chat-msg-content');
-                    msg_content.innerHTML = u.addEmoji(
-                        _converse, emojione, u.addHyperlinks(xss.filterXSS(text, {'whiteList': {}}))
-                    );
+                    text = xss.filterXSS(text, {'whiteList': {}});
+                    msg_content.innerHTML = _.flow(
+                        _.partial(u.geoUriToHttp, _, _converse.geouri_replacement),
+                        _.partial(u.addHyperlinks, _),
+                        _.partial(u.addEmoji, _converse, emojione, _),
+                        u.renderMovieURLs,
+                        u.renderAudioURLs
+                    )(text);
 
-                    if (msg_content.textContent.endsWith('mp4')) {
-                        msg_content.innerHTML = u.renderMovieURLs(msg_content);
-                    } else if (msg_content.textContent.endsWith('mp3')) {
-                        msg_content.innerHTML = u.renderAudioURLs(msg_content);
-                    } else {
-                        u.renderImageURLs(msg_content).then(() => {
-                            this.model.collection.trigger('rendered');
-                        });
+                    u.renderImageURLs(msg_content).then(() => {
+                        this.model.collection.trigger('rendered');
+                    });
+                    if (!_.isNil(this.el.parentElement)) {
+                        this.el.parentElement.replaceChild(msg, this.el);
                     }
+                    this.setElement(msg);
+                    return this.el;
+                },
+
+                renderFileUploadProgresBar () {
+                    const msg = u.stringToElement(tpl_file(this.model.toJSON()));
                     if (!_.isNil(this.el.parentElement)) {
                         this.el.parentElement.replaceChild(msg, this.el);
                     }
@@ -99,13 +107,33 @@
                     return this.el;
                 },
 
-                getExtraMessageClasses () {
-                    let extra_classes;
-                    if (_converse.show_message_load_animation) {
-                        extra_classes =  'onload ' + (this.model.get('delayed') && 'delayed' || '');
+                isMeCommand () {
+                    const match = this.model.get('message').match(/^\/(.*?)(?: (.*))?$/);
+                    return match && match[1] === 'me';
+                },
+
+                getValuesForMeCommand() {
+                    let username, text;
+                    const match = this.model.get('message').match(/^\/(.*?)(?: (.*))?$/);
+                    if (match && match[1] === 'me') {
+                        text = this.model.get('message').replace(/^\/me/, '');
+                    }
+                    if (this.model.get('sender') === 'me') {
+                        const fullname = _converse.xmppstatus.get('fullname') || this.model.get('fullname');
+                        username = _.isNil(fullname) ? _converse.bare_jid : fullname;
                     } else {
-                        extra_classes = this.model.get('delayed') && 'delayed' || '';
+                        username = this.model.get('fullname') || this.model.get('from');
                     }
+                    return [tpl_action, username, text]
+                },
+
+                processMessageText () {
+                    var text = this.get('message');
+                    text = u.geoUriToHttp(text, _converse.geouri_replacement);
+                },
+
+                getExtraMessageClasses () {
+                    let extra_classes = this.model.get('delayed') && 'delayed' || '';
                     if (this.model.get('type') === 'groupchat' && this.model.get('sender') === 'them') {
                         if (this.model.collection.chatbox.isUserMentioned(this.model.get('message'))) {
                             // Add special class to mark groupchat messages

+ 1 - 0
src/converse-muc.js

@@ -182,6 +182,7 @@
                           'features_fetched': false,
                           'roomconfig': {},
                           'type': converse.CHATROOMS_TYPE,
+                          'message_type': 'groupchat'
                         }
                     );
                 },

+ 3 - 3
src/templates/action.html

@@ -1,4 +1,4 @@
-<div class="message chat-message {{{o.extra_classes}}}" data-isodate="{{{o.isodate}}}">
-    <span class="chat-msg-author chat-msg-{{{o.sender}}}">{{{o.time}}} **{{{o.username}}}&nbsp;</span>
-    <span class="chat-msg-content chat-action"><!-- message gets added here via renderMessage --></span>
+<div class="message chat-message chat-action {{{o.extra_classes}}}" data-isodate="{{{o.time}}}">
+    <span class="chat-msg-author chat-msg-{{{o.sender}}}">{{{o.pretty_time}}} **{{{o.username}}}</span>
+    <span class="chat-msg-content"><!-- message gets added here via renderMessage --></span>
 </div>

+ 3 - 0
src/templates/file.html

@@ -0,0 +1,3 @@
+<div class="message" data-isodate="{{{o.time}}}" data-msgid="{{{o.msgid}}}">
+    <progress value="{{{o.progress}}}"/>
+</div>

+ 2 - 2
src/templates/message.html

@@ -1,4 +1,4 @@
-<div class="message chat-message {{{o.extra_classes}}}" data-isodate="{{{o.isodate}}}" data-msgid="{{{o.msgid}}}">
-    <span class="chat-msg-author chat-msg-{{{o.sender}}}">{{{o.time}}} {{{o.username}}}:&nbsp;</span>
+<div class="message chat-message {{{o.extra_classes}}}" data-isodate="{{{o.time}}}" data-msgid="{{{o.msgid}}}">
+    <span class="chat-msg-author chat-msg-{{{o.sender}}}">{{{o.pretty_time}}} {{{o.username}}}:&nbsp;</span>
     <span class="chat-msg-content"><!-- message gets added here via renderMessage --></span>
 </div>

+ 2 - 2
src/templates/spoiler_message.html

@@ -1,5 +1,5 @@
-<div class="message chat-message {{{o.extra_classes}}}" data-isodate="{{{o.isodate}}}" data-msgid="{{{o.msgid}}}">
-    <span class="chat-msg-author chat-msg-{{{o.sender}}}">{{{o.time}}} {{{o.username}}}:&nbsp;</span>
+<div class="message chat-message {{{o.extra_classes}}}" data-isodate="{{{o.time}}}" data-msgid="{{{o.msgid}}}">
+    <span class="chat-msg-author chat-msg-{{{o.sender}}}">{{{o.pretty_time}}} {{{o.username}}}:&nbsp;</span>
     <div class="spoiler-hint">{{{o.spoiler_hint}}}</div>
     <a class="icon-eye toggle-spoiler" data-toggle-state="closed" href="#">{{{o.label_show}}}</a>
     <div class="chat-msg-content spoiler collapsed"><!-- message gets added here via renderMessage --></div>

+ 12 - 6
src/utils/core.js

@@ -213,12 +213,18 @@
             ))
     };
 
-    u.renderMovieURLs = function (obj) {
-        return "<video controls><source src=\"" + obj.textContent + "\" type=\"video/mp4\"></video>";
+    u.renderMovieURLs = function (text) {
+        if (text.endsWith('mp4')) {
+            return "<video controls><source src=\"" + text + "\" type=\"video/mp4\"></video>";
+        }
+        return text;
     };
 
-    u.renderAudioURLs = function (obj) {
-        return "<audio controls><source src=\"" + obj.textContent + "\" type=\"audio/mpeg\"></audio>";
+    u.renderAudioURLs = function (text) {
+        if (text.endsWith('mp3')) {
+            return "<audio controls><source src=\"" + text+ "\" type=\"audio/mpeg\"></audio>";
+        }
+        return text;
     };
 
     u.slideInAllElements = function (elements, duration=300) {
@@ -714,9 +720,9 @@
         el.dispatchEvent(evt);
     };
 
-    u.geoUriToHttp = function(text, _converse) {
+    u.geoUriToHttp = function(text, geouri_replacement) {
         const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
-        return text.replace(regex, _converse.geouri_replacement);
+        return text.replace(regex, geouri_replacement);
     };
 
     u.httpToGeoUri = function(text, _converse) {

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません