Преглед на файлове

Honor the filesize restrictions of the XMPP server

updates #161
JC Brand преди 7 години
родител
ревизия
133df99aec
променени са 8 файла, в които са добавени 268 реда и са изтрити 76 реда
  1. 6 0
      package-lock.json
  2. 3 2
      package.json
  3. 31 8
      spec/chatbox.js
  4. 131 2
      spec/http-file-upload.js
  5. 1 0
      src/config.js
  6. 29 13
      src/converse-chatboxes.js
  7. 46 46
      src/converse-chatview.js
  8. 21 5
      src/converse-message-view.js

+ 6 - 0
package-lock.json

@@ -1929,6 +1929,12 @@
       "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
       "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
       "dev": true
       "dev": true
     },
     },
+    "filesize": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
+      "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
+      "dev": true
+    },
     "fill-range": {
     "fill-range": {
       "version": "2.2.3",
       "version": "2.2.3",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz",

+ 3 - 2
package.json

@@ -37,16 +37,17 @@
     "backbone": "1.3.3",
     "backbone": "1.3.3",
     "backbone.browserStorage": "0.0.3",
     "backbone.browserStorage": "0.0.3",
     "backbone.nativeview": "^0.3.3",
     "backbone.nativeview": "^0.3.3",
-    "bootstrap": "^4.0.0",
-    "bootstrap.native": "^2.0.21",
     "backbone.overview": "1.0.2",
     "backbone.overview": "1.0.2",
     "backbone.vdomview": "1.0.1",
     "backbone.vdomview": "1.0.1",
+    "bootstrap": "^4.0.0",
+    "bootstrap.native": "^2.0.21",
     "bourbon": "^4.3.2",
     "bourbon": "^4.3.2",
     "clean-css-cli": "^4.0.10",
     "clean-css-cli": "^4.0.10",
     "emojione": "^3.0.3",
     "emojione": "^3.0.3",
     "es6-promise": "^4.1.0",
     "es6-promise": "^4.1.0",
     "eslint": "4.19.0",
     "eslint": "4.19.0",
     "eslint-plugin-lodash": "^2.3.3",
     "eslint-plugin-lodash": "^2.3.3",
+    "filesize": "^3.6.1",
     "font-awesome": "^4.7.0",
     "font-awesome": "^4.7.0",
     "http-server": "^0.10.0",
     "http-server": "^0.10.0",
     "install": "^0.9.5",
     "install": "^0.9.5",

+ 31 - 8
spec/chatbox.js

@@ -765,7 +765,7 @@
                             // We send another message, for which an error will
                             // We send another message, for which an error will
                             // not be received, to test that errors appear
                             // not be received, to test that errors appear
                             // after the relevant message.
                             // after the relevant message.
-                            msg_text = 'This message will be sent, and not receive an error';
+                            msg_text = 'This message will be sent, and also receive an error';
                             message = view.model.messages.create({
                             message = view.model.messages.create({
                                 'msgid': '6fcdeee3-000f-4ce8-a17e-9ce28f0ae104',
                                 'msgid': '6fcdeee3-000f-4ce8-a17e-9ce28f0ae104',
                                 'fullname': fullname,
                                 'fullname': fullname,
@@ -802,12 +802,6 @@
                             _converse.connection._dataRecv(test_utils.createRequest(stanza));
                             _converse.connection._dataRecv(test_utils.createRequest(stanza));
                             expect($chat_content.find('.chat-error').text()).toEqual(error_txt);
                             expect($chat_content.find('.chat-error').text()).toEqual(error_txt);
 
 
-                            /* Incoming error messages that are not tied to a
-                             * certain show message (via the msgid attribute),
-                             * are not shown at all. The reason for this is
-                             * that we may get error messages for chat state
-                             * notifications as well.
-                             */
                             stanza = $msg({
                             stanza = $msg({
                                     'to': _converse.connection.jid,
                                     'to': _converse.connection.jid,
                                     'type':'error',
                                     'type':'error',
@@ -819,7 +813,36 @@
                                 .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
                                 .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
                                     .t('Server-to-server connection failed: Connecting failed: connection timeout');
                                     .t('Server-to-server connection failed: Connecting failed: connection timeout');
                             _converse.connection._dataRecv(test_utils.createRequest(stanza));
                             _converse.connection._dataRecv(test_utils.createRequest(stanza));
-                            expect($chat_content.find('.chat-error').length).toEqual(1);
+                            expect($chat_content.find('.chat-error').length).toEqual(2);
+
+                            // If the last message is already an error message,
+                            // then we don't render it another time.
+                            stanza = $msg({
+                                    'to': _converse.connection.jid,
+                                    'type':'error',
+                                    'id':'another-unused-id',
+                                    'from': sender_jid
+                                })
+                                .c('error', {'type': 'cancel'})
+                                .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
+                                .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
+                                    .t('Server-to-server connection failed: Connecting failed: connection timeout');
+                            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                            expect($chat_content.find('.chat-error').length).toEqual(2);
+
+                            // A different error message will however render
+                            stanza = $msg({
+                                    'to': _converse.connection.jid,
+                                    'type':'error',
+                                    'id':'another-id',
+                                    'from': sender_jid
+                                })
+                                .c('error', {'type': 'cancel'})
+                                .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
+                                .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
+                                    .t('Something else went wrong as well');
+                            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                            expect($chat_content.find('.chat-error').length).toEqual(3);
                             done();
                             done();
                         }));
                         }));
                     });
                     });

