Use flex-direction: column-reverse

On the `<converse-chat-content>` element. This removes the need for all
the manual scrolling.

Firefox finally supports this feature. Unfortunately Firefox ESR doesn't
yet, but I can't wait anymore.
This commit is contained in:
JC Brand 2021-06-07 19:26:16 +02:00
parent 9bcf5f2947
commit 825e2643ae
8 changed files with 25 additions and 92 deletions

View File

@ -138,13 +138,6 @@
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
converse-chat-content {
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
}
converse-chat-message { converse-chat-message {
.spinner { .spinner {
width: 100%; width: 100%;

View File

@ -75,7 +75,6 @@ export default class ChatView extends BaseChatView {
afterShown () { afterShown () {
this.model.setChatState(_converse.ACTIVE); this.model.setChatState(_converse.ACTIVE);
this.scrollDown();
this.maybeFocus(); this.maybeFocus();
} }
} }

View File

@ -25,7 +25,6 @@ class HeadlinesView extends BaseChatView {
await this.model.messages.fetched; await this.model.messages.fetched;
this.model.maybeShow(); this.model.maybeShow();
this.scrollDown();
/** /**
* Triggered once the {@link _converse.HeadlinesBoxView} has been initialized * Triggered once the {@link _converse.HeadlinesBoxView} has been initialized
* @event _converse#headlinesBoxViewInitialized * @event _converse#headlinesBoxViewInitialized

View File

@ -21,15 +21,12 @@ export default class MUCView extends BaseChatView {
this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged); this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged);
this.listenTo(this.model, 'change:composing_spoiler', this.requestUpdateMessageForm); this.listenTo(this.model, 'change:composing_spoiler', this.requestUpdateMessageForm);
this.listenTo(this.model, 'change:hidden', () => this.afterShown());
this.listenTo(this.model, 'change:minimized', () => this.afterShown());
this.listenTo(this.model, 'show', this.show); this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model.session, 'change:connection_status', this.updateAfterTransition); this.listenTo(this.model.session, 'change:connection_status', this.updateAfterTransition);
this.listenTo(this.model.session, 'change:view', this.requestUpdate); this.listenTo(this.model.session, 'change:view', this.requestUpdate);
this.updateAfterTransition(); this.updateAfterTransition();
this.model.maybeShow(); this.model.maybeShow();
this.scrollDown();
/** /**
* Triggered once a { @link _converse.ChatRoomView } has been opened * Triggered once a { @link _converse.ChatRoomView } has been opened
* @event _converse#chatRoomViewInitialized * @event _converse#chatRoomViewInitialized
@ -43,17 +40,6 @@ export default class MUCView extends BaseChatView {
return tpl_muc({ 'model': this.model }); return tpl_muc({ 'model': this.model });
} }
/**
* Callback method that gets called after the chat has become visible.
* @private
* @method _converse.ChatRoomView#afterShown
*/
afterShown () {
if (!this.model.get('hidden') && !this.model.get('minimized')) {
this.scrollDown();
}
}
/** /**
* Closes this chat, which implies leaving the MUC as well. * Closes this chat, which implies leaving the MUC as well.
* @private * @private

View File

@ -6,6 +6,8 @@ import { html } from 'lit';
import { onScrolledDown } from './utils.js'; import { onScrolledDown } from './utils.js';
import { safeSave } from '@converse/headless/utils/core.js'; import { safeSave } from '@converse/headless/utils/core.js';
import './styles/chat-content.scss';
export default class ChatContent extends CustomElement { export default class ChatContent extends CustomElement {
@ -17,12 +19,11 @@ export default class ChatContent extends CustomElement {
connectedCallback () { connectedCallback () {
super.connectedCallback(); super.connectedCallback();
this.debouncedMaintainScroll = debounce(this.maintainScrollPosition, 100);
this.markScrolled = debounce(this._markScrolled, 50); this.markScrolled = debounce(this._markScrolled, 50);
this.model = _converse.chatboxes.get(this.jid); this.model = _converse.chatboxes.get(this.jid);
this.listenTo(this.model, 'change:hidden_occupants', this.requestUpdate); this.listenTo(this.model, 'change:hidden_occupants', this.requestUpdate);
this.listenTo(this.model, 'change:scrolled', this.requestUpdate); this.listenTo(this.model, 'change:scrolled', this.scrollDown);
this.listenTo(this.model.messages, 'add', this.requestUpdate); this.listenTo(this.model.messages, 'add', this.requestUpdate);
this.listenTo(this.model.messages, 'change', this.requestUpdate); this.listenTo(this.model.messages, 'change', this.requestUpdate);
this.listenTo(this.model.messages, 'remove', this.requestUpdate); this.listenTo(this.model.messages, 'remove', this.requestUpdate);
@ -34,55 +35,22 @@ export default class ChatContent extends CustomElement {
if (this.model.occupants) { if (this.model.occupants) {
this.listenTo(this.model.occupants, 'change', this.requestUpdate); this.listenTo(this.model.occupants, 'change', this.requestUpdate);
} }
// We jot down whether we were scrolled down before rendering, because when an
// 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.was_scrolled_up = this.model.get('scrolled');
this.addEventListener('imageLoaded', () => {
this.debouncedMaintainScroll();
});
this.addEventListener('scroll', () => this.markScrolled()); this.addEventListener('scroll', () => this.markScrolled());
this.initIntersectionObserver();
} }
render () { render () {
// This element has "flex-direction: reverse", so elements here are
// shown in reverse order.
return html` return html`
${ this.model.ui?.get('chat-content-spinner-top') ? html`<span class="spinner fa fa-spinner centered"></span>` : '' } <div class="chat-content__notifications">${this.model.getNotificationsText()}</div>
<converse-message-history <converse-message-history
.model=${this.model} .model=${this.model}
.observer=${this.observer}
.messages=${[...this.model.messages.models]}> .messages=${[...this.model.messages.models]}>
</converse-message-history> </converse-message-history>
<div class="chat-content__notifications">${this.model.getNotificationsText()}</div> ${ this.model.ui?.get('chat-content-spinner-top') ? html`<span class="spinner fa fa-spinner centered"></span>` : '' }
`; `;
} }
updated () {
const scrolled = this.model.get('scrolled');
if (this.was_scrolled_up === scrolled) {
this.debouncedMaintainScroll();
} else {
this.was_scrolled_up = scrolled;
if (!this.scrolled) {
this.scrollDown();
}
}
}
initIntersectionObserver () {
if (this.observer) {
this.observer.disconnect();
} else {
const options = {
root: this,
threshold: [0.1]
}
const handler = ev => this.setAnchoredMessage(ev);
this.observer = new IntersectionObserver(handler, options);
}
}
/** /**
* Called when the chat content is scrolled up or down. * Called when the chat content is scrolled up or down.
* We want to record when the user has scrolled away from * We want to record when the user has scrolled away from
@ -95,11 +63,14 @@ export default class ChatContent extends CustomElement {
*/ */
_markScrolled () { _markScrolled () {
let scrolled = true; let scrolled = true;
const is_at_bottom = this.scrollTop + this.clientHeight >= this.scrollHeight; const is_at_bottom = this.scrollTop === 0;
const is_at_top =
Math.ceil(this.clientHeight-this.scrollTop) >= (this.scrollHeight-Math.ceil(this.scrollHeight/20));
if (is_at_bottom) { if (is_at_bottom) {
scrolled = false; scrolled = false;
onScrolledDown(this.model); onScrolledDown(this.model);
} else if (this.scrollTop === 0) { } else if (is_at_top) {
/** /**
* Triggered once the chat's message area has been scrolled to the top * Triggered once the chat's message area has been scrolled to the top
* @event _converse#chatBoxScrolledUp * @event _converse#chatBoxScrolledUp
@ -113,31 +84,15 @@ export default class ChatContent extends CustomElement {
} }
} }
setAnchoredMessage (entries) { scrollDown () {
if (!this.model?.ui || this.model.ui.get('chat-content-spinner-top')) { if (this.model.get('scrolled')) {
return; return;
} }
entries = entries.filter(e => e.isIntersecting);
const current = entries.reduce((p, c) => c.boundingClientRect.y >= (p?.boundingClientRect.y || 0) ? c : p, null);
if (current) {
this.anchored_message = current.target;
}
}
maintainScrollPosition () {
if (this.was_scrolled_up) {
this.anchored_message?.scrollIntoView(true);
} else {
this.scrollDown();
}
}
scrollDown () {
if (this.scrollTo) { if (this.scrollTo) {
const behavior = this.scrollTop ? 'smooth' : 'auto'; const behavior = this.scrollTop ? 'smooth' : 'auto';
this.scrollTo({ 'top': this.scrollHeight, behavior }); this.scrollTo({ 'top': 0, behavior });
} else { } else {
this.scrollTop = this.scrollHeight; this.scrollTop = 0;
} }
/** /**
* Triggered once the converse-chat-content element has been scrolled down to the bottom. * Triggered once the converse-chat-content element has been scrolled down to the bottom.

View File

@ -51,7 +51,6 @@ export default class MessageHistory extends CustomElement {
static get properties () { static get properties () {
return { return {
model: { type: Object }, model: { type: Object },
observer: { type: Object },
messages: { type: Array } messages: { type: Array }
} }
} }
@ -68,7 +67,6 @@ export default class MessageHistory extends CustomElement {
const day = getDayIndicator(model); const day = getDayIndicator(model);
const templates = day ? [day] : []; const templates = day ? [day] : [];
const message = html`<converse-chat-message const message = html`<converse-chat-message
.observer=${this.observer}
jid="${this.model.get('jid')}" jid="${this.model.get('jid')}"
mid="${model.get('id')}"></converse-chat-message>` mid="${model.get('id')}"></converse-chat-message>`

View File

@ -24,8 +24,7 @@ export default class Message extends CustomElement {
static get properties () { static get properties () {
return { return {
jid: { type: String }, jid: { type: String },
mid: { type: String }, mid: { type: String }
observer: { type: Object }
} }
} }
@ -60,10 +59,6 @@ export default class Message extends CustomElement {
} }
} }
firstUpdated () {
this.observer.observe(this);
}
getProps () { getProps () {
return Object.assign( return Object.assign(
this.model.toJSON(), this.model.toJSON(),

View File

@ -0,0 +1,8 @@
converse-chat-content {
display: flex;
flex-direction: column-reverse;
height: 100%;
justify-content: space-between;
overflow: auto;
}