From fe3e63d8c5315d7ad42a68062c1734eb3ec8d0de Mon Sep 17 00:00:00 2001 From: JC Brand Date: Thu, 3 Jun 2021 12:10:30 +0200 Subject: [PATCH] Declarative scrolling and rendering new messages indicator - Increment `num_unread` when new messages appear while scrolled up - Set scrolling state in model code (as opposed to view) --- src/headless/plugins/chat/model.js | 8 ++- src/plugins/chatview/bottom-panel.js | 33 +++++----- .../templates/chatbox_message_form.js | 62 +++++++++++-------- src/plugins/chatview/view.js | 1 - src/plugins/muc-views/bottom-panel.js | 1 - src/plugins/muc-views/muc.js | 1 - src/plugins/muc-views/sidebar.js | 2 - src/plugins/muc-views/tests/muc.js | 1 - src/shared/chat/baseview.js | 50 --------------- src/shared/chat/chat-content.js | 37 +++++++++-- src/shared/chat/unfurl.js | 4 +- src/shared/directives/rich-text.js | 2 +- webpack.html | 2 +- 13 files changed, 91 insertions(+), 113 deletions(-) diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js index 674e4f66c..a44439fdb 100644 --- a/src/headless/plugins/chat/model.js +++ b/src/headless/plugins/chat/model.js @@ -1036,7 +1036,13 @@ const ChatBox = ModelWithContact.extend({ return } if (u.isNewMessage(message)) { - if (this.isHidden()) { + if (message.get('sender') === 'me') { + // We remove the "scrolled" flag so that the chat area + // 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')) { const settings = { 'num_unread': this.get('num_unread') + 1 }; diff --git a/src/plugins/chatview/bottom-panel.js b/src/plugins/chatview/bottom-panel.js index a96f95665..8ad426da5 100644 --- a/src/plugins/chatview/bottom-panel.js +++ b/src/plugins/chatview/bottom-panel.js @@ -2,7 +2,7 @@ import tpl_chatbox_message_form from './templates/chatbox_message_form.js'; import tpl_toolbar from './templates/toolbar.js'; import { ElementView } from '@converse/skeletor/src/element.js'; import { __ } from 'i18n'; -import { _converse, api, converse } from "@converse/headless/core"; +import { _converse, api, converse } from '@converse/headless/core'; import { html, render } from 'lit'; import { clearMessages, parseMessageForCommands } from './utils.js'; @@ -11,20 +11,24 @@ import './styles/chat-bottom-panel.scss'; const { u } = converse.env; export default class ChatBottomPanel extends ElementView { - events = { 'click .send-button': 'onFormSubmitted', - 'click .toggle-clear': 'clearMessages', - } + 'click .toggle-clear': 'clearMessages' + }; async connectedCallback () { super.connectedCallback(); this.model = _converse.chatboxes.get(this.getAttribute('jid')); - this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm); + this.listenTo(this.model, 'change', o => this.onModelChanged(o.changed)); await this.model.initialized; this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting); this.render(); - api.listen.on('chatBoxScrolledDown', () => this.hideNewMessagesIndicator()); + } + + onModelChanged (changed) { + if ('composing_spoiler' in changed || 'num_unread' in changed || 'scrolled' in changed) { + this.renderMessageForm(); + } } render () { @@ -36,7 +40,8 @@ export default class ChatBottomPanel extends ElementView { if (!api.settings.get('show_toolbar')) { return this; } - const options = Object.assign({ + const options = Object.assign( + { 'model': this.model, 'chatview': _converse.chatboxviews.get(this.getAttribute('jid')) }, @@ -62,17 +67,12 @@ export default class ChatBottomPanel extends ElementView { 'onDrop': ev => this.onDrop(ev), 'hint_value': this.querySelector('.spoiler-hint')?.value, 'inputChanged': ev => this.inputChanged(ev), - 'label_message': this.model.get('composing_spoiler') ? __('Hidden message') : __('Message'), - 'label_spoiler_hint': __('Optional hint'), 'message_value': this.querySelector('.chat-textarea')?.value, 'onChange': ev => this.updateCharCounter(ev.target.value), 'onKeyDown': ev => this.onKeyDown(ev), 'onKeyUp': ev => this.onKeyUp(ev), 'onPaste': ev => this.onPaste(ev), - 'show_send_button': api.settings.get('show_send_button'), - 'show_toolbar': api.settings.get('show_toolbar'), - 'unread_msgs': __('You have unread messages'), - 'viewUnreadMessages': ev => this.viewUnreadMessages(ev), + 'viewUnreadMessages': ev => this.viewUnreadMessages(ev) }) ), form_container @@ -85,11 +85,6 @@ export default class ChatBottomPanel extends ElementView { viewUnreadMessages (ev) { ev?.preventDefault?.(); this.model.save({ 'scrolled': false, 'scrollTop': null }); - _converse.chatboxviews.get(this.getAttribute('jid'))?.scrollDown(); - } - - hideNewMessagesIndicator () { - this.querySelector('.new-msgs-indicator')?.classList.add('hidden'); } onMessageCorrecting (message) { @@ -244,7 +239,7 @@ export default class ChatBottomPanel extends ElementView { } const ev = document.createEvent('HTMLEvents'); ev.initEvent('change', false, true); - textarea.dispatchEvent(ev) + textarea.dispatchEvent(ev); u.placeCaretAtEnd(textarea); } diff --git a/src/plugins/chatview/templates/chatbox_message_form.js b/src/plugins/chatview/templates/chatbox_message_form.js index ef20ec959..9a9e0fd88 100644 --- a/src/plugins/chatview/templates/chatbox_message_form.js +++ b/src/plugins/chatview/templates/chatbox_message_form.js @@ -1,31 +1,39 @@ +import { __ } from 'i18n'; +import { api } from "@converse/headless/core"; import { html } from "lit"; -export default (o) => html` - - -
- - +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/chatview/view.js b/src/plugins/chatview/view.js index 358996386..31289539d 100644 --- a/src/plugins/chatview/view.js +++ b/src/plugins/chatview/view.js @@ -32,7 +32,6 @@ export default class ChatView extends BaseChatView { this.render(); // Need to be registered after render has been called. - this.listenTo(this.model.messages, 'add', this.onMessageAdded); this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages); await this.model.messages.fetched; diff --git a/src/plugins/muc-views/bottom-panel.js b/src/plugins/muc-views/bottom-panel.js index cc0ef7c60..7dc5385d6 100644 --- a/src/plugins/muc-views/bottom-panel.js +++ b/src/plugins/muc-views/bottom-panel.js @@ -78,7 +78,6 @@ export default class MUCBottomPanel extends BottomPanel { ev?.preventDefault?.(); ev?.stopPropagation?.(); this.model.save({ 'hidden_occupants': true }); - _converse.chatboxviews.get(this.getAttribute('jid'))?.scrollDown(); } onKeyDown (ev) { diff --git a/src/plugins/muc-views/muc.js b/src/plugins/muc-views/muc.js index 0e8bb358b..44b88ab3b 100644 --- a/src/plugins/muc-views/muc.js +++ b/src/plugins/muc-views/muc.js @@ -29,7 +29,6 @@ export default class MUCView extends BaseChatView { await this.render(); // Need to be registered after render has been called. - this.listenTo(this.model.messages, 'add', this.onMessageAdded); this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification); this.updateAfterTransition(); diff --git a/src/plugins/muc-views/sidebar.js b/src/plugins/muc-views/sidebar.js index ec92bcc76..8c45809a0 100644 --- a/src/plugins/muc-views/sidebar.js +++ b/src/plugins/muc-views/sidebar.js @@ -39,8 +39,6 @@ export default class MUCSidebar extends CustomElement { ev?.preventDefault?.(); ev?.stopPropagation?.(); u.safeSave(this.model, { 'hidden_occupants': true }); - // FIXME: do this declaratively - _converse.chatboxviews.get(this.jid)?.scrollDown(); } onOccupantClicked (ev) { diff --git a/src/plugins/muc-views/tests/muc.js b/src/plugins/muc-views/tests/muc.js index 46c4ed56b..593bf4664 100644 --- a/src/plugins/muc-views/tests/muc.js +++ b/src/plugins/muc-views/tests/muc.js @@ -1934,7 +1934,6 @@ describe("Groupchats", function () { const message = 'This message is received while the chat area is scrolled up'; await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); const view = _converse.chatboxviews.get('lounge@montague.lit'); - spyOn(view, 'scrollDown').and.callThrough(); // Create enough messages so that there's a scrollbar. const promises = []; for (let i=0; i<20; i++) { diff --git a/src/shared/chat/baseview.js b/src/shared/chat/baseview.js index 48f532df5..80181376f 100644 --- a/src/shared/chat/baseview.js +++ b/src/shared/chat/baseview.js @@ -9,7 +9,6 @@ export default class BaseChatView extends ElementView { initDebounced () { this.markScrolled = debounce(this._markScrolled, 100); - this.debouncedScrollDown = debounce(this.scrollDown, 100); } disconnectedCallback () { @@ -18,13 +17,6 @@ export default class BaseChatView extends ElementView { _converse.chatboxviews.remove(jid, this); } - hideNewMessagesIndicator () { - const new_msgs_indicator = this.querySelector('.new-msgs-indicator'); - if (new_msgs_indicator !== null) { - new_msgs_indicator.classList.add('hidden'); - } - } - maybeFocus () { api.settings.get('auto_focus') && this.focus(); } @@ -77,20 +69,6 @@ export default class BaseChatView extends ElementView { api.trigger('chatBoxFocused', this, ev); } - /** - * Scroll to the previously saved scrollTop position, or scroll - * down if it wasn't set. - */ - maintainScrollTop () { - const pos = this.model.get('scrollTop'); - if (pos) { - const msgs_container = this.querySelector('.chat-content__messages'); - msgs_container.scrollTop = pos; - } else { - this.scrollDown(); - } - } - onStatusMessageChanged (item) { this.renderHeading(); /** @@ -107,24 +85,6 @@ export default class BaseChatView extends ElementView { }); } - showNewMessagesIndicator () { - u.showElement(this.querySelector('.new-msgs-indicator')); - } - - onMessageAdded (message) { - if (u.isNewMessage(message)) { - if (message.get('sender') === 'me') { - // We remove the "scrolled" flag so that the chat area - // 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.model.get('scrolled', true)) { - this.showNewMessagesIndicator(); - } - } - } - getBottomPanel () { if (this.model.get('type') === _converse.CHATROOMS_TYPE) { return this.querySelector('converse-muc-bottom-panel'); @@ -183,12 +143,10 @@ export default class BaseChatView extends ElementView { 'scrollTop': null }); } - this.querySelector('.chat-content__messages')?.scrollDown(); this.onScrolledDown(); } onScrolledDown () { - this.hideNewMessagesIndicator(); if (!this.model.isHidden()) { this.model.clearUnreadMsgCounter(); if (api.settings.get('allow_url_history_change')) { @@ -197,14 +155,6 @@ export default class BaseChatView extends ElementView { 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 }); // TODO: clean up } onWindowStateChanged (data) { diff --git a/src/shared/chat/chat-content.js b/src/shared/chat/chat-content.js index 85dd829af..86a75a7c5 100644 --- a/src/shared/chat/chat-content.js +++ b/src/shared/chat/chat-content.js @@ -1,7 +1,7 @@ -import "./message-history"; -import debounce from 'lodash-es/debounce'; +import './message-history'; +import debounce from 'lodash/debounce'; 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'; export default class ChatContent extends CustomElement { @@ -14,14 +14,19 @@ export default class ChatContent extends CustomElement { connectedCallback () { super.connectedCallback(); - this.debouncedScrolldown = debounce(this.scrollDown, 100); + this.debouncedMaintainScroll = debounce(this.maintainScrollPosition, 100); + this.model = _converse.chatboxes.get(this.jid); + this.listenTo(this.model, 'change:hidden_occupants', this.requestUpdate); + this.listenTo(this.model, 'change:scrolled', this.requestUpdate); this.listenTo(this.model.messages, 'add', this.requestUpdate); this.listenTo(this.model.messages, 'change', this.requestUpdate); this.listenTo(this.model.messages, 'remove', this.requestUpdate); + this.listenTo(this.model.messages, 'rendered', this.requestUpdate); this.listenTo(this.model.messages, 'reset', this.requestUpdate); this.listenTo(this.model.notifications, 'change', this.requestUpdate); this.listenTo(this.model.ui, 'change', this.requestUpdate); + if (this.model.occupants) { this.listenTo(this.model.occupants, 'change', this.requestUpdate); } @@ -31,7 +36,7 @@ export default class ChatContent extends CustomElement { // didn't initiate the scrolling. this.was_scrolled_up = this.model.get('scrolled'); this.addEventListener('imageLoaded', () => { - !this.was_scrolled_up && this.scrollDown(); + this.debouncedMaintainScroll(this.was_scrolled_up); }); } @@ -47,7 +52,19 @@ export default class ChatContent extends CustomElement { } updated () { - !this.model.get('scrolled') && this.debouncedScrolldown(); + this.was_scrolled_up = this.model.get('scrolled'); + this.debouncedMaintainScroll(); + } + + maintainScrollPosition () { + if (this.was_scrolled_up) { + const pos = this.model.get('scrollTop'); + if (pos) { + this.scrollTop = pos; + } + } else { + this.scrollDown(); + } } scrollDown () { @@ -57,6 +74,14 @@ export default class ChatContent extends CustomElement { } else { this.scrollTop = this.scrollHeight; } + /** + * Triggered once the converse-chat-content element 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 }); } } diff --git a/src/shared/chat/unfurl.js b/src/shared/chat/unfurl.js index f9f310fdf..d4ecb9646 100644 --- a/src/shared/chat/unfurl.js +++ b/src/shared/chat/unfurl.js @@ -1,5 +1,5 @@ import { CustomElement } from 'shared/components/element.js'; -import { _converse, api } from "@converse/headless/core"; +import { api } from "@converse/headless/core"; import tpl_unfurl from './templates/unfurl.js'; import './styles/unfurl.scss'; @@ -29,7 +29,7 @@ export default class MessageUnfurl extends CustomElement { } onImageLoad () { - _converse.chatboxviews.get(this.getAttribute('jid'))?.scrollDown(); + this.dispatchEvent(new CustomEvent('imageLoaded', { detail: this, 'bubbles': true })); } } diff --git a/src/shared/directives/rich-text.js b/src/shared/directives/rich-text.js index ee4f5a8fb..56e74dc56 100644 --- a/src/shared/directives/rich-text.js +++ b/src/shared/directives/rich-text.js @@ -28,7 +28,7 @@ class RichTextRenderer { class RichTextDirective extends Directive { render (text, offset, mentions, options, callback) { // eslint-disable-line class-methods-use-this const renderer = new RichTextRenderer(text, offset, mentions, options); - const result =renderer.render(); + const result = renderer.render(); callback?.(); return result; } diff --git a/webpack.html b/webpack.html index 4de397fa2..0f79a1cb3 100644 --- a/webpack.html +++ b/webpack.html @@ -30,7 +30,7 @@ modtools_disable_assign: ['owner', 'moderator', 'participant', 'visitor'], modtools_disable_query: ['moderator', 'participant', 'visitor'], enable_smacks: true, - connection_options: { 'worker': '/dist/shared-connection-worker.js' }, + // connection_options: { 'worker': '/dist/shared-connection-worker.js' }, persistent_store: 'IndexedDB', message_archiving: 'always', muc_domain: 'conference.chat.example.org',