From b6f2662ad7a4de3438c794df74a0e6f6442e0c2e Mon Sep 17 00:00:00 2001 From: JC Brand Date: Thu, 17 Jun 2021 10:43:08 +0200 Subject: [PATCH] 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. --- src/headless/plugins/chat/model.js | 14 ++--- src/headless/plugins/muc/muc.js | 6 +-- src/headless/plugins/muc/tests/pruning.js | 6 +-- src/plugins/chatview/bottom-panel.js | 2 +- .../chatview/templates/bottom-panel.js | 2 +- src/plugins/chatview/tests/chatbox.js | 12 ++--- src/plugins/chatview/tests/messages.js | 4 +- src/plugins/minimize/tests/minchats.js | 2 +- src/plugins/muc-views/message-form.js | 3 +- .../muc-views/templates/message-form.js | 3 -- .../muc-views/templates/muc-bottom-panel.js | 5 +- src/plugins/muc-views/tests/muc.js | 4 +- src/shared/chat/baseview.js | 4 +- src/shared/chat/chat-content.js | 52 +++++-------------- src/shared/chat/utils.js | 42 ++++++++++++++- 15 files changed, 87 insertions(+), 74 deletions(-) diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js index ad9ce4a0a..814fa4781 100644 --- a/src/headless/plugins/chat/model.js +++ b/src/headless/plugins/chat/model.js @@ -33,8 +33,8 @@ const ChatBox = ModelWithContact.extend({ 'message_type': 'chat', 'nickname': undefined, 'num_unread': 0, - 'time_sent': (new Date(0)).toISOString(), 'time_opened': this.get('time_opened') || (new Date()).getTime(), + 'time_sent': (new Date(0)).toISOString(), 'type': _converse.PRIVATE_CHAT_TYPE, 'url': '' } @@ -65,7 +65,7 @@ const ChatBox = ModelWithContact.extend({ this.presence.on('change:show', item => this.onPresenceChanged(item)); } 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(); /** @@ -249,7 +249,7 @@ const ChatBox = ModelWithContact.extend({ onMessageAdded (message) { 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) ) { debouncedPruneHistory(this); @@ -331,7 +331,7 @@ const ChatBox = ModelWithContact.extend({ }, onScrolledChanged () { - if (!this.get('scrolled')) { + if (!this.ui.get('scrolled')) { this.clearUnreadMsgCounter(); this.pruneHistoryWhenScrolledDown(); } @@ -1072,8 +1072,8 @@ const ChatBox = ModelWithContact.extend({ // gets scrolled down. We always want to scroll down // when the user writes a message as opposed to when a // message is received. - this.set('scrolled', false); - } else if (this.isHidden() || this.get('scrolled')) { + this.ui.set('scrolled', false); + } else if (this.isHidden() || this.ui.get('scrolled')) { const settings = { 'num_unread': this.get('num_unread') + 1 }; @@ -1095,7 +1095,7 @@ const ChatBox = ModelWithContact.extend({ }, isScrolledUp () { - return this.get('scrolled'); + return this.ui.get('scrolled'); } }); diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js index 02e5dd0dc..988cc62f3 100644 --- a/src/headless/plugins/muc/muc.js +++ b/src/headless/plugins/muc/muc.js @@ -97,8 +97,8 @@ const ChatRoomMixin = { this.on('change:chat_state', this.sendChatState, this); this.on('change:hidden', this.onHiddenChange, this); - this.on('change:scrolled', this.onScrolledChanged, this); this.on('destroy', this.removeHandlers, this); + this.ui.on('change:scrolled', this.onScrolledChanged, this); await this.restoreSession(); this.session.on('change:connection_status', this.onConnectionStatusChanged, this); @@ -2585,8 +2585,8 @@ const ChatRoomMixin = { // gets scrolled down. We always want to scroll down // when the user writes a message as opposed to when a // message is received. - this.model.set('scrolled', false); - } else if (this.isHidden() || this.get('scrolled')) { + this.ui.set('scrolled', false); + } else if (this.isHidden() || this.ui.get('scrolled')) { const settings = { 'num_unread_general': this.get('num_unread_general') + 1 }; diff --git a/src/headless/plugins/muc/tests/pruning.js b/src/headless/plugins/muc/tests/pruning.js index 176649a20..4eaecb7ae 100644 --- a/src/headless/plugins/muc/tests/pruning.js +++ b/src/headless/plugins/muc/tests/pruning.js @@ -12,7 +12,7 @@ describe("A Groupchat Message", function () { const muc_jid = 'lounge@montague.lit'; 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('2nd message'); @@ -25,7 +25,7 @@ describe("A Groupchat Message", function () { await u.waitUntil(() => model.messages.length === 4); await u.waitUntil(() => model.messages.length === 3, 550); - model.set('scrolled', true); + model.ui.set('scrolled', true); model.sendMessage('5th message'); model.sendMessage('6th message'); 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. await new Promise(resolve => setTimeout(resolve, 550)); expect(model.messages.length).toBe(5); - model.set('scrolled', false); + model.ui.set('scrolled', false); await u.waitUntil(() => model.messages.length === 3, 550); // Test incoming messages diff --git a/src/plugins/chatview/bottom-panel.js b/src/plugins/chatview/bottom-panel.js index fc5659c19..5fd685c2a 100644 --- a/src/plugins/chatview/bottom-panel.js +++ b/src/plugins/chatview/bottom-panel.js @@ -41,7 +41,7 @@ export default class ChatBottomPanel extends ElementView { viewUnreadMessages (ev) { ev?.preventDefault?.(); - this.model.save({ 'scrolled': false }); + this.model.ui.set({ 'scrolled': false }); } emitFocused (ev) { diff --git a/src/plugins/chatview/templates/bottom-panel.js b/src/plugins/chatview/templates/bottom-panel.js index a2e885295..f60df5fff 100644 --- a/src/plugins/chatview/templates/bottom-panel.js +++ b/src/plugins/chatview/templates/bottom-panel.js @@ -12,7 +12,7 @@ export default (o) => { const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler; const show_toolbar = api.settings.get('show_toolbar'); return html` - ${ o.model.get('scrolled') && o.model.get('num_unread') ? + ${ o.model.ui.get('scrolled') && o.model.get('num_unread') ? html`
o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼
` : '' } ${api.settings.get('show_toolbar') ? html` sent_stanzas.push(s?.nodeTree ?? s)); - view.model.save('scrolled', true); + view.model.ui.set('scrolled', true); await _converse.handleMessageStanza(msg); await u.waitUntil(() => view.model.messages.length); expect(view.model.get('num_unread')).toBe(1); @@ -1026,7 +1026,7 @@ describe("Chatboxes", function () { const sent_stanzas = []; spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s)); const chatbox = _converse.chatboxes.get(sender_jid); - chatbox.save('scrolled', true); + chatbox.ui.set('scrolled', true); _converse.windowState = 'hidden'; const msg = msgFactory(); _converse.handleMessageStanza(msg); @@ -1075,7 +1075,7 @@ describe("Chatboxes", function () { const sent_stanzas = []; spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s)); const chatbox = _converse.chatboxes.get(sender_jid); - chatbox.save('scrolled', true); + chatbox.ui.set('scrolled', true); _converse.windowState = 'hidden'; const msg = msgFactory(); _converse.handleMessageStanza(msg); @@ -1105,7 +1105,7 @@ describe("Chatboxes", function () { await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500); await mock.openChatBoxFor(_converse, 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'); await _converse.handleMessageStanza(msg); 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 selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`; const select_msgs_indicator = () => sizzle(selector, rosterview).pop(); - chatbox.save('scrolled', true); + chatbox.ui.set('scrolled', true); _converse.handleMessageStanza(msgFactory()); const view = _converse.chatboxviews.get(sender_jid); await u.waitUntil(() => view.model.messages.length); @@ -1211,7 +1211,7 @@ describe("Chatboxes", function () { const msgFactory = () => mock.createChatMessage(_converse, sender_jid, msg); const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator'; const select_msgs_indicator = () => sizzle(selector, rosterview).pop(); - chatbox.save('scrolled', true); + chatbox.ui.set('scrolled', true); _converse.handleMessageStanza(msgFactory()); await u.waitUntil(() => view.model.messages.length); expect(select_msgs_indicator().textContent).toBe('1'); diff --git a/src/plugins/chatview/tests/messages.js b/src/plugins/chatview/tests/messages.js index 444f940e9..667a50bea 100644 --- a/src/plugins/chatview/tests/messages.js +++ b/src/plugins/chatview/tests/messages.js @@ -1197,7 +1197,7 @@ describe("A Chat Message", function () { // Create enough messages so that there's a scrollbar. const promises = []; view.querySelector('.chat-content').scrollTop = 0; - view.model.set('scrolled', true); + view.model.ui.set('scrolled', true); for (let i=0; i<20; i++) { _converse.handleMessageStanza($msg({ @@ -1213,7 +1213,7 @@ describe("A Chat Message", function () { 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); indicator_el.click(); await u.waitUntil(() => !view.querySelector('.new-msgs-indicator')); diff --git a/src/plugins/minimize/tests/minchats.js b/src/plugins/minimize/tests/minchats.js index de7929b85..df8ed22e4 100644 --- a/src/plugins/minimize/tests/minchats.js +++ b/src/plugins/minimize/tests/minchats.js @@ -190,7 +190,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () { const minimized_chats = document.querySelector("converse-minimized-chats") const selectUnreadMsgCount = () => minimized_chats.querySelector('#toggle-minimized-chats .unread-message-count'); const chatbox = _converse.chatboxes.get(sender_jid); - chatbox.save('scrolled', true); + chatbox.ui.set('scrolled', true); _converse.handleMessageStanza(msgFactory()); await u.waitUntil(() => chatbox.messages.length); const view = _converse.chatboxviews.get(sender_jid); diff --git a/src/plugins/muc-views/message-form.js b/src/plugins/muc-views/message-form.js index 0e0aa3784..089d7e365 100644 --- a/src/plugins/muc-views/message-form.js +++ b/src/plugins/muc-views/message-form.js @@ -9,13 +9,14 @@ export default class MUCMessageForm extends MessageForm { toHTML () { return tpl_muc_message_form( Object.assign(this.model.toJSON(), { - 'onDrop': ev => this.onDrop(ev), 'hint_value': this.querySelector('.spoiler-hint')?.value, 'message_value': this.querySelector('.chat-textarea')?.value, 'onChange': ev => this.model.set({'draft': ev.target.value}), + 'onDrop': ev => this.onDrop(ev), 'onKeyDown': ev => this.onKeyDown(ev), 'onKeyUp': ev => this.onKeyUp(ev), 'onPaste': ev => this.onPaste(ev), + 'scrolled': this.model.ui.get('scrolled'), 'viewUnreadMessages': ev => this.viewUnreadMessages(ev) })); } diff --git a/src/plugins/muc-views/templates/message-form.js b/src/plugins/muc-views/templates/message-form.js index 3de032f64..2ac18947e 100644 --- a/src/plugins/muc-views/templates/message-form.js +++ b/src/plugins/muc-views/templates/message-form.js @@ -5,13 +5,10 @@ import { resetElementHeight } from 'plugins/chatview/utils.js'; export default (o) => { - const unread_msgs = __('You have unread messages'); const label_message = o.composing_spoiler ? __('Hidden message') : __('Message'); const label_spoiler_hint = __('Optional hint'); const show_send_button = api.settings.get('show_send_button'); - return html` - ${ (o.scrolled && o.num_unread) ? html`
o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼
` : '' } diff --git a/src/plugins/muc-views/templates/muc-bottom-panel.js b/src/plugins/muc-views/templates/muc-bottom-panel.js index 757f02e4c..5990f58fc 100644 --- a/src/plugins/muc-views/templates/muc-bottom-panel.js +++ b/src/plugins/muc-views/templates/muc-bottom-panel.js @@ -7,6 +7,7 @@ import { html } from "lit"; const tpl_can_edit = (o) => { + const unread_msgs = __('You have unread messages'); const message_limit = api.settings.get('message_limit'); const show_call_button = api.settings.get('visible_toolbar_buttons').call; 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_toolbar = api.settings.get('show_toolbar'); return html` + ${ (o.model.ui.get('scrolled') && o.model.get('num_unread')) ? + html`
o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼
` : '' } ${show_toolbar ? html` { const i18n_not_allowed = __("You're not allowed to send messages in this room"); if (conn_status === converse.ROOMSTATUS.ENTERED) { return html` - ${ o.model.get('scrolled') && o.model.get('num_unread_general') ? + ${ o.model.ui.get('scrolled') && o.model.get('num_unread_general') ? html`
o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼
` : '' } ${(o.can_edit) ? tpl_can_edit(o) : html`${i18n_not_allowed}`}`; } else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) { diff --git a/src/plugins/muc-views/tests/muc.js b/src/plugins/muc-views/tests/muc.js index ea638054d..9b9cebce2 100644 --- a/src/plugins/muc-views/tests/muc.js +++ b/src/plugins/muc-views/tests/muc.js @@ -277,13 +277,13 @@ describe("Groupchats", function () { `); - view.model.save('scrolled', true); // hack + view.model.ui.set('scrolled', true); // hack _converse.connection._dataRecv(mock.createRequest(message)); await u.waitUntil(() => view.model.messages.length); const chat_new_msgs_indicator = await u.waitUntil(() => view.querySelector('.new-msgs-indicator')); 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)); done(); })); diff --git a/src/shared/chat/baseview.js b/src/shared/chat/baseview.js index 41b6d614f..844d65e32 100644 --- a/src/shared/chat/baseview.js +++ b/src/shared/chat/baseview.js @@ -98,8 +98,8 @@ export default class BaseChatView extends CustomElement { scrollDown (ev) { ev?.preventDefault?.(); ev?.stopPropagation?.(); - if (this.model.get('scrolled')) { - u.safeSave(this.model, { 'scrolled': false }); + if (this.model.ui.get('scrolled')) { + this.model.ui.set({ 'scrolled': false }); } onScrolledDown(this.model); } diff --git a/src/shared/chat/chat-content.js b/src/shared/chat/chat-content.js index 1db22813c..e46d1fb80 100644 --- a/src/shared/chat/chat-content.js +++ b/src/shared/chat/chat-content.js @@ -1,10 +1,8 @@ import './message-history'; -import debounce from 'lodash/debounce'; import { CustomElement } from 'shared/components/element.js'; import { _converse, api } from '@converse/headless/core'; import { html } from 'lit'; -import { onScrolledDown } from './utils.js'; -import { safeSave } from '@converse/headless/utils/core.js'; +import { markScrolled } from './utils.js'; import './styles/chat-content.scss'; @@ -19,11 +17,17 @@ export default class ChatContent extends CustomElement { 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.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, 'change', 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.notifications, 'change', this.requestUpdate); this.listenTo(this.model.ui, 'change', this.requestUpdate); + this.listenTo(this.model.ui, 'change:scrolled', this.scrollDown); if (this.model.occupants) { this.listenTo(this.model.occupants, 'change', this.requestUpdate); } - this.addEventListener('scroll', () => this.markScrolled()); + this.addEventListener('scroll', markScrolled); } 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 () { - if (this.model.get('scrolled')) { + if (this.model.ui.get('scrolled')) { return; } if (this.scrollTo) { diff --git a/src/shared/chat/utils.js b/src/shared/chat/utils.js index 652fb162a..39ea4761f 100644 --- a/src/shared/chat/utils.js +++ b/src/shared/chat/utils.js @@ -1,9 +1,10 @@ +import debounce from 'lodash/debounce'; import tpl_new_day from "./templates/new-day.js"; import { _converse, api, converse } from '@converse/headless/core'; const { dayjs } = converse.env; -export function onScrolledDown (model) { +function onScrolledDown (model) { if (!model.isHidden()) { if (api.settings.get('allow_url_history_change')) { // 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 * the passed in message is more than a day later than its predecessor.