2
0
Эх сурвалжийг харах

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)
JC Brand 4 жил өмнө
parent
commit
fe3e63d8c5

+ 7 - 1
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
                 };

+ 14 - 19
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);
     }
 

+ 35 - 27
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`
-    <div class="new-msgs-indicator hidden" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ o.unread_msgs } ▼</div>
-    <form class="setNicknameButtonForm hidden">
-        <input type="submit" class="btn btn-primary" name="join" value="Join"/>
-    </form>
-    <form class="sendXMPPMessage">
-        <span class="chat-toolbar no-text-select"></span>
-        <input type="text" placeholder="${o.label_spoiler_hint || ''}" value="${o.hint_value || ''}" class="${o.composing_spoiler ? '' : 'hidden'} spoiler-hint"/>
+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');
 
-        <div class="suggestion-box">
-            <ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
-            <textarea
-                autofocus
-                type="text"
-                @drop=${o.onDrop}
-                @input=${o.inputChanged}
-                @keydown=${o.onKeyDown}
-                @keyup=${o.onKeyUp}
-                @paste=${o.onPaste}
-                @change=${o.onChange}
-                class="chat-textarea suggestion-box__input
-                    ${ o.show_send_button ? 'chat-textarea-send-button' : '' }
-                    ${ o.composing_spoile ? 'spoiler' : '' }"
-                placeholder="${o.label_message}">${ o.message_value || '' }</textarea>
-            <span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
-        </div>
-    </form>
-`;
+    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">
+            <input type="submit" class="btn btn-primary" name="join" value="Join"/>
+        </form>
+        <form class="sendXMPPMessage">
+            <span class="chat-toolbar no-text-select"></span>
+            <input type="text" placeholder="${label_spoiler_hint || ''}" value="${o.hint_value || ''}" class="${o.composing_spoiler ? '' : 'hidden'} spoiler-hint"/>
+
+            <div class="suggestion-box">
+                <ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
+                <textarea
+                    autofocus
+                    type="text"
+                    @drop=${o.onDrop}
+                    @input=${o.inputChanged}
+                    @keydown=${o.onKeyDown}
+                    @keyup=${o.onKeyUp}
+                    @paste=${o.onPaste}
+                    @change=${o.onChange}
+                    class="chat-textarea suggestion-box__input
+                        ${ show_send_button ? 'chat-textarea-send-button' : '' }
+                        ${ o.composing_spoiler ? 'spoiler' : '' }"
+                    placeholder="${label_message}">${ o.message_value || '' }</textarea>
+                <span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
+            </div>
+        </form>`;
+}

+ 0 - 1
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;

+ 0 - 1
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) {

+ 0 - 1
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();

+ 0 - 2
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) {

+ 0 - 1
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++) {

+ 0 - 50
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) {

+ 31 - 6
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 });
     }
 }
 

+ 2 - 2
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 }));
     }
 }
 

+ 1 - 1
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;
     }

+ 1 - 1
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',