From 4927d561a5f59918ce6fafd7a5187932b7691868 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 28 Jul 2020 10:41:07 +0200 Subject: [PATCH] Maintain scroll position when re-inserting #conversejs element --- src/converse-chatboxviews.js | 4 ++ src/converse-chatview.js | 73 +++++++++++++++++--------------- src/converse-muc-views.js | 2 +- src/templates/directives/body.js | 4 +- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/converse-chatboxviews.js b/src/converse-chatboxviews.js index 858a96b37..e34e1474c 100644 --- a/src/converse-chatboxviews.js +++ b/src/converse-chatboxviews.js @@ -176,6 +176,10 @@ converse.plugins.add('converse-chatboxviews', { const el = _converse.chatboxviews?.el; if (el && !container.contains(el)) { container.insertAdjacentElement('afterBegin', el); + api.chatviews.get() + .filter(v => v.model.get('id') !== 'controlbox') + .forEach(v => v.maintainScrollTop()); + } else if (!el) { throw new Error("Cannot insert non-existing #conversejs element into the DOM"); } diff --git a/src/converse-chatview.js b/src/converse-chatview.js index 700c843dc..d931621c5 100644 --- a/src/converse-chatview.js +++ b/src/converse-chatview.js @@ -208,7 +208,7 @@ converse.plugins.add('converse-chatview', { this.listenTo(this.model.messages, 'add', this.onMessageAdded); this.listenTo(this.model.messages, 'change', this.renderChatHistory); this.listenTo(this.model.messages, 'remove', this.renderChatHistory); - this.listenTo(this.model.messages, 'rendered', this.maybeScrollDownOnMessage); + this.listenTo(this.model.messages, 'rendered', this.maybeScrollDown); this.listenTo(this.model.messages, 'reset', this.renderChatHistory); this.listenTo(this.model.notifications, 'change', this.renderNotifications); this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages); @@ -241,7 +241,7 @@ converse.plugins.add('converse-chatview', { render () { const result = tpl_chatbox( - Object.assign(this.model.toJSON(), {'markScrolled': () => this.markScrolled()}) + Object.assign(this.model.toJSON(), {'markScrolled': ev => this.markScrolled(ev)}) ); render(result, this.el); this.content = this.el.querySelector('.chat-content'); @@ -486,19 +486,35 @@ converse.plugins.add('converse-chatview', { api.trigger('afterMessagesFetched', this.model); }, - maybeScrollDownOnMessage (message) { - if (message.get('sender') === 'me' || !this.model.get('scrolled')) { + /** + * Scrolls the chat down, *if* appropriate. + * + * Will only scroll down if we have received a message from + * ourselves, or if the chat was scrolled down before (i.e. the + * `scrolled` flag is `false`); + * @param { _converse.Message|_converse.ChatRoomMessage } [message] + * - An optional message that serves as the cause for needing to scroll down. + */ + maybeScrollDown (message) { + if (message?.get('sender') === 'me' || !this.model.get('scrolled')) { this.debouncedScrollDown(); } }, + /** + * Scrolls the chat down. + * + * This method will always scroll the chat down, regardless of + * whether the user scrolled up manually or not. + * @param { Event } [ev] - An optional event that is the cause for needing to scroll down. + */ scrollDown (ev) { ev?.preventDefault?.(); ev?.stopPropagation?.(); if (this.model.get('scrolled')) { u.safeSave(this.model, { 'scrolled': false, - 'top_visible_message': null, + 'scrollTop': null, }); } if (this.msgs_container.scrollTo) { @@ -510,6 +526,19 @@ converse.plugins.add('converse-chatview', { this.onScrolledDown(); }, + /** + * Scroll to the previously saved scrollTop position, or scroll + * down if it wasn't set. + */ + maintainScrollTop () { + const pos = this.model.get('scrollTop'); + if (pos) { + this.msgs_container.scrollTop = pos; + } else { + this.scrollDown(); + } + }, + insertIntoDOM () { _converse.chatboxviews.insertRowColumn(this.el); /** @@ -537,28 +566,6 @@ converse.plugins.add('converse-chatview', { this.content.querySelectorAll('.spinner').forEach(u.removeElement); }, - setScrollPosition (message_el) { - /* Given a newly inserted message, determine whether we - * should keep the scrollbar in place (so as to not scroll - * up when using infinite scroll). - */ - if (this.model.get('scrolled')) { - const next_msg_el = u.getNextElement(message_el, ".chat-msg"); - if (next_msg_el) { - // The currently received message is not new, there - // are newer messages after it. So let's see if we - // should maintain our current scroll position. - if (this.content.scrollTop === 0 || this.model.get('top_visible_message')) { - const top_visible_message = this.model.get('top_visible_message') || next_msg_el; - this.model.set('top_visible_message', top_visible_message); - this.content.scrollTop = top_visible_message.offsetTop - 30; - } - } - } else { - this.scrollDown(); - } - }, - onStatusMessageChanged (item) { this.renderHeading(); /** @@ -1091,8 +1098,9 @@ converse.plugins.add('converse-chatview', { * which debounces this method by 100ms. * @private */ - _markScrolled: function () { + _markScrolled: function (ev) { let scrolled = true; + let scrollTop = null; const is_at_bottom = (this.msgs_container.scrollTop + this.msgs_container.clientHeight) >= this.msgs_container.scrollHeight - 62; // sigh... @@ -1108,15 +1116,14 @@ converse.plugins.add('converse-chatview', { * @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... }); */ api.trigger('chatBoxScrolledUp', this); + } else { + scrollTop = ev.target.scrollTop; } - u.safeSave(this.model, { - 'scrolled': scrolled, - 'top_visible_message': null - }); + u.safeSave(this.model, { scrolled, scrollTop }); }, viewUnreadMessages () { - this.model.save({'scrolled': false, 'top_visible_message': null}); + this.model.save({'scrolled': false, 'scrollTop': null}); this.scrollDown(); }, diff --git a/src/converse-muc-views.js b/src/converse-muc-views.js index cb47a02b8..27f23d9bd 100644 --- a/src/converse-muc-views.js +++ b/src/converse-muc-views.js @@ -190,7 +190,7 @@ converse.plugins.add('converse-muc-views', { this.listenTo(this.model, 'show', this.show); this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel); this.listenTo(this.model.features, 'change:open', this.renderHeading); - this.listenTo(this.model.messages, 'rendered', this.maybeScrollDownOnMessage); + this.listenTo(this.model.messages, 'rendered', this.maybeScrollDown); this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged); // Bind so that we can pass it to addEventListener and removeEventListener diff --git a/src/templates/directives/body.js b/src/templates/directives/body.js index ba55b993c..8173b4615 100644 --- a/src/templates/directives/body.js +++ b/src/templates/directives/body.js @@ -168,12 +168,12 @@ class MessageBodyRenderer { // image loads, it triggers 'scroll' and the chat will be marked as scrolled, // which is technically true, but not what we want because the user // didn't initiate the scrolling. - this.scrolled = this.chatview.model.get('scrolled'); + this.was_scrolled_up = this.chatview.model.get('scrolled'); this.text = this.component.model.getMessageText(); } scrollDownOnImageLoad () { - if (!this.scrolled) { + if (!this.was_scrolled_up) { this.chatview.scrollDown(); } }