瀏覽代碼

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.
JC Brand 4 年之前
父節點
當前提交
b6f2662ad7

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

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

+ 3 - 3
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

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

+ 1 - 1
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`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
         ${api.settings.get('show_toolbar') ? html`
             <converse-chat-toolbar

+ 6 - 6
src/plugins/chatview/tests/chatbox.js

@@ -964,7 +964,7 @@ describe("Chatboxes", function () {
             const view = await mock.openChatBoxFor(_converse, sender_jid)
             const sent_stanzas = [];
             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 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');

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

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

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

+ 0 - 3
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`<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>

+ 4 - 1
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`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
         ${show_toolbar ? html`
             <converse-chat-toolbar
                 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");
     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`<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>`}`;
     } else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) {

+ 2 - 2
src/plugins/muc-views/tests/muc.js

@@ -277,13 +277,13 @@ describe("Groupchats", function () {
                     <delay xmlns="urn:xmpp:delay" stamp="2020-07-14T17:46:47Z" from="juliet@shakespeare.lit"/>
                 </message>`);
 
-            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();
         }));

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

+ 12 - 40
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) {

+ 41 - 1
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.