Use intersection observer to remember scrolling position
This commit is contained in:
parent
279a3c3413
commit
58d96c8594
@ -362,13 +362,16 @@ u.onMultipleEvents = function (events=[], callback) {
|
||||
events.forEach(e => e.object.on(e.event, handler));
|
||||
};
|
||||
|
||||
u.safeSave = function (model, attributes, options) {
|
||||
|
||||
export function safeSave (model, attributes, options) {
|
||||
if (u.isPersistableModel(model)) {
|
||||
model.save(attributes, options);
|
||||
} else {
|
||||
model.set(attributes, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
u.safeSave = safeSave;
|
||||
|
||||
u.siblingIndex = function (el) {
|
||||
/* eslint-disable no-cond-assign */
|
||||
|
@ -84,7 +84,7 @@ export default class ChatBottomPanel extends ElementView {
|
||||
|
||||
viewUnreadMessages (ev) {
|
||||
ev?.preventDefault?.();
|
||||
this.model.save({ 'scrolled': false, 'scrollTop': null });
|
||||
this.model.save({ 'scrolled': false });
|
||||
}
|
||||
|
||||
onMessageCorrecting (message) {
|
||||
|
@ -8,8 +8,7 @@ export default (o) => html`
|
||||
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
|
||||
<converse-chat-content
|
||||
class="chat-content__messages"
|
||||
jid="${o.jid}"
|
||||
@scroll=${o.markScrolled}></converse-chat-content>
|
||||
jid="${o.jid}"></converse-chat-content>
|
||||
|
||||
<div class="chat-content__help"></div>
|
||||
</div>
|
||||
|
@ -22,10 +22,7 @@ export default class ChatView extends BaseChatView {
|
||||
async initialize () {
|
||||
const jid = this.getAttribute('jid');
|
||||
_converse.chatboxviews.add(jid, this);
|
||||
|
||||
this.model = _converse.chatboxes.get(jid);
|
||||
this.initDebounced();
|
||||
|
||||
this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged);
|
||||
this.listenTo(this.model, 'change:hidden', () => !this.model.get('hidden') && this.afterShown());
|
||||
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
|
||||
@ -46,9 +43,7 @@ export default class ChatView extends BaseChatView {
|
||||
}
|
||||
|
||||
render () {
|
||||
const result = tpl_chat(Object.assign(
|
||||
this.model.toJSON(), { 'markScrolled': ev => this.markScrolled(ev) })
|
||||
);
|
||||
const result = tpl_chat(this.model.toJSON());
|
||||
render(result, this);
|
||||
this.help_container = this.querySelector('.chat-content__help');
|
||||
return this;
|
||||
|
@ -9,8 +9,7 @@ export default (o) => html`
|
||||
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
|
||||
<converse-chat-content
|
||||
class="chat-content__messages"
|
||||
jid="${o.jid}"
|
||||
@scroll=${o.markScrolled}></converse-chat-content>
|
||||
jid="${o.jid}"></converse-chat-content>
|
||||
|
||||
<div class="chat-content__help"></div>
|
||||
</div>
|
||||
|
@ -11,8 +11,6 @@ class HeadlinesView extends BaseChatView {
|
||||
_converse.chatboxviews.add(jid, this);
|
||||
|
||||
this.model = _converse.chatboxes.get(jid);
|
||||
this.initDebounced();
|
||||
|
||||
this.model.disable_mam = true; // Don't do MAM queries for this box
|
||||
this.listenTo(this.model, 'change:hidden', () => this.afterShown());
|
||||
this.listenTo(this.model, 'destroy', this.remove);
|
||||
|
@ -14,10 +14,11 @@ export async function fetchMessagesOnScrollUp (view) {
|
||||
} else {
|
||||
await fetchArchivedMessages(view.model, { 'end': oldest_message.get('time') });
|
||||
}
|
||||
view.model.ui.set('chat-content-spinner-top', false);
|
||||
if (api.settings.get('allow_url_history_change')) {
|
||||
_converse.router.history.navigate(`#${oldest_message.get('msgid')}`);
|
||||
}
|
||||
|
||||
setTimeout(() => view.model.ui.set('chat-content-spinner-top', false), 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -151,9 +151,6 @@ export function minimize (ev, model) {
|
||||
} else {
|
||||
model = ev;
|
||||
}
|
||||
// save the scroll position to restore it on maximize
|
||||
const view = _converse.chatboxviews.get(model.get('jid'));
|
||||
view.querySelector('.chat-content__messages')?.saveScrollPosition();
|
||||
model.setChatState(_converse.INACTIVE);
|
||||
u.safeSave(model, {
|
||||
'hidden': true,
|
||||
|
@ -3,6 +3,8 @@ import tpl_muc_chatarea from './templates/muc-chatarea.js';
|
||||
import { CustomElement } from 'shared/components/element.js';
|
||||
import { __ } from 'i18n';
|
||||
import { _converse, api, converse } from '@converse/headless/core';
|
||||
import { onScrolledDown } from 'shared/chat/utils.js';
|
||||
import { safeSave } from '@converse/headless/utils/core.js';
|
||||
|
||||
|
||||
const { u } = converse.env;
|
||||
@ -93,17 +95,13 @@ export default class MUCChatArea extends CustomElement {
|
||||
* which debounces this method by 100ms.
|
||||
* @private
|
||||
*/
|
||||
_markScrolled (ev) {
|
||||
_markScrolled () {
|
||||
let scrolled = true;
|
||||
let scrollTop = null;
|
||||
const msgs_container = this.querySelector('.chat-content__messages');
|
||||
const is_at_bottom =
|
||||
msgs_container.scrollTop + msgs_container.clientHeight >= msgs_container.scrollHeight - 62; // sigh...
|
||||
|
||||
const is_at_bottom = this.scrollTop + this.clientHeight >= this.scrollHeight;
|
||||
if (is_at_bottom) {
|
||||
scrolled = false;
|
||||
this.onScrolledDown();
|
||||
} else if (msgs_container.scrollTop === 0) {
|
||||
onScrolledDown(this.model);
|
||||
} else if (this.scrollTop === 0) {
|
||||
/**
|
||||
* Triggered once the chat's message area has been scrolled to the top
|
||||
* @event _converse#chatBoxScrolledUp
|
||||
@ -111,29 +109,8 @@ export default class MUCChatArea extends CustomElement {
|
||||
* @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
|
||||
*/
|
||||
api.trigger('chatBoxScrolledUp', this);
|
||||
} else {
|
||||
scrollTop = ev.target.scrollTop;
|
||||
}
|
||||
u.safeSave(this.model, { scrolled, scrollTop });
|
||||
}
|
||||
|
||||
onScrolledDown () {
|
||||
if (!this.model.isHidden()) {
|
||||
this.model.clearUnreadMsgCounter();
|
||||
if (api.settings.get('allow_url_history_change')) {
|
||||
// Clear location hash if set to one of the messages in our history
|
||||
const hash = window.location.hash;
|
||||
hash && this.model.messages.get(hash.slice(1)) && _converse.router.history.navigate();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Triggered once the chat's message area has been scrolled down to the bottom.
|
||||
* @event _converse#chatBoxScrolledDown
|
||||
* @type {object}
|
||||
* @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
|
||||
* @example _converse.api.listen.on('chatBoxScrolledDown', obj => { ... });
|
||||
*/
|
||||
api.trigger('chatBoxScrolledDown', { 'chatbox': this.model });
|
||||
safeSave(this.model, { scrolled });
|
||||
}
|
||||
|
||||
onMousedown (ev) {
|
||||
|
@ -14,8 +14,6 @@ export default class MUCView extends BaseChatView {
|
||||
const jid = this.getAttribute('jid');
|
||||
this.model = await api.rooms.get(jid);
|
||||
_converse.chatboxviews.add(jid, this);
|
||||
this.initDebounced();
|
||||
|
||||
this.setAttribute('id', this.model.get('box_id'));
|
||||
|
||||
this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged);
|
||||
|
@ -10,8 +10,7 @@ export default (o) => html`
|
||||
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
|
||||
<converse-chat-content
|
||||
class="chat-content__messages"
|
||||
jid="${o.jid}"
|
||||
@scroll=${o.markScrolled}></converse-chat-content>
|
||||
jid="${o.jid}"></converse-chat-content>
|
||||
|
||||
${o.show_help_messages ? html`<div class="chat-content__help">
|
||||
<converse-chat-help
|
||||
|
@ -1,16 +1,12 @@
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import log from '@converse/headless/log';
|
||||
import { ElementView } from '@converse/skeletor/src/element.js';
|
||||
import { _converse, api, converse } from '@converse/headless/core';
|
||||
import { onScrolledDown } from './utils.js';
|
||||
|
||||
const u = converse.env.utils;
|
||||
|
||||
export default class BaseChatView extends ElementView {
|
||||
|
||||
initDebounced () {
|
||||
this.markScrolled = debounce(this._markScrolled, 100);
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
super.disconnectedCallback();
|
||||
const jid = this.getAttribute('jid');
|
||||
@ -93,38 +89,6 @@ export default class BaseChatView extends ElementView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the chat content is scrolled up or down.
|
||||
* We want to record when the user has scrolled away from
|
||||
* the bottom, so that we don't automatically scroll away
|
||||
* from what the user is reading when new messages are received.
|
||||
*
|
||||
* Don't call this method directly, instead, call `markScrolled`,
|
||||
* which debounces this method by 100ms.
|
||||
* @private
|
||||
*/
|
||||
_markScrolled (ev) {
|
||||
let scrolled = true;
|
||||
let scrollTop = null;
|
||||
const is_at_bottom = ev.target.scrollTop + ev.target.clientHeight >= ev.target.scrollHeight - 62; // sigh...
|
||||
if (is_at_bottom) {
|
||||
scrolled = false;
|
||||
this.onScrolledDown();
|
||||
} else if (ev.target.scrollTop === 0) {
|
||||
scrollTop = ev.target.scrollTop;
|
||||
/**
|
||||
* Triggered once the chat's message area has been scrolled to the top
|
||||
* @event _converse#chatBoxScrolledUp
|
||||
* @property { _converse.ChatBoxView | _converse.ChatRoomView } view
|
||||
* @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
|
||||
*/
|
||||
api.trigger('chatBoxScrolledUp', this);
|
||||
} else {
|
||||
scrollTop = ev.target.scrollTop;
|
||||
}
|
||||
u.safeSave(this.model, { scrolled, scrollTop });
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the chat down.
|
||||
*
|
||||
@ -136,23 +100,9 @@ export default class BaseChatView extends ElementView {
|
||||
ev?.preventDefault?.();
|
||||
ev?.stopPropagation?.();
|
||||
if (this.model.get('scrolled')) {
|
||||
u.safeSave(this.model, {
|
||||
'scrolled': false,
|
||||
'scrollTop': null
|
||||
});
|
||||
}
|
||||
this.onScrolledDown();
|
||||
}
|
||||
|
||||
onScrolledDown () {
|
||||
if (!this.model.isHidden()) {
|
||||
this.model.clearUnreadMsgCounter();
|
||||
if (api.settings.get('allow_url_history_change')) {
|
||||
// Clear location hash if set to one of the messages in our history
|
||||
const hash = window.location.hash;
|
||||
hash && this.model.messages.get(hash.slice(1)) && _converse.router.history.navigate();
|
||||
}
|
||||
u.safeSave(this.model, { 'scrolled': false });
|
||||
}
|
||||
onScrolledDown(this.model);
|
||||
}
|
||||
|
||||
onWindowStateChanged (data) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import './message-history';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { CustomElement } from 'shared/components/element.js';
|
||||
import { _converse, api, converse } from '@converse/headless/core';
|
||||
import { _converse, api } from '@converse/headless/core';
|
||||
import { html } from 'lit';
|
||||
import { onScrolledDown } from './utils.js';
|
||||
import { safeSave } from '@converse/headless/utils/core.js';
|
||||
|
||||
const { u } = converse;
|
||||
|
||||
export default class ChatContent extends CustomElement {
|
||||
|
||||
@ -17,6 +18,7 @@ export default class ChatContent extends CustomElement {
|
||||
connectedCallback () {
|
||||
super.connectedCallback();
|
||||
this.debouncedMaintainScroll = debounce(this.maintainScrollPosition, 100);
|
||||
this.markScrolled = debounce(this._markScrolled, 100);
|
||||
|
||||
this.model = _converse.chatboxes.get(this.jid);
|
||||
this.listenTo(this.model, 'change:hidden_occupants', this.requestUpdate);
|
||||
@ -40,6 +42,8 @@ export default class ChatContent extends CustomElement {
|
||||
this.addEventListener('imageLoaded', () => {
|
||||
this.debouncedMaintainScroll(this.was_scrolled_up);
|
||||
});
|
||||
this.addEventListener('scroll', () => this.markScrolled());
|
||||
this.initIntersectionObserver();
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -47,6 +51,7 @@ export default class ChatContent extends CustomElement {
|
||||
${ this.model.ui.get('chat-content-spinner-top') ? html`<span class="spinner fa fa-spinner centered"></span>` : '' }
|
||||
<converse-message-history
|
||||
.model=${this.model}
|
||||
.observer=${this.observer}
|
||||
.messages=${[...this.model.messages.models]}>
|
||||
</converse-message-history>
|
||||
<div class="chat-content__notifications">${this.model.getNotificationsText()}</div>
|
||||
@ -58,19 +63,62 @@ export default class ChatContent extends CustomElement {
|
||||
this.debouncedMaintainScroll();
|
||||
}
|
||||
|
||||
saveScrollPosition () {
|
||||
const scrollTop = this.scrollTop;
|
||||
if (scrollTop) {
|
||||
u.safeSave(this.model, { 'scrolled': true, scrollTop });
|
||||
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.
|
||||
* We want to record when the user has scrolled away from
|
||||
* the bottom, so that we don't automatically scroll away
|
||||
* from what the user is reading when new messages are received.
|
||||
*
|
||||
* Don't call this method directly, instead, call `markScrolled`,
|
||||
* which debounces this method by 100ms.
|
||||
* @private
|
||||
*/
|
||||
_markScrolled () {
|
||||
let scrolled = true;
|
||||
const is_at_bottom = this.scrollTop + this.clientHeight >= this.scrollHeight;
|
||||
if (is_at_bottom) {
|
||||
scrolled = false;
|
||||
onScrolledDown(this.model);
|
||||
} else if (this.scrollTop === 0) {
|
||||
/**
|
||||
* Triggered once the chat's message area has been scrolled to the top
|
||||
* @event _converse#chatBoxScrolledUp
|
||||
* @property { _converse.ChatBoxView | _converse.ChatRoomView } view
|
||||
* @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
|
||||
*/
|
||||
api.trigger('chatBoxScrolledUp', this);
|
||||
}
|
||||
safeSave(this.model, { scrolled });
|
||||
}
|
||||
|
||||
setAnchoredMessage (entries) {
|
||||
if (this.model.ui.get('chat-content-spinner-top')) {
|
||||
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) {
|
||||
const pos = this.model.get('scrollTop');
|
||||
if (pos) {
|
||||
this.scrollTop = pos;
|
||||
}
|
||||
console.warn('scrolling into view');
|
||||
this.anchored_message?.scrollIntoView(true);
|
||||
} else {
|
||||
this.scrollDown();
|
||||
}
|
||||
|
@ -54,16 +54,16 @@ export default class EmojiPickerContent extends CustomElement {
|
||||
sizzle('.emoji-picker', this).forEach(a => this.observer.observe(a));
|
||||
}
|
||||
|
||||
setCategoryOnVisibilityChange (ev) {
|
||||
setCategoryOnVisibilityChange (entries) {
|
||||
const selected = this.parentElement.navigator.selected;
|
||||
const intersection_with_selected = ev.filter(i => i.target.contains(selected)).pop();
|
||||
const intersection_with_selected = entries.filter(i => i.target.contains(selected)).pop();
|
||||
let current;
|
||||
// Choose the intersection that contains the currently selected
|
||||
// element, or otherwise the one with the largest ratio.
|
||||
if (intersection_with_selected) {
|
||||
current = intersection_with_selected;
|
||||
} else {
|
||||
current = ev.reduce((p, c) => c.intersectionRatio >= (p?.intersectionRatio || 0) ? c : p, null);
|
||||
current = entries.reduce((p, c) => c.intersectionRatio >= (p?.intersectionRatio || 0) ? c : p, null);
|
||||
}
|
||||
if (current && current.isIntersecting) {
|
||||
const category = current.target.getAttribute('data-category');
|
||||
|
@ -51,6 +51,7 @@ export default class MessageHistory extends CustomElement {
|
||||
static get properties () {
|
||||
return {
|
||||
model: { type: Object },
|
||||
observer: { type: Object },
|
||||
messages: { type: Array }
|
||||
}
|
||||
}
|
||||
@ -67,6 +68,7 @@ export default class MessageHistory extends CustomElement {
|
||||
const day = getDayIndicator(model);
|
||||
const templates = day ? [day] : [];
|
||||
const message = html`<converse-chat-message
|
||||
.observer=${this.observer}
|
||||
jid="${this.model.get('jid')}"
|
||||
mid="${model.get('id')}"></converse-chat-message>`
|
||||
|
||||
|
@ -24,7 +24,8 @@ export default class Message extends CustomElement {
|
||||
static get properties () {
|
||||
return {
|
||||
jid: { type: String },
|
||||
mid: { type: String }
|
||||
mid: { type: String },
|
||||
observer: { type: Object }
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +60,10 @@ export default class Message extends CustomElement {
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated () {
|
||||
this.observer.observe(this);
|
||||
}
|
||||
|
||||
getProps () {
|
||||
return Object.assign(
|
||||
this.model.toJSON(),
|
||||
|
12
src/shared/chat/utils.js
Normal file
12
src/shared/chat/utils.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { _converse, api } from '@converse/headless/core';
|
||||
|
||||
export function onScrolledDown (model) {
|
||||
if (!model.isHidden()) {
|
||||
model.clearUnreadMsgCounter();
|
||||
if (api.settings.get('allow_url_history_change')) {
|
||||
// Clear location hash if set to one of the messages in our history
|
||||
const hash = window.location.hash;
|
||||
hash && model.messages.get(hash.slice(1)) && _converse.router.history.navigate();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user