+ 131 - 2
spec/http-file-upload.js

@@ -204,9 +204,9 @@
                     done();
                     done();
                 }));
                 }));
 
 
-                describe("when clicked", function () {
+                describe("when clicked and a file chosen", function () {
 
 
-                    it("a file upload slot is requested", mock.initConverseWithAsync(function (done, _converse) {
+                    it("is uploaded and sent out", mock.initConverseWithAsync(function (done, _converse) {
                         test_utils.waitUntilDiscoConfirmed(
                         test_utils.waitUntilDiscoConfirmed(
                             _converse, _converse.domain,
                             _converse, _converse.domain,
                             [{'category': 'server', 'type':'IM'}],
                             [{'category': 'server', 'type':'IM'}],
@@ -311,6 +311,135 @@
                             });
                             });
                         });
                         });
                     }));
                     }));
+
+                    it("shows and error message if the file is too large", mock.initConverseWithAsync(function (done, _converse) {
+                        var IQ_stanzas = _converse.connection.IQ_stanzas;
+                        var IQ_ids =  _converse.connection.IQ_ids;
+                        var send_backup = XMLHttpRequest.prototype.send;
+
+                        test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []).then(function () {
+                            test_utils.waitUntil(function () {
+                                return _.filter(IQ_stanzas, function (iq) {
+                                    return iq.nodeTree.querySelector(
+                                        'iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                                }).length > 0;
+                            }, 300).then(function () {
+                                var stanza = _.filter(IQ_stanzas, function (iq) {
+                                    return iq.nodeTree.querySelector(
+                                        'iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                                })[0];
+                                var info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+
+                                stanza = $iq({
+                                    'type': 'result',
+                                    'from': 'localhost',
+                                    'to': 'dummy@localhost/resource',
+                                    'id': info_IQ_id
+                                }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
+                                    .c('identity', {
+                                        'category': 'server',
+                                        'type': 'im'}).up()
+                                    .c('feature', {
+                                        'var': 'http://jabber.org/protocol/disco#info'}).up()
+                                    .c('feature', {
+                                        'var': 'http://jabber.org/protocol/disco#items'});
+                                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                                _converse.api.disco.entities.get().then(function(entities) {
+                                    expect(entities.length).toBe(2);
+                                    expect(_.includes(entities.pluck('jid'), 'localhost')).toBe(true);
+                                    expect(_.includes(entities.pluck('jid'), 'dummy@localhost')).toBe(true);
+
+                                    expect(entities.get(_converse.domain).features.length).toBe(2);
+                                    expect(entities.get(_converse.domain).identities.length).toBe(1);
+
+                                    return test_utils.waitUntil(function () {
+                                        // Converse.js sees that the entity has a disco#items feature,
+                                        // so it will make a query for it.
+                                        return _.filter(IQ_stanzas, function (iq) {
+                                            return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
+                                        }).length > 0;
+                                    }, 300);
+                                });
+                            }).then(function () {
+                            var stanza = _.filter(IQ_stanzas, function (iq) {
+                                return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
+                            })[0];
+                            var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+                            stanza = $iq({
+                                'type': 'result',
+                                'from': 'localhost',
+                                'to': 'dummy@localhost/resource',
+                                'id': items_IQ_id
+                            }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
+                                .c('item', {
+                                    'jid': 'upload.localhost',
+                                    'name': 'HTTP File Upload'});
+                                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                                _converse.api.disco.entities.get().then(function (entities) {
+                                    expect(entities.length).toBe(2);
+                                    expect(entities.get('localhost').items.length).toBe(1);
+                                    return test_utils.waitUntil(function () {
+                                        // Converse.js sees that the entity has a disco#info feature,
+                                        // so it will make a query for it.
+                                        return _.filter(IQ_stanzas, function (iq) {
+                                            return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                                        }).length > 0;
+                                    }, 300);
+                                });
+                            }).then(function () {
+                                var stanza = _.filter(IQ_stanzas, function (iq) {
+                                    return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
+                                })[0];
+                                var IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
+                                expect(stanza.toLocaleString()).toBe(
+                                    "<iq from='dummy@localhost/resource' to='upload.localhost' type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                                        "<query xmlns='http://jabber.org/protocol/disco#info'/>"+
+                                    "</iq>");
+
+                                // Upload service responds and reports a maximum file size of 5MiB
+                                stanza = $iq({'type': 'result', 'to': 'dummy@localhost/resource', 'id': IQ_id, 'from': 'upload.localhost'})
+                                    .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
+                                        .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
+                                        .c('feature', {'var':'urn:xmpp:http:upload:0'}).up()
+                                        .c('x', {'type':'result', 'xmlns':'jabber:x:data'})
+                                            .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
+                                                .c('value').t('urn:xmpp:http:upload:0').up().up()
+                                            .c('field', {'var':'max-file-size'})
+                                                .c('value').t('5242880');
+                                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                                _converse.api.disco.entities.get().then(function (entities) {
+                                    expect(entities.get('localhost').items.get('upload.localhost').identities.where({'category': 'store'}).length).toBe(1);
+                                    _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then(
+                                        function (result) {
+                                            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': '5242881',
+                                                'lastModifiedDate': "",
+                                                'name': "my-juliet.jpg"
+                                            };
+                                            view.model.sendFiles([file]);
+                                            return test_utils.waitUntil(function () {
+                                                return view.el.querySelectorAll('.message').length;
+                                            }).then(function () {
+                                                const messages = view.el.querySelectorAll('.message.chat-error');
+                                                expect(messages.length).toBe(1);
+                                                expect(messages[0].textContent).toBe(
+                                                    'The size of your file, my-juliet.jpg, exceeds the maximum allowed by your server, which is 5 MB.');
+                                                done();
+                                            });
+                                        }
+                                    );
+                                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                            })
+                        });
+                    }));
                 });
                 });
             });
             });
         });
         });

