Browse Source

Create new `converse-message-history` component

- Fix rendering of notifications
- Remove unused view methods
- Chat views no longer need to be Overviews
- Implement scrolling on the chat content component
JC Brand 5 years ago
parent
commit
1d875391d0

+ 1 - 2
sass/_chatbox.scss

@@ -220,8 +220,7 @@
             height: 100%;
             line-height: 1.3em;
             overflow: hidden;
-            padding: 1em 0 0 0;
-
+            padding: 0;
             display: flex;
             flex-direction: column;
             justify-content: space-between;

+ 0 - 1
sass/_chatrooms.scss

@@ -99,7 +99,6 @@
 
     .empty-history-feedback {
         position: relative;
-        height: 100%;
         span {
             width: 100%;
             text-align: center;

+ 1 - 1
spec/bookmarks.js

@@ -1,6 +1,6 @@
 /* global mock */
 
-describe("A chat room", function () {
+fdescribe("A chat room", function () {
 
     it("can be bookmarked", mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) {
 

+ 1 - 1
spec/spoilers.js

@@ -1,6 +1,6 @@
 /* global mock */
 
-describe("A spoiler message", function () {
+fdescribe("A spoiler message", function () {
 
     it("can be received with a hint",
         mock.initConverse(

+ 15 - 66
src/components/chat_content.js

@@ -1,35 +1,9 @@
-import "../components/message";
-import 'fa-icons';
-import dayjs from 'dayjs';
-import tpl_message from "templates/message.js";
-import tpl_new_day from "../templates//new_day.js";
+import "../components/message-history";
 import xss from "xss/dist/xss";
 import { CustomElement } from './element.js';
-import { __ } from '@converse/headless/i18n';
-import { api } from "@converse/headless/converse-core";
 import { html } from 'lit-element';
-import { repeat } from 'lit-html/directives/repeat.js';
 import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
 
-const i18n_no_history = __('No message history available.');
-
-
-// Return a TemplateResult indicating a new day if the passed in message is
-// more than a day later than its predecessor.
-function getDayIndicator (model) {
-    const models = model.collection.models;
-    const idx = models.indexOf(model);
-    const prev_model =  models[idx-1];
-    if (!prev_model || dayjs(model.get('time')).isAfter(dayjs(prev_model.get('time')), 'day')) {
-        const day_date = dayjs(model.get('time')).startOf('day');
-        return tpl_new_day({
-            'type': 'date',
-            'time': day_date.toISOString(),
-            'datestring': day_date.format("dddd MMM Do YYYY")
-        });
-    }
-}
-
 
 class ChatContent extends CustomElement {
 
@@ -42,51 +16,26 @@ class ChatContent extends CustomElement {
     }
 
     render () {
-        const msgs = this.messages;
         const notifications = xss.filterXSS(this.notifications, {'whiteList': {}});
-        return [
-            msgs.length ?
-                html`${repeat(msgs, m => m.get('id'), m => this.renderMessage(m)) }` :
-                html`<div class="empty-history-feedback form-help"><span>${i18n_no_history}</span></div>`,
-            html`<div class="chat-content__notifications">${unsafeHTML(notifications)}</div>`
-        ];
+        return html`
+            <converse-message-history
+                .chatview=${this.chatview}
+                .messages=${this.messages}>
+            </converse-message-history>
+            <div class="chat-content__notifications">${unsafeHTML(notifications)}</div>
+        `;
     }
 
-    renderMessage (model) {
-        // XXX: leaky abstraction "is_only_key" from converse-omemo
-        if (model.get('dangling_retraction') || model.get('is_only_key')) {
-            return '';
+    scrollDown () {
+        if (!this.chatview.model.get('scrolled')) {
+            this.parentElement.scrollTop = this.parentElement.scrollHeight;
         }
-        const day = getDayIndicator(model);
-        const templates = day ? [day] : [];
-        const is_retracted = model.get('retracted') || model.get('moderated') === 'retracted';
-        const is_groupchat_message = model.get('type') === 'groupchat';
-
-        let hats = [];
-        if (is_groupchat_message) {
-            if (api.settings.get('muc_hats_from_vcard')) {
-                const role = model.vcard ? model.vcard.get('role') : null;
-                hats = role ? role.split(',') : [];
-            } else {
-                hats = model.occupant?.get('hats') || [];
-            }
-        }
-
-        const message = tpl_message(
-            Object.assign(model.toJSON(), {
-                'chatview': this.chatview,
-                'is_me_message': model.isMeCommand(),
-                'occupant': model.occupant,
-                'username': model.getDisplayName(),
-                hats,
-                is_groupchat_message,
-                is_retracted,
-                model,
-            }));
-        return [...templates, message];
+        this.parentElement.scrollTop = this.parentElement.scrollHeight;
     }
 
-
+    updated () {
+        this.scrollDown();
+    }
 }
 
 customElements.define('converse-chat-content', ChatContent);

+ 2 - 1
src/components/help_messages.js

@@ -1,8 +1,9 @@
+import 'fa-icons';
 import xss from "xss/dist/xss";
 import { CustomElement } from './element.js';
+import { _converse } from "@converse/headless/converse-core";
 import { html } from 'lit-element';
 import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
-import { _converse } from "@converse/headless/converse-core";
 
 
 class ChatHelp extends CustomElement {

+ 82 - 0
src/components/message-history.js

@@ -0,0 +1,82 @@
+import "../components/message";
+import dayjs from 'dayjs';
+import tpl_message from "templates/message.js";
+import tpl_new_day from "../templates//new_day.js";
+import { CustomElement } from './element.js';
+import { __ } from '@converse/headless/i18n';
+import { api } from "@converse/headless/converse-core";
+import { html } from 'lit-element';
+import { repeat } from 'lit-html/directives/repeat.js';
+
+const i18n_no_history = __('No message history available.');
+
+
+// Return a TemplateResult indicating a new day if the passed in message is
+// more than a day later than its predecessor.
+function getDayIndicator (model) {
+    const models = model.collection.models;
+    const idx = models.indexOf(model);
+    const prev_model =  models[idx-1];
+    if (!prev_model || dayjs(model.get('time')).isAfter(dayjs(prev_model.get('time')), 'day')) {
+        const day_date = dayjs(model.get('time')).startOf('day');
+        return tpl_new_day({
+            'type': 'date',
+            'time': day_date.toISOString(),
+            'datestring': day_date.format("dddd MMM Do YYYY")
+        });
+    }
+}
+
+
+class MessageHistory extends CustomElement {
+
+    static get properties () {
+        return {
+            chatview: { type: Object},
+            messages: { type: Array}
+        }
+    }
+
+    render () {
+        const msgs = this.messages;
+        return msgs.length ?
+            html`${repeat(msgs, m => m.get('id'), m => this.renderMessage(m)) }` :
+            html`<div class="empty-history-feedback form-help"><span>${i18n_no_history}</span></div>`;
+    }
+
+    renderMessage (model) {
+        // XXX: leaky abstraction "is_only_key" from converse-omemo
+        if (model.get('dangling_retraction') || model.get('is_only_key')) {
+            return '';
+        }
+        const day = getDayIndicator(model);
+        const templates = day ? [day] : [];
+        const is_retracted = model.get('retracted') || model.get('moderated') === 'retracted';
+        const is_groupchat_message = model.get('type') === 'groupchat';
+
+        let hats = [];
+        if (is_groupchat_message) {
+            if (api.settings.get('muc_hats_from_vcard')) {
+                const role = model.vcard ? model.vcard.get('role') : null;
+                hats = role ? role.split(',') : [];
+            } else {
+                hats = model.occupant?.get('hats') || [];
+            }
+        }
+
+        const message = tpl_message(
+            Object.assign(model.toJSON(), {
+                'chatview': this.chatview,
+                'is_me_message': model.isMeCommand(),
+                'occupant': model.occupant,
+                'username': model.getDisplayName(),
+                hats,
+                is_groupchat_message,
+                is_retracted,
+                model,
+            }));
+        return [...templates, message];
+    }
+}
+
+customElements.define('converse-message-history', MessageHistory);

+ 29 - 116
src/converse-chatview.js

@@ -18,14 +18,14 @@ import tpl_toolbar from "templates/toolbar.html";
 import tpl_toolbar_fileupload from "templates/toolbar_fileupload.html";
 import tpl_user_details_modal from "templates/user_details_modal.js";
 import { BootstrapModal } from "./converse-modal.js";
-import { Overview } from "skeletor.js/src/overview";
+import { View } from 'skeletor.js/src/view.js';
 import { __ } from '@converse/headless/i18n';
 import { _converse, api, converse } from "@converse/headless/converse-core";
 import { debounce, isString } from "lodash";
 import { html, render } from "lit-html";
 
 
-const { Strophe, sizzle, dayjs } = converse.env;
+const { Strophe, dayjs } = converse.env;
 const u = converse.env.utils;
 
 
@@ -163,7 +163,7 @@ converse.plugins.add('converse-chatview', {
          * @namespace _converse.ChatBoxView
          * @memberOf _converse
          */
-        _converse.ChatBoxView = Overview.extend({
+        _converse.ChatBoxView = View.extend({
             length: 200,
             className: 'chatbox hidden',
             is_chatroom: false,  // Leaky abstraction from MUC
@@ -188,15 +188,10 @@ converse.plugins.add('converse-chatview', {
             async initialize () {
                 this.initDebounced();
 
-                this.listenTo(this.model.notifications, 'change', this.debouncedChatContentRender);
-                this.listenTo(this.model.messages, 'add', this.debouncedChatContentRender);
-                this.listenTo(this.model.messages, 'change', this.debouncedChatContentRender);
-
-                this.listenTo(this.model.messages, 'rendered', this.scrollDown);
-                this.model.messages.on('reset', () => {
-                    this.msgs_container.innerHTML = '';
-                    this.removeAll();
-                });
+                this.listenTo(this.model.messages, 'add', this.renderChatHistory);
+                this.listenTo(this.model.messages, 'change', this.renderChatHistory);
+                this.listenTo(this.model.messages, 'reset', this.renderChatHistory);
+                this.listenTo(this.model.notifications, 'change', this.renderNotifications);
 
                 this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
                 this.listenTo(this.model, 'destroy', this.remove);
@@ -227,9 +222,9 @@ converse.plugins.add('converse-chatview', {
             },
 
             initDebounced () {
-                this.scrollDown = debounce(this._scrollDown, 50);
                 this.markScrolled = debounce(this._markScrolled, 100);
-                this.debouncedChatContentRender = debounce(this.renderChatContent, 100);
+                this.renderChatHistory = debounce(() => this.renderChatContent(false), 100);
+                this.renderNotifications = debounce(() => this.renderChatContent(true), 100);
             },
 
             async render () {
@@ -243,7 +238,12 @@ converse.plugins.add('converse-chatview', {
                 );
                 render(result, this.el);
                 this.tpl_chat_content = (o) => {
-                    return html`<converse-chat-content .chatview=${this} .messages=${o.messages}></converse-chat-content>`
+                    return html`
+                        <converse-chat-content
+                            .chatview=${this}
+                            .messages=${o.messages}
+                            .notifications=${o.notifications}>
+                        </converse-chat-content>`
                 };
                 this.content = this.el.querySelector('.chat-content');
                 this.notifications = this.el.querySelector('.chat-content__notifications');
@@ -268,11 +268,12 @@ converse.plugins.add('converse-chatview', {
                 }
             },
 
-            renderChatContent () {
-                render(this.tpl_chat_content({
-                    'notifications': this.getNotifications(),
-                    'messages': Array.from(this.model.messages)
-                }), this.msgs_container);
+            renderChatContent (msgs_by_ref=false) {
+                const messages = msgs_by_ref ? this.model.messages : Array.from(this.model.messages);
+                render(
+                    this.tpl_chat_content({ messages, 'notifications': this.getNotifications() }),
+                    this.msgs_container
+                );
             },
 
             renderToolbar () {
@@ -487,7 +488,6 @@ converse.plugins.add('converse-chatview', {
                 this.renderChatContent();
                 this.insertIntoDOM();
                 this.scrollDown();
-                this.content.addEventListener('scroll', () => this.markScrolled());
                 /**
                  * Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
                  * `sessionStorage` but **NOT** from the server.
@@ -495,7 +495,12 @@ converse.plugins.add('converse-chatview', {
                  * @type {_converse.ChatBoxView | _converse.ChatRoomView}
                  * @example _converse.api.listen.on('afterMessagesFetched', view => { ... });
                  */
-                api.trigger('afterMessagesFetched', this);
+                api.trigger('afterMessagesFetched', this.model);
+            },
+
+            scrollDown () {
+                const el = this.msgs_container.firstElementChild;
+                el && el.scrollDown();
             },
 
             insertIntoDOM () {
@@ -554,47 +559,6 @@ converse.plugins.add('converse-chatview', {
                 }
             },
 
-            /**
-             * Return the ISO8601 format date of the latest message.
-             * @private
-             * @method _converse.ChatBoxView#getLastMessageDate
-             * @param { Date } cutoff - Moment Date cutoff date. The last
-             *      message received cutoff this date will be returned.
-             * @returns { Date }
-             */
-            getLastMessageDate (cutoff) {
-                const sel = '.msg-wrapper';
-                const first_msg = u.getFirstChildElement(this.msgs_container, sel);
-                const oldest_date = first_msg ? first_msg.getAttribute('data-isodate') : null;
-                if (oldest_date !== null && dayjs(oldest_date).isAfter(cutoff)) {
-                    return null;
-                }
-                const last_msg = u.getLastChildElement(this.msgs_container, sel);
-                const most_recent_date = last_msg ? last_msg.getAttribute('data-isodate') : null;
-                if (most_recent_date === null) {
-                    return null;
-                }
-                if (dayjs(most_recent_date).isBefore(cutoff)) {
-                    return dayjs(most_recent_date).toDate();
-                }
-                /* XXX: We avoid .chat-state-notification messages, since they are
-                 * temporary and get removed once a new element is
-                 * inserted into the chat area, so we don't query for
-                 * them here, otherwise we get a null reference later
-                 * upon element insertion.
-                 */
-                const msg_dates = sizzle(sel, this.msgs_container).map(e => e.getAttribute('data-isodate'));
-                const cutoff_iso = cutoff.toISOString();
-                msg_dates.push(cutoff_iso);
-                msg_dates.sort();
-                const idx = msg_dates.lastIndexOf(cutoff_iso);
-                if (idx === 0) {
-                    return null;
-                } else {
-                    return dayjs(msg_dates[idx-1]).toDate();
-                }
-            },
-
             setScrollPosition (message_el) {
                 /* Given a newly inserted message, determine whether we
                  * should keep the scrollbar in place (so as to not scroll
@@ -646,40 +610,6 @@ converse.plugins.add('converse-chatview', {
                 return !u.isVisible(this.el);
             },
 
-            /**
-             * Given a view representing a message, insert it into the
-             * content area of the chat box.
-             * @private
-             * @method _converse.ChatBoxView#insertMessage
-             * @param { View } message - The message View
-             */
-            insertMessage (view) {
-                if (view.model.get('type') === 'error') {
-                    const previous_msg_el = this.msgs_container.querySelector(`[data-msgid="${view.model.get('msgid')}"]`);
-                    if (previous_msg_el) {
-                        previous_msg_el.insertAdjacentElement('afterend', view.el);
-                        return this.trigger('messageInserted', view.el);
-                    }
-                }
-                const current_msg_date = dayjs(view.model.get('time')).toDate() || new Date();
-                const previous_msg_date = this.getLastMessageDate(current_msg_date);
-
-                if (previous_msg_date === null) {
-                    this.msgs_container.insertAdjacentElement('afterbegin', view.el);
-                } else {
-                    const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date.toISOString()}"]:last`, this.msgs_container).pop();
-                    if (view.model.get('type') === 'error' &&
-                            u.hasClass('chat-error', previous_msg_el) &&
-                            previous_msg_el.textContent === view.model.get('message')) {
-                        // We don't show a duplicate error message
-                        return;
-                    }
-                    previous_msg_el.insertAdjacentElement('afterend', view.el);
-                    this.markFollowups(view.el);
-                }
-                return this.trigger('messageInserted', view.el);
-            },
-
             /**
              * Given a message element, determine wether it should be
              * marked as a followup message to the previous element.
@@ -750,10 +680,8 @@ converse.plugins.add('converse-chatview', {
                     return;
                 }
                 if (!_converse.connection.authenticated) {
-                    this.showHelpMessages(
-                        ['Sorry, the connection has been lost, and your message could not be sent'],
-                        'error'
-                    );
+                    const err_msg = __('Sorry, the connection has been lost, and your message could not be sent');
+                    api.alert('error', __('Error'), err_msg);
                     api.connection.reconnect();
                     return;
                 }
@@ -1215,21 +1143,6 @@ converse.plugins.add('converse-chatview', {
                 this.scrollDown();
             },
 
-            _scrollDown () {
-                /* Inner method that gets debounced */
-                if (this.content === undefined) {
-                    return;
-                }
-                if (u.isVisible(this.content) && !this.model.get('scrolled')) {
-                    if ((this.content.scrollTop === 0 || this.content.scrollTop < this.content.scrollHeight/2)) {
-                        u.removeClass('smooth-scroll', this.content);
-                    } else if (api.settings.get('animate')) {
-                        u.addClass('smooth-scroll', this.content);
-                    }
-                    this.content.scrollTop = this.content.scrollHeight;
-                }
-            },
-
             onScrolledDown () {
                 this.hideNewMessagesIndicator();
                 if (_converse.windowState !== 'hidden') {

+ 4 - 24
src/converse-muc-views.js

@@ -459,14 +459,11 @@ converse.plugins.add('converse-muc-views', {
             async initialize () {
                 this.initDebounced();
 
-                this.listenTo(this.model.messages, 'add', debounce(this.renderChatContent, 100));
-                this.listenTo(this.model.messages, 'change', debounce(this.renderChatContent, 100));
-
+                this.listenTo(this.model.messages, 'add', this.renderChatHistory);
+                this.listenTo(this.model.messages, 'change', this.renderChatHistory);
                 this.listenTo(this.model.messages, 'rendered', this.scrollDown);
-                this.model.messages.on('reset', () => {
-                    this.msgs_container.innerHTML = '';
-                    this.removeAll();
-                });
+                this.listenTo(this.model.messages, 'reset', this.renderChatHistory);
+                this.listenTo(this.model.notifications, 'change', this.renderNotifications);
 
                 this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
 
@@ -1667,23 +1664,6 @@ converse.plugins.add('converse-muc-views', {
                 u.showElement(container);
             },
 
-            removeEmptyHistoryFeedback () {
-                const el = this.msgs_container.firstElementChild;
-                if (_converse.muc_show_logs_before_join && el && el.matches('.empty-history-feedback')) {
-                    this.msgs_container.removeChild(this.msgs_container.firstElementChild);
-                }
-            },
-
-            insertDayIndicator () {
-                this.removeEmptyHistoryFeedback();
-                return _converse.ChatBoxView.prototype.insertDayIndicator.apply(this, arguments);
-            },
-
-            insertMessage (view) {
-                this.removeEmptyHistoryFeedback();
-                return _converse.ChatBoxView.prototype.insertMessage.call(this, view);
-            },
-
             onOccupantAdded (occupant) {
                 if (occupant.get('jid') === _converse.bare_jid) {
                     this.renderHeading();

+ 2 - 3
src/templates/chatbox.js

@@ -4,9 +4,8 @@ export default (o) => html`
     <div class="flyout box-flyout">
         <div class="chat-head chat-head-chatbox row no-gutters"></div>
         <div class="chat-body">
-            <div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" @scroll=${o.markScrolled} aria-live="polite">
-                <div class="chat-content__messages"></div>
-                <div class="chat-content__notifications"></div>
+            <div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
+                <div class="chat-content__messages smooth-scroll" @scroll=${o.markScrolled}></div>
                 <div class="chat-content__help"></div>
             </div>
             <div class="bottom-panel">

+ 1 - 7
src/templates/chatroom.js

@@ -1,8 +1,4 @@
 import { html } from "lit-html";
-import { __ } from '@converse/headless/i18n';
-
-const i18n_no_history = __('No message history available.');
-
 
 export default (o) => html`
     <div class="flyout box-flyout">
@@ -10,9 +6,7 @@ export default (o) => html`
         <div class="chat-body chatroom-body row no-gutters">
             <div class="chat-area col">
                 <div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
-                    <div class="chat-content__messages">
-                        ${ o.muc_show_logs_before_join ? html`<div class="empty-history-feedback"><span>${ i18n_no_history }</span></div>`  : '' }
-                    </div>
+                    <div class="chat-content__messages smooth-scroll" @scroll=${o.markScrolled}></div>
                     <div class="chat-content__help"></div>
                 </div>
                 <div class="bottom-panel"></div>