Set 'scrolled' flag on model.ui

This prevents it from being persisted across page loads and makes more
sense logically.

Also move markScrolled to utils and MUC unread messages indicator to bottom panel.
This commit is contained in:
JC Brand 2021-06-17 10:43:08 +02:00
parent 5ea9564cc3
commit b6f2662ad7
15 changed files with 87 additions and 74 deletions

View File

@ -33,8 +33,8 @@ const ChatBox = ModelWithContact.extend({
'message_type': 'chat', 'message_type': 'chat',
'nickname': undefined, 'nickname': undefined,
'num_unread': 0, 'num_unread': 0,
'time_sent': (new Date(0)).toISOString(),
'time_opened': this.get('time_opened') || (new Date()).getTime(), 'time_opened': this.get('time_opened') || (new Date()).getTime(),
'time_sent': (new Date(0)).toISOString(),
'type': _converse.PRIVATE_CHAT_TYPE, 'type': _converse.PRIVATE_CHAT_TYPE,
'url': '' 'url': ''
} }
@ -65,7 +65,7 @@ const ChatBox = ModelWithContact.extend({
this.presence.on('change:show', item => this.onPresenceChanged(item)); this.presence.on('change:show', item => this.onPresenceChanged(item));
} }
this.on('change:chat_state', this.sendChatState, this); this.on('change:chat_state', this.sendChatState, this);
this.on('change:scrolled', this.onScrolledChanged, this); this.ui.on('change:scrolled', this.onScrolledChanged, this);
await this.fetchMessages(); await this.fetchMessages();
/** /**
@ -249,7 +249,7 @@ const ChatBox = ModelWithContact.extend({
onMessageAdded (message) { onMessageAdded (message) {
if (api.settings.get('prune_messages_above') && if (api.settings.get('prune_messages_above') &&
(api.settings.get('pruning_behavior') === 'scrolled' || !this.get('scrolled')) && (api.settings.get('pruning_behavior') === 'scrolled' || !this.ui.get('scrolled')) &&
!u.isEmptyMessage(message) !u.isEmptyMessage(message)
) { ) {
debouncedPruneHistory(this); debouncedPruneHistory(this);
@ -331,7 +331,7 @@ const ChatBox = ModelWithContact.extend({
}, },
onScrolledChanged () { onScrolledChanged () {
if (!this.get('scrolled')) { if (!this.ui.get('scrolled')) {
this.clearUnreadMsgCounter(); this.clearUnreadMsgCounter();
this.pruneHistoryWhenScrolledDown(); this.pruneHistoryWhenScrolledDown();
} }
@ -1072,8 +1072,8 @@ const ChatBox = ModelWithContact.extend({
// gets scrolled down. We always want to scroll down // gets scrolled down. We always want to scroll down
// when the user writes a message as opposed to when a // when the user writes a message as opposed to when a
// message is received. // message is received.
this.set('scrolled', false); this.ui.set('scrolled', false);
} else if (this.isHidden() || this.get('scrolled')) { } else if (this.isHidden() || this.ui.get('scrolled')) {
const settings = { const settings = {
'num_unread': this.get('num_unread') + 1 'num_unread': this.get('num_unread') + 1
}; };
@ -1095,7 +1095,7 @@ const ChatBox = ModelWithContact.extend({
}, },
isScrolledUp () { isScrolledUp () {
return this.get('scrolled'); return this.ui.get('scrolled');
} }
}); });

View File

@ -97,8 +97,8 @@ const ChatRoomMixin = {
this.on('change:chat_state', this.sendChatState, this); this.on('change:chat_state', this.sendChatState, this);
this.on('change:hidden', this.onHiddenChange, this); this.on('change:hidden', this.onHiddenChange, this);
this.on('change:scrolled', this.onScrolledChanged, this);
this.on('destroy', this.removeHandlers, this); this.on('destroy', this.removeHandlers, this);
this.ui.on('change:scrolled', this.onScrolledChanged, this);
await this.restoreSession(); await this.restoreSession();
this.session.on('change:connection_status', this.onConnectionStatusChanged, this); this.session.on('change:connection_status', this.onConnectionStatusChanged, this);
@ -2585,8 +2585,8 @@ const ChatRoomMixin = {
// gets scrolled down. We always want to scroll down // gets scrolled down. We always want to scroll down
// when the user writes a message as opposed to when a // when the user writes a message as opposed to when a
// message is received. // message is received.
this.model.set('scrolled', false); this.ui.set('scrolled', false);
} else if (this.isHidden() || this.get('scrolled')) { } else if (this.isHidden() || this.ui.get('scrolled')) {
const settings = { const settings = {
'num_unread_general': this.get('num_unread_general') + 1 'num_unread_general': this.get('num_unread_general') + 1
}; };

View File

@ -12,7 +12,7 @@ describe("A Groupchat Message", function () {
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
expect(model.get('scrolled')).toBeFalsy(); expect(model.ui.get('scrolled')).toBeFalsy();
model.sendMessage('1st message'); model.sendMessage('1st message');
model.sendMessage('2nd message'); model.sendMessage('2nd message');
@ -25,7 +25,7 @@ describe("A Groupchat Message", function () {
await u.waitUntil(() => model.messages.length === 4); await u.waitUntil(() => model.messages.length === 4);
await u.waitUntil(() => model.messages.length === 3, 550); await u.waitUntil(() => model.messages.length === 3, 550);
model.set('scrolled', true); model.ui.set('scrolled', true);
model.sendMessage('5th message'); model.sendMessage('5th message');
model.sendMessage('6th message'); model.sendMessage('6th message');
await u.waitUntil(() => model.messages.length === 5); await u.waitUntil(() => model.messages.length === 5);
@ -33,7 +33,7 @@ describe("A Groupchat Message", function () {
// Wait long enough to be sure the debounced pruneHistory method didn't fire. // Wait long enough to be sure the debounced pruneHistory method didn't fire.
await new Promise(resolve => setTimeout(resolve, 550)); await new Promise(resolve => setTimeout(resolve, 550));
expect(model.messages.length).toBe(5); expect(model.messages.length).toBe(5);
model.set('scrolled', false); model.ui.set('scrolled', false);
await u.waitUntil(() => model.messages.length === 3, 550); await u.waitUntil(() => model.messages.length === 3, 550);
// Test incoming messages // Test incoming messages

View File

@ -41,7 +41,7 @@ export default class ChatBottomPanel extends ElementView {
viewUnreadMessages (ev) { viewUnreadMessages (ev) {
ev?.preventDefault?.(); ev?.preventDefault?.();
this.model.save({ 'scrolled': false }); this.model.ui.set({ 'scrolled': false });
} }
emitFocused (ev) { emitFocused (ev) {

View File

@ -12,7 +12,7 @@ export default (o) => {
const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler; const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler;
const show_toolbar = api.settings.get('show_toolbar'); const show_toolbar = api.settings.get('show_toolbar');
return html` return html`
${ o.model.get('scrolled') && o.model.get('num_unread') ? ${ o.model.ui.get('scrolled') && o.model.get('num_unread') ?
html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' } html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
${api.settings.get('show_toolbar') ? html` ${api.settings.get('show_toolbar') ? html`
<converse-chat-toolbar <converse-chat-toolbar

View File

@ -964,7 +964,7 @@ describe("Chatboxes", function () {
const view = await mock.openChatBoxFor(_converse, sender_jid) const view = await mock.openChatBoxFor(_converse, sender_jid)
const sent_stanzas = []; const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s)); spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
view.model.save('scrolled', true); view.model.ui.set('scrolled', true);
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length); await u.waitUntil(() => view.model.messages.length);
expect(view.model.get('num_unread')).toBe(1); expect(view.model.get('num_unread')).toBe(1);
@ -1026,7 +1026,7 @@ describe("Chatboxes", function () {
const sent_stanzas = []; const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s)); spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid); const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true); chatbox.ui.set('scrolled', true);
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
const msg = msgFactory(); const msg = msgFactory();
_converse.handleMessageStanza(msg); _converse.handleMessageStanza(msg);
@ -1075,7 +1075,7 @@ describe("Chatboxes", function () {
const sent_stanzas = []; const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s)); spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid); const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true); chatbox.ui.set('scrolled', true);
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
const msg = msgFactory(); const msg = msgFactory();
_converse.handleMessageStanza(msg); _converse.handleMessageStanza(msg);
@ -1105,7 +1105,7 @@ describe("Chatboxes", function () {
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500); await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
await mock.openChatBoxFor(_converse, sender_jid); await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid); const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true); chatbox.ui.set('scrolled', true);
msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length); await u.waitUntil(() => chatbox.messages.length);
@ -1186,7 +1186,7 @@ describe("Chatboxes", function () {
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read'); const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
const selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`; const selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`;
const select_msgs_indicator = () => sizzle(selector, rosterview).pop(); const select_msgs_indicator = () => sizzle(selector, rosterview).pop();
chatbox.save('scrolled', true); chatbox.ui.set('scrolled', true);
_converse.handleMessageStanza(msgFactory()); _converse.handleMessageStanza(msgFactory());
const view = _converse.chatboxviews.get(sender_jid); const view = _converse.chatboxviews.get(sender_jid);
await u.waitUntil(() => view.model.messages.length); await u.waitUntil(() => view.model.messages.length);
@ -1211,7 +1211,7 @@ describe("Chatboxes", function () {
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, msg); const msgFactory = () => mock.createChatMessage(_converse, sender_jid, msg);
const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator'; const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
const select_msgs_indicator = () => sizzle(selector, rosterview).pop(); const select_msgs_indicator = () => sizzle(selector, rosterview).pop();
chatbox.save('scrolled', true); chatbox.ui.set('scrolled', true);
_converse.handleMessageStanza(msgFactory()); _converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => view.model.messages.length); await u.waitUntil(() => view.model.messages.length);
expect(select_msgs_indicator().textContent).toBe('1'); expect(select_msgs_indicator().textContent).toBe('1');

View File

@ -1197,7 +1197,7 @@ describe("A Chat Message", function () {
// Create enough messages so that there's a scrollbar. // Create enough messages so that there's a scrollbar.
const promises = []; const promises = [];
view.querySelector('.chat-content').scrollTop = 0; view.querySelector('.chat-content').scrollTop = 0;
view.model.set('scrolled', true); view.model.ui.set('scrolled', true);
for (let i=0; i<20; i++) { for (let i=0; i<20; i++) {
_converse.handleMessageStanza($msg({ _converse.handleMessageStanza($msg({
@ -1213,7 +1213,7 @@ describe("A Chat Message", function () {
const indicator_el = await u.waitUntil(() => view.querySelector('.new-msgs-indicator')); const indicator_el = await u.waitUntil(() => view.querySelector('.new-msgs-indicator'));
expect(view.model.get('scrolled')).toBe(true); expect(view.model.ui.get('scrolled')).toBe(true);
expect(view.querySelector('.chat-content').scrollTop).toBe(0); expect(view.querySelector('.chat-content').scrollTop).toBe(0);
indicator_el.click(); indicator_el.click();
await u.waitUntil(() => !view.querySelector('.new-msgs-indicator')); await u.waitUntil(() => !view.querySelector('.new-msgs-indicator'));

View File

@ -190,7 +190,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
const minimized_chats = document.querySelector("converse-minimized-chats") const minimized_chats = document.querySelector("converse-minimized-chats")
const selectUnreadMsgCount = () => minimized_chats.querySelector('#toggle-minimized-chats .unread-message-count'); const selectUnreadMsgCount = () => minimized_chats.querySelector('#toggle-minimized-chats .unread-message-count');
const chatbox = _converse.chatboxes.get(sender_jid); const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true); chatbox.ui.set('scrolled', true);
_converse.handleMessageStanza(msgFactory()); _converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length); await u.waitUntil(() => chatbox.messages.length);
const view = _converse.chatboxviews.get(sender_jid); const view = _converse.chatboxviews.get(sender_jid);

View File

@ -9,13 +9,14 @@ export default class MUCMessageForm extends MessageForm {
toHTML () { toHTML () {
return tpl_muc_message_form( return tpl_muc_message_form(
Object.assign(this.model.toJSON(), { Object.assign(this.model.toJSON(), {
'onDrop': ev => this.onDrop(ev),
'hint_value': this.querySelector('.spoiler-hint')?.value, 'hint_value': this.querySelector('.spoiler-hint')?.value,
'message_value': this.querySelector('.chat-textarea')?.value, 'message_value': this.querySelector('.chat-textarea')?.value,
'onChange': ev => this.model.set({'draft': ev.target.value}), 'onChange': ev => this.model.set({'draft': ev.target.value}),
'onDrop': ev => this.onDrop(ev),
'onKeyDown': ev => this.onKeyDown(ev), 'onKeyDown': ev => this.onKeyDown(ev),
'onKeyUp': ev => this.onKeyUp(ev), 'onKeyUp': ev => this.onKeyUp(ev),
'onPaste': ev => this.onPaste(ev), 'onPaste': ev => this.onPaste(ev),
'scrolled': this.model.ui.get('scrolled'),
'viewUnreadMessages': ev => this.viewUnreadMessages(ev) 'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
})); }));
} }

View File

@ -5,13 +5,10 @@ import { resetElementHeight } from 'plugins/chatview/utils.js';
export default (o) => { export default (o) => {
const unread_msgs = __('You have unread messages');
const label_message = o.composing_spoiler ? __('Hidden message') : __('Message'); const label_message = o.composing_spoiler ? __('Hidden message') : __('Message');
const label_spoiler_hint = __('Optional hint'); const label_spoiler_hint = __('Optional hint');
const show_send_button = api.settings.get('show_send_button'); const show_send_button = api.settings.get('show_send_button');
return html` return html`
${ (o.scrolled && o.num_unread) ? html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
<form class="setNicknameButtonForm hidden"> <form class="setNicknameButtonForm hidden">
<input type="submit" class="btn btn-primary" name="join" value="Join"/> <input type="submit" class="btn btn-primary" name="join" value="Join"/>
</form> </form>

View File

@ -7,6 +7,7 @@ import { html } from "lit";
const tpl_can_edit = (o) => { const tpl_can_edit = (o) => {
const unread_msgs = __('You have unread messages');
const message_limit = api.settings.get('message_limit'); const message_limit = api.settings.get('message_limit');
const show_call_button = api.settings.get('visible_toolbar_buttons').call; const show_call_button = api.settings.get('visible_toolbar_buttons').call;
const show_emoji_button = api.settings.get('visible_toolbar_buttons').emoji; const show_emoji_button = api.settings.get('visible_toolbar_buttons').emoji;
@ -14,6 +15,8 @@ const tpl_can_edit = (o) => {
const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler; const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler;
const show_toolbar = api.settings.get('show_toolbar'); const show_toolbar = api.settings.get('show_toolbar');
return html` return html`
${ (o.model.ui.get('scrolled') && o.model.get('num_unread')) ?
html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
${show_toolbar ? html` ${show_toolbar ? html`
<converse-chat-toolbar <converse-chat-toolbar
class="chat-toolbar no-text-select" class="chat-toolbar no-text-select"
@ -38,7 +41,7 @@ export default (o) => {
const i18n_not_allowed = __("You're not allowed to send messages in this room"); const i18n_not_allowed = __("You're not allowed to send messages in this room");
if (conn_status === converse.ROOMSTATUS.ENTERED) { if (conn_status === converse.ROOMSTATUS.ENTERED) {
return html` return html`
${ o.model.get('scrolled') && o.model.get('num_unread_general') ? ${ o.model.ui.get('scrolled') && o.model.get('num_unread_general') ?
html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' } html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
${(o.can_edit) ? tpl_can_edit(o) : html`<span class="muc-bottom-panel muc-bottom-panel--muted">${i18n_not_allowed}</span>`}`; ${(o.can_edit) ? tpl_can_edit(o) : html`<span class="muc-bottom-panel muc-bottom-panel--muted">${i18n_not_allowed}</span>`}`;
} else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) { } else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) {

View File

@ -277,13 +277,13 @@ describe("Groupchats", function () {
<delay xmlns="urn:xmpp:delay" stamp="2020-07-14T17:46:47Z" from="juliet@shakespeare.lit"/> <delay xmlns="urn:xmpp:delay" stamp="2020-07-14T17:46:47Z" from="juliet@shakespeare.lit"/>
</message>`); </message>`);
view.model.save('scrolled', true); // hack view.model.ui.set('scrolled', true); // hack
_converse.connection._dataRecv(mock.createRequest(message)); _converse.connection._dataRecv(mock.createRequest(message));
await u.waitUntil(() => view.model.messages.length); await u.waitUntil(() => view.model.messages.length);
const chat_new_msgs_indicator = await u.waitUntil(() => view.querySelector('.new-msgs-indicator')); const chat_new_msgs_indicator = await u.waitUntil(() => view.querySelector('.new-msgs-indicator'));
chat_new_msgs_indicator.click(); chat_new_msgs_indicator.click();
expect(view.model.get('scrolled')).toBeFalsy(); expect(view.model.ui.get('scrolled')).toBeFalsy();
await u.waitUntil(() => !u.isVisible(chat_new_msgs_indicator)); await u.waitUntil(() => !u.isVisible(chat_new_msgs_indicator));
done(); done();
})); }));

View File

@ -98,8 +98,8 @@ export default class BaseChatView extends CustomElement {
scrollDown (ev) { scrollDown (ev) {
ev?.preventDefault?.(); ev?.preventDefault?.();
ev?.stopPropagation?.(); ev?.stopPropagation?.();
if (this.model.get('scrolled')) { if (this.model.ui.get('scrolled')) {
u.safeSave(this.model, { 'scrolled': false }); this.model.ui.set({ 'scrolled': false });
} }
onScrolledDown(this.model); onScrolledDown(this.model);
} }

View File

@ -1,10 +1,8 @@
import './message-history'; import './message-history';
import debounce from 'lodash/debounce';
import { CustomElement } from 'shared/components/element.js'; import { CustomElement } from 'shared/components/element.js';
import { _converse, api } from '@converse/headless/core'; import { _converse, api } from '@converse/headless/core';
import { html } from 'lit'; import { html } from 'lit';
import { onScrolledDown } from './utils.js'; import { markScrolled } from './utils.js';
import { safeSave } from '@converse/headless/utils/core.js';
import './styles/chat-content.scss'; import './styles/chat-content.scss';
@ -19,11 +17,17 @@ export default class ChatContent extends CustomElement {
connectedCallback () { connectedCallback () {
super.connectedCallback(); super.connectedCallback();
this.markScrolled = debounce(this._markScrolled, 50); this.initialize();
}
disconnectedCallback () {
super.disconnectedCallback();
this.removeEventListener('scroll', markScrolled);
}
initialize () {
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.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);
@ -31,11 +35,12 @@ export default class ChatContent extends CustomElement {
this.listenTo(this.model.messages, 'reset', this.requestUpdate); this.listenTo(this.model.messages, 'reset', this.requestUpdate);
this.listenTo(this.model.notifications, 'change', this.requestUpdate); this.listenTo(this.model.notifications, 'change', this.requestUpdate);
this.listenTo(this.model.ui, 'change', this.requestUpdate); this.listenTo(this.model.ui, 'change', this.requestUpdate);
this.listenTo(this.model.ui, 'change:scrolled', this.scrollDown);
if (this.model.occupants) { if (this.model.occupants) {
this.listenTo(this.model.occupants, 'change', this.requestUpdate); this.listenTo(this.model.occupants, 'change', this.requestUpdate);
} }
this.addEventListener('scroll', () => this.markScrolled()); this.addEventListener('scroll', markScrolled);
} }
render () { render () {
@ -51,41 +56,8 @@ export default class ChatContent extends CustomElement {
`; `;
} }
/**
* 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 === 0;
const is_at_top =
Math.ceil(this.clientHeight-this.scrollTop) >= (this.scrollHeight-Math.ceil(this.scrollHeight/20));
if (is_at_bottom) {
scrolled = false;
onScrolledDown(this.model);
} else if (is_at_top) {
/**
* 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);
}
if (this.model.get('scolled') !== scrolled) {
safeSave(this.model, { scrolled });
}
}
scrollDown () { scrollDown () {
if (this.model.get('scrolled')) { if (this.model.ui.get('scrolled')) {
return; return;
} }
if (this.scrollTo) { if (this.scrollTo) {

View File

@ -1,9 +1,10 @@
import debounce from 'lodash/debounce';
import tpl_new_day from "./templates/new-day.js"; import tpl_new_day from "./templates/new-day.js";
import { _converse, api, converse } from '@converse/headless/core'; import { _converse, api, converse } from '@converse/headless/core';
const { dayjs } = converse.env; const { dayjs } = converse.env;
export function onScrolledDown (model) { function onScrolledDown (model) {
if (!model.isHidden()) { if (!model.isHidden()) {
if (api.settings.get('allow_url_history_change')) { if (api.settings.get('allow_url_history_change')) {
// Clear location hash if set to one of the messages in our history // Clear location hash if set to one of the messages in our history
@ -13,6 +14,45 @@ export function onScrolledDown (model) {
} }
} }
/**
* 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.
*/
function _markScrolled (ev) {
const el = ev.target;
if (el.nodeName.toLowerCase() !== 'converse-chat-content') {
return;
}
let scrolled = true;
const is_at_bottom = Math.floor(el.scrollTop) === 0;
const is_at_top =
Math.ceil(el.clientHeight-el.scrollTop) >= (el.scrollHeight-Math.ceil(el.scrollHeight/20));
if (is_at_bottom) {
scrolled = false;
onScrolledDown(el.model);
} else if (is_at_top) {
/**
* 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', el);
}
if (el.model.get('scolled') !== scrolled) {
el.model.ui.set({ scrolled });
}
}
export const markScrolled = debounce((ev) => _markScrolled(ev), 50);
/** /**
* Given a message object, returns a TemplateResult indicating a new day if * Given a message object, returns a TemplateResult indicating a new day if
* the passed in message is more than a day later than its predecessor. * the passed in message is more than a day later than its predecessor.