+ 1 - 0
src/config.js

@@ -29,6 +29,7 @@ require.config({
         "emojione":                 "node_modules/emojione/lib/js/emojione",
         "emojione":                 "node_modules/emojione/lib/js/emojione",
         "es6-promise":              "node_modules/es6-promise/dist/es6-promise.auto",
         "es6-promise":              "node_modules/es6-promise/dist/es6-promise.auto",
         "eventemitter":             "node_modules/otr/build/dep/eventemitter",
         "eventemitter":             "node_modules/otr/build/dep/eventemitter",
+        "filesize":                 "node_modules/filesize/lib/filesize",
         "form-utils":               "src/utils/form",
         "form-utils":               "src/utils/form",
         "i18n":                     "src/i18n",
         "i18n":                     "src/i18n",
         "jed":                      "node_modules/jed/jed",
         "jed":                      "node_modules/jed/jed",

+ 29 - 13
src/converse-chatboxes.js

@@ -8,10 +8,11 @@
     define([
     define([
         "converse-core",
         "converse-core",
         "emojione",
         "emojione",
+        "filesize",
         "tpl!chatboxes",
         "tpl!chatboxes",
         "backbone.overview"
         "backbone.overview"
     ], factory);
     ], factory);
-}(this, function (converse, emojione, tpl_chatboxes) {
+}(this, function (converse, emojione, filesize, tpl_chatboxes) {
     "use strict";
     "use strict";
 
 
     const { $msg, Backbone, Promise, Strophe, b64_sha1, moment, utils, _ } = converse.env;
     const { $msg, Backbone, Promise, Strophe, b64_sha1, moment, utils, _ } = converse.env;
@@ -289,21 +290,36 @@
 
 
                 sendFiles (files) {
                 sendFiles (files) {
                     _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then((result) => {
                     _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then((result) => {
-                        const slot_request_url = _.get(result.pop(), 'id');
+                        const item = result.pop(),
+                              data = item.dataforms.where({'FORM_TYPE': {'value': Strophe.NS.HTTPUPLOAD, 'type': "hidden"}}).pop(),
+                              max_file_size = window.parseInt(_.get(data, 'attributes.max-file-size.value')),
+                              slot_request_url = _.get(item, 'id');
+
                         if (!slot_request_url) {
                         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.messages.create({
+                                'message': __("Sorry, looks like file upload is not supported by your server."),
+                                'type': 'error',
+                            });
+                            return;
                         }
                         }
                         _.each(files, (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'),
-                                })
-                            );
+                            if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) {
+                                return this.messages.create({
+                                    'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
+                                        file.name, filesize(max_file_size)),
+                                    'type': 'error',
+                                });
+                            } else {
+                                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));
                     }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                 },
                 },

+ 46 - 46
src/converse-chatview.js

@@ -529,33 +529,6 @@
                     }
                     }
                 },
                 },
 
 
