Maintain scroll position when re-inserting #conversejs element

This commit is contained in:
JC Brand 2020-07-28 10:41:07 +02:00
parent 867f80e95e
commit 4927d561a5
4 changed files with 47 additions and 36 deletions

View File

@ -176,6 +176,10 @@ converse.plugins.add('converse-chatboxviews', {
const el = _converse.chatboxviews?.el; const el = _converse.chatboxviews?.el;
if (el && !container.contains(el)) { if (el && !container.contains(el)) {
container.insertAdjacentElement('afterBegin', el); container.insertAdjacentElement('afterBegin', el);
api.chatviews.get()
.filter(v => v.model.get('id') !== 'controlbox')
.forEach(v => v.maintainScrollTop());
} else if (!el) { } else if (!el) {
throw new Error("Cannot insert non-existing #conversejs element into the DOM"); throw new Error("Cannot insert non-existing #conversejs element into the DOM");
} }

View File

@ -208,7 +208,7 @@ converse.plugins.add('converse-chatview', {
this.listenTo(this.model.messages, 'add', this.onMessageAdded); this.listenTo(this.model.messages, 'add', this.onMessageAdded);
this.listenTo(this.model.messages, 'change', this.renderChatHistory); this.listenTo(this.model.messages, 'change', this.renderChatHistory);
this.listenTo(this.model.messages, 'remove', 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.messages, 'reset', this.renderChatHistory);
this.listenTo(this.model.notifications, 'change', this.renderNotifications); this.listenTo(this.model.notifications, 'change', this.renderNotifications);
this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages); this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
@ -241,7 +241,7 @@ converse.plugins.add('converse-chatview', {
render () { render () {
const result = tpl_chatbox( 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); render(result, this.el);
this.content = this.el.querySelector('.chat-content'); this.content = this.el.querySelector('.chat-content');
@ -486,19 +486,35 @@ converse.plugins.add('converse-chatview', {
api.trigger('afterMessagesFetched', this.model); 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(); 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) { scrollDown (ev) {
ev?.preventDefault?.(); ev?.preventDefault?.();
ev?.stopPropagation?.(); ev?.stopPropagation?.();
if (this.model.get('scrolled')) { if (this.model.get('scrolled')) {
u.safeSave(this.model, { u.safeSave(this.model, {
'scrolled': false, 'scrolled': false,
'top_visible_message': null, 'scrollTop': null,
}); });
} }
if (this.msgs_container.scrollTo) { if (this.msgs_container.scrollTo) {
@ -510,6 +526,19 @@ converse.plugins.add('converse-chatview', {
this.onScrolledDown(); 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 () { insertIntoDOM () {
_converse.chatboxviews.insertRowColumn(this.el); _converse.chatboxviews.insertRowColumn(this.el);
/** /**
@ -537,28 +566,6 @@ converse.plugins.add('converse-chatview', {
this.content.querySelectorAll('.spinner').forEach(u.removeElement); 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) { onStatusMessageChanged (item) {
this.renderHeading(); this.renderHeading();
/** /**
@ -1091,8 +1098,9 @@ converse.plugins.add('converse-chatview', {
* which debounces this method by 100ms. * which debounces this method by 100ms.
* @private * @private
*/ */
_markScrolled: function () { _markScrolled: function (ev) {
let scrolled = true; let scrolled = true;
let scrollTop = null;
const is_at_bottom = const is_at_bottom =
(this.msgs_container.scrollTop + this.msgs_container.clientHeight) >= (this.msgs_container.scrollTop + this.msgs_container.clientHeight) >=
this.msgs_container.scrollHeight - 62; // sigh... this.msgs_container.scrollHeight - 62; // sigh...
@ -1108,15 +1116,14 @@ converse.plugins.add('converse-chatview', {
* @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... }); * @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
*/ */
api.trigger('chatBoxScrolledUp', this); api.trigger('chatBoxScrolledUp', this);
} else {
scrollTop = ev.target.scrollTop;
} }
u.safeSave(this.model, { u.safeSave(this.model, { scrolled, scrollTop });
'scrolled': scrolled,
'top_visible_message': null
});
}, },
viewUnreadMessages () { viewUnreadMessages () {
this.model.save({'scrolled': false, 'top_visible_message': null}); this.model.save({'scrolled': false, 'scrollTop': null});
this.scrollDown(); this.scrollDown();
}, },

View File

@ -190,7 +190,7 @@ converse.plugins.add('converse-muc-views', {
this.listenTo(this.model, 'show', this.show); this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel); this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
this.listenTo(this.model.features, 'change:open', this.renderHeading); 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); this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
// Bind so that we can pass it to addEventListener and removeEventListener // Bind so that we can pass it to addEventListener and removeEventListener

View File

@ -168,12 +168,12 @@ class MessageBodyRenderer {
// image loads, it triggers 'scroll' and the chat will be marked as scrolled, // 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 // which is technically true, but not what we want because the user
// didn't initiate the scrolling. // 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(); this.text = this.component.model.getMessageText();
} }
scrollDownOnImageLoad () { scrollDownOnImageLoad () {
if (!this.scrolled) { if (!this.was_scrolled_up) {
this.chatview.scrollDown(); this.chatview.scrollDown();
} }
} }