diff --git a/spec/chatbox.js b/spec/chatbox.js index a869ad275..d8557c2ad 100644 --- a/spec/chatbox.js +++ b/spec/chatbox.js @@ -879,6 +879,144 @@ }).then(done); })); + it("can be received out of order, and will still be displayed in the right order", + mock.initConverseWithPromises( + null, ['rosterGroupsFetched'], {}, + function (done, _converse) { + + test_utils.createContacts(_converse, 'current'); + test_utils.openControlBox(); + test_utils.openContactsPanel(_converse); + + test_utils.waitUntil(function () { + return _converse.rosterview.$el.find('.roster-group').length; + }, 300) + .then(function () { + var message, msg; + spyOn(_converse, 'log'); + spyOn(_converse.chatboxes, 'getChatBox').and.callThrough(); + _converse.filter_by_resource = true; + var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; + + /* + * + * + * + * Call me but love, and I'll be new baptized; Henceforth I never will be Romeo. + * + * + * + */ + msg = $msg({'id': 'aeb213', 'to': _converse.bare_jid}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T13:08:25Z'}).up() + .c('message', { + 'xmlns': 'jabber:client', + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("message from today") + .tree(); + _converse.chatboxes.onMessage(msg); + + msg = $msg({'id': 'aeb214', 'to': _converse.bare_jid}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T23:08:25Z'}).up() + .c('message', { + 'xmlns': 'jabber:client', + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("Older message") + .tree(); + _converse.chatboxes.onMessage(msg); + + msg = $msg({'id': 'aeb215', 'to': _converse.bare_jid}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}).up() + .c('message', { + 'xmlns': 'jabber:client', + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("Inbetween message") + .tree(); + _converse.chatboxes.onMessage(msg); + + msg = $msg({'id': 'aeb216', 'to': _converse.bare_jid}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}).up() + .c('message', { + 'xmlns': 'jabber:client', + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("another inbetween message") + .tree(); + _converse.chatboxes.onMessage(msg); + + msg = $msg({'id': 'aeb217', 'to': _converse.bare_jid}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'}).up() + .c('message', { + 'xmlns': 'jabber:client', + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("An earlier message today") + .tree(); + _converse.chatboxes.onMessage(msg); + + msg = $msg({'id': 'aeb218', 'to': _converse.bare_jid}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T23:28:23Z'}).up() + .c('message', { + 'xmlns': 'jabber:client', + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("newer message from today") + .tree(); + _converse.chatboxes.onMessage(msg); + + var chatboxview = _converse.chatboxviews.get(sender_jid); + var $chat_content = chatboxview.$el.find('.chat-content'); + chatboxview.clearSpinner(); //cleanup + + var $time = $chat_content.find('time'); + expect($time.length).toEqual(3); + $time = $chat_content.find('time:first'); + expect($time.data('isodate')).toEqual('2017-12-31T00:00:00+00:00'); + + expect($time[0].nextElementSibling.querySelector('.chat-msg-content').textContent).toBe('Older message'); + var $el = $chat_content.find('.chat-message:first').find('.chat-msg-content') + expect($el.text()).toEqual('Older message'); + + $time = $chat_content.find('time:eq(1)'); + expect($time.data('isodate')).toEqual('2018-01-01T00:00:00+00:00'); + expect($time[0].nextElementSibling.querySelector('.chat-msg-content').textContent).toBe('Inbetween message'); + $el = $chat_content.find('.chat-message:eq(1)'); + expect($el.find('.chat-msg-content').text()).toEqual('Inbetween message'); + expect($el[0].nextElementSibling.querySelector('.chat-msg-content').textContent).toEqual('another inbetween message'); + $el = $chat_content.find('.chat-message:eq(2)'); + expect($el.find('.chat-msg-content').text()).toEqual('another inbetween message'); + + $time = $chat_content.find('time:last'); + expect($time.data('isodate')).toEqual('2018-01-02T00:00:00+00:00'); + expect($time[0].nextElementSibling.querySelector('.chat-msg-content').textContent).toBe('An earlier message today'); + $el = $chat_content.find('.chat-message:eq(3)'); + expect($el.find('.chat-msg-content').text()).toEqual('An earlier message today'); + + $el = $chat_content.find('.chat-message:eq(4)'); + expect($el.find('.chat-msg-content').text()).toEqual('message from today'); + expect($el[0].nextElementSibling.querySelector('.chat-msg-content').textContent).toEqual('newer message from today'); + done(); + }); + })); + it("is ignored if it's intended for a different resource and filter_by_resource is set to true", mock.initConverseWithPromises( null, ['rosterGroupsFetched'], {}, diff --git a/src/converse-chatview.js b/src/converse-chatview.js index 0a8f399a0..703e72337 100644 --- a/src/converse-chatview.js +++ b/src/converse-chatview.js @@ -42,7 +42,7 @@ tpl_toolbar ) { "use strict"; - const { $msg, Backbone, Strophe, _, b64_sha1, moment } = converse.env; + const { $msg, Backbone, Strophe, _, b64_sha1, sizzle, moment } = converse.env; const u = converse.env.utils; const KEY = { ENTER: 13, @@ -277,7 +277,6 @@ this.model.on('sendMessage', this.sendMessage, this); this.render().renderToolbar().insertHeading().fetchMessages(); - this.createEmojiPicker(); u.refreshWebkit(); _converse.emit('chatBoxOpened', this); _converse.emit('chatBoxInitialized', this); @@ -418,6 +417,26 @@ )(this.renderMessage(attrs)); }, + getLastMessageElement () { + let last_msg_el = this.content.lastElementChild; + while (!_.isNull(last_msg_el) && + !u.hasClass(last_msg_el, 'message') && + !u.hasClass(last_msg_el, 'chat-info')) { + last_msg_el = last_msg_el.previousSibling + } + return last_msg_el; + }, + + getFirstMessageElement () { + let first_msg_el = this.content.firstElementChild; + while (!_.isNull(first_msg_el) && + !u.hasClass(first_msg_el, 'message') && + !u.hasClass(first_msg_el, 'chat-info')) { + first_msg_el = first_msg_el.nextSibling + } + return first_msg_el; + }, + getLastMessageDate (cutoff) { /* Return the ISO8601 format date of the latest message. * @@ -425,22 +444,36 @@ * (Object) cutoff: Moment Date cutoff date. The last * message received cutoff this date will be returned. */ - if (!cutoff) { - const last_msg = this.content.lastElementChild; - return last_msg ? last_msg.getAttribute('data-isodate') : null + const first_msg = this.getFirstMessageElement(), + oldest_date = first_msg ? first_msg.getAttribute('data-isodate') : null; + if (!_.isNull(oldest_date) && moment(oldest_date).isAfter(cutoff)) { + return null; + } + const last_msg = this.getLastMessageElement(), + most_recent_date = last_msg ? last_msg.getAttribute('data-isodate') : null; + if (_.isNull(most_recent_date) || moment(most_recent_date).isBefore(cutoff)) { + return most_recent_date; } const msg_dates = _.invokeMap( - this.content.querySelectorAll('.message'), - Element.prototype.getAttribute, - 'data-isodate' + sizzle('.message, .chat-info', this.content), + Element.prototype.getAttribute, 'data-isodate' ) if (_.isObject(cutoff)) { cutoff = cutoff.format(); } msg_dates.push(cutoff); msg_dates.sort(); - const idx = msg_dates.indexOf(cutoff); - return msg_dates[idx === 0 ? idx : idx-1]; + const idx = msg_dates.lastIndexOf(cutoff); + if (idx === 0) { + return null; + } else { + return msg_dates[idx-1]; + } + }, + + getDayIndicatorElement (date) { + return this.content.querySelector( + `.chat-date[data-isodate="${date.startOf('day').format()}"]`); }, showMessage (attrs) { @@ -456,54 +489,37 @@ * attributes. */ const current_msg_date = moment(attrs.time) || moment, - first_msg_el = this.content.firstElementChild, - first_msg_date = first_msg_el ? first_msg_el.getAttribute('data-isodate') : null, - append_element = _.bind(this.content.insertAdjacentElement, this.content, 'beforeend'), - append_html = _.bind(this.content.insertAdjacentHTML, this.content, 'beforeend'), - prepend_element = _.bind(this.content.insertAdjacentElement, this.content, 'afterbegin'); + prepend_html = _.bind(this.content.insertAdjacentHTML, this.content, 'afterbegin'), + previous_msg_date = this.getLastMessageDate(current_msg_date); - if (!first_msg_date) { - // This is the first received message, so we insert a - // date indicator before it. - this.insertDayIndicator(current_msg_date, append_html); - this.insertMessage(attrs, append_element); - return; - } - - const last_msg_date = this.getLastMessageDate(); - if (current_msg_date.isAfter(last_msg_date) || - current_msg_date.isSame(last_msg_date)) { - // The new message is after the last message - if (current_msg_date.isAfter(last_msg_date, 'day')) { - // Append a new day indicator - this.insertDayIndicator(current_msg_date, append_html); - } - this.insertMessage(attrs, append_element); - return; - } - - if (current_msg_date.isBefore(first_msg_date) || - current_msg_date.isSame(first_msg_date)) { - // The message is before the first, but on the same day. - // We need to prepend the message immediately before the - // first message (so that it'll still be after the day - // indicator). - this.insertMessage(attrs, prepend_element); - if (current_msg_date.isBefore(first_msg_date, 'day')) { - // This message is also on a different day, so - // we prepend a day indicator. - this.insertDayIndicator(current_msg_date, append_html); - } - return; - } - const previous_msg_date = this.getLastMessageDate(current_msg_date); - const previous_msg_el = this.content.querySelector( - `.message[data-isodate="${previous_msg_date}"]`); - if (_.isNull(previous_msg_el)) { - this.insertMessage(attrs, prepend_element); + if (_.isNull(previous_msg_date)) { + this.insertMessage(attrs, _.bind(this.content.insertAdjacentElement, this.content, 'afterbegin')); + this.insertDayIndicator(current_msg_date, prepend_html); } else { - this.insertMessage( - attrs, _.bind(previous_msg_el.insertAdjacentElement, previous_msg_el, 'afterend')); + const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date}"]:last`, this.content).pop(); + const day_el = this.getDayIndicatorElement(current_msg_date) + if (current_msg_date.isAfter(previous_msg_date, 'day')) { + if (_.isNull(day_el)) { + this.insertMessage( + attrs, + _.bind(previous_msg_el.insertAdjacentElement, previous_msg_el, 'afterend') + ); + this.insertDayIndicator( + current_msg_date, + _.bind(this.content.insertAdjacentHTML, previous_msg_el, 'afterend') + ); + } else { + this.insertMessage( + attrs, + _.bind(previous_msg_el.insertAdjacentElement, day_el, 'afterend') + ); + } + } else { + this.insertMessage( + attrs, + _.bind(previous_msg_el.insertAdjacentElement, previous_msg_el, 'afterend') + ); + } } }, @@ -590,7 +606,7 @@ if (spinner === true) { $(this.content).append(tpl_spinner); } else if (spinner === false) { - $(this.content).find('span.spinner').remove(); + this.clearSpinner(); } return this.scrollDown(); }, diff --git a/src/converse-headline.js b/src/converse-headline.js index 9809c85a4..ae9aa161a 100644 --- a/src/converse-headline.js +++ b/src/converse-headline.js @@ -82,6 +82,7 @@ }, initialize () { + this.markScrolled = _.debounce(this._markScrolled, 100); this.disable_mam = true; // Don't do MAM queries for this box this.model.messages.on('add', this.onMessageAdded, this); this.model.on('show', this.show, this);