-                showMessage (message) {
-                    /* Inserts a chat message into the content area of the chat box.
-                     * Will also insert a new day indicator if the message is on a
-                     * different day.
-                     *
-                     * The message to show may either be newer than the newest
-                     * message, or older than the oldest message.
-                     *
-                     * Parameters:
-                     *  (Backbone.Model) message: The message object
-                     */
-                    const view = new _converse.MessageView({'model': message}),
-                          current_msg_date = moment(message.get('time')) || moment,
-                          previous_msg_date = this.getLastMessageDate(current_msg_date),
-                          message_el = view.el;
-
-                    if (_.isNull(previous_msg_date)) {
-                        this.content.insertAdjacentElement('afterbegin', message_el);
-                    } else {
-                        const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date}"]:last`, this.content).pop();
-                        previous_msg_el.insertAdjacentElement('afterend', message_el);
-                    }
-                    this.insertDayIndicator(message_el);
-                    this.clearChatStateNotification(message.get('from'));
-                    this.setScrollPosition(message_el);
-                },
-
                 setScrollPosition (message_el) {
                 setScrollPosition (message_el) {
                     /* Given a newly inserted message, determine whether we
                     /* Given a newly inserted message, determine whether we
                      * should keep the scrollbar in place (so as to not scroll
                      * should keep the scrollbar in place (so as to not scroll
@@ -655,8 +628,50 @@
                     return !u.isVisible(this.el);
                     return !u.isVisible(this.el);
                 },
                 },
 
 
-                handleTextMessage (message) {
-                    this.showMessage(message);
+                insertMessage (view) {
+                    /* Given a view representing a message, insert it inot the
+                     * content area of the chat box.
+                     *
+                     * Parameters:
+                     *  (Backbone.View) message: The message Backbone.View
+                     */
+                    if (view.model.get('type') === 'error') {
+                        const previous_msg_el = this.content.querySelector(`[data-msgid="${view.model.get('msgid')}"]`);
+                        if (previous_msg_el) {
+                            return previous_msg_el.insertAdjacentElement('afterend', view.el);
+                        }
+                    }
+                    const current_msg_date = moment(view.model.get('time')) || moment,
+                            previous_msg_date = this.getLastMessageDate(current_msg_date);
+
+                    if (_.isNull(previous_msg_date)) {
+                        this.content.insertAdjacentElement('afterbegin', view.el);
+                    } else {
+                        const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date}"]:last`, this.content).pop();
+                        if (view.model.get('type') === 'error' &&
+                                u.hasClass('chat-error', previous_msg_el) &&
+                                previous_msg_el.textContent === view.model.get('message')) {
+                            // We don't show a duplicate error message
+                            return;
+                        }
+                        previous_msg_el.insertAdjacentElement('afterend', view.el);
+                    }
+                },
+
+                showMessage (message) {
+                    /* Inserts a chat message into the content area of the chat box.
+                     *
+                     * Will also insert a new day indicator if the message is on a
+                     * different day.
+                     *
+                     * Parameters:
+                     *  (Backbone.Model) message: The message object
+                     */
+                    const view = new _converse.MessageView({'model': message});
+                    this.insertMessage(view);
+                    this.insertDayIndicator(view.el);
+                    this.clearChatStateNotification(message.get('from'));
+                    this.setScrollPosition(view.el);
 
 
                     if (u.isNewMessage(message)) {
                     if (u.isNewMessage(message)) {
                         if (message.get('sender') === 'me') {
                         if (message.get('sender') === 'me') {
@@ -676,21 +691,6 @@
                     }
                     }
                 },
                 },
 
 
-                handleErrorMessage (message) {
-                    const message_el = this.content.querySelector(`[data-msgid="${message.get('msgid')}"]`);
-                    if (!_.isNull(message_el)) {
-                        message_el.insertAdjacentHTML(
-                            'afterend',
-                            tpl_info({
-                                'extra_classes': 'chat-error',
-                                'message': message.get('message'),
-                                'isodate': moment().format(),
-                                'data': ''
-                            }));
-                        this.scrollDown();
-                    }
-                },
-
                 onMessageAdded (message) {
                 onMessageAdded (message) {
                     /* Handler that gets called when a new message object is created.
                     /* Handler that gets called when a new message object is created.
                      *
                      *
@@ -702,13 +702,13 @@
                         delete this.clear_status_timeout;
                         delete this.clear_status_timeout;
                     }
                     }
                     if (message.get('type') === 'error') {
                     if (message.get('type') === 'error') {
-                        this.handleErrorMessage(message);
+                        this.showMessage(message);
                     } else {
                     } else {
                         if (message.get('chat_state')) {
                         if (message.get('chat_state')) {
                             this.showChatStateNotification(message);
                             this.showChatStateNotification(message);
                         }
                         }
                         if (message.get('file') || message.get('message')) {
                         if (message.get('file') || message.get('message')) {
-                            this.handleTextMessage(message);
+                            this.showMessage(message);
                         }
                         }
                     }
                     }
                     _converse.emit('messageAdded', {
                     _converse.emit('messageAdded', {

+ 21 - 5
src/converse-message-view.js

@@ -11,6 +11,7 @@
         "emojione",
         "emojione",
         "tpl!action",
         "tpl!action",
         "tpl!file",
         "tpl!file",
+        "tpl!info",
         "tpl!message",
         "tpl!message",
         "tpl!spoiler_message"
         "tpl!spoiler_message"
     ], factory);
     ], factory);
@@ -20,6 +21,7 @@
         emojione,
         emojione,
         tpl_action,
         tpl_action,
         tpl_file,
         tpl_file,
+        tpl_info,
         tpl_message,
         tpl_message,
         tpl_spoiler_message
         tpl_spoiler_message
     ) {
     ) {
@@ -53,7 +55,10 @@
                 render () {
                 render () {
                     if (this.model.get('file') && !this.model.get('message')) {
                     if (this.model.get('file') && !this.model.get('message')) {
                         return this.renderFileUploadProgresBar();
                         return this.renderFileUploadProgresBar();
+                    } else if (this.model.get('type') === 'error') {
+                        return this.renderErrorMessage();
                     }
                     }
+
                     let template, username,
                     let template, username,
                         text = this.model.get('message');
                         text = this.model.get('message');
 
 
@@ -91,6 +96,10 @@
                     u.renderImageURLs(msg_content).then(() => {
                     u.renderImageURLs(msg_content).then(() => {
                         this.model.collection.trigger('rendered');
                         this.model.collection.trigger('rendered');
                     });
                     });
+                    return this.replaceElement(msg);
+                },
+
+                replaceElement (msg) {
                     if (!_.isNil(this.el.parentElement)) {
                     if (!_.isNil(this.el.parentElement)) {
                         this.el.parentElement.replaceChild(msg, this.el);
                         this.el.parentElement.replaceChild(msg, this.el);
                     }
                     }
@@ -98,13 +107,20 @@
                     return this.el;
                     return this.el;
                 },
                 },
 
 
+                renderErrorMessage () {
+                    const moment_time = moment(this.model.get('time')),
+                          msg = u.stringToElement(
+                        tpl_info(_.extend(this.model.toJSON(), {
+                            'extra_classes': 'chat-error',
+                            'isodate': moment_time.format(),
+                            'data': ''
+                        })));
+                    return this.replaceElement(msg);
+                },
+
                 renderFileUploadProgresBar () {
                 renderFileUploadProgresBar () {
                     const msg = u.stringToElement(tpl_file(this.model.toJSON()));
                     const msg = u.stringToElement(tpl_file(this.model.toJSON()));
-                    if (!_.isNil(this.el.parentElement)) {
-                        this.el.parentElement.replaceChild(msg, this.el);
-                    }
-                    this.setElement(msg);
-                    return this.el;
+                    return this.replaceElement(msg);
                 },
                 },
 
 
                 isMeCommand () {
                 isMeCommand () {