Selaa lähdekoodia

Render chat content as a <converse-chat-content> component

JC Brand 5 vuotta sitten
vanhempi
commit
bbf3a6cbeb

+ 86 - 0
src/components/chat_content.js

@@ -0,0 +1,86 @@
+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 { 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.');
+const tpl_no_msgs = html`<div class="empty-history-feedback"><span>${i18n_no_history}</span></div>`
+
+
+// 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")
+        });
+    }
+}
+
+
+function renderMessage (model) {
+    if (model.get('dangling_retraction')) {
+        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(), {
+            'is_me_message': model.isMeCommand(),
+            'occupant': model.occupant,
+            'username': model.getDisplayName(),
+            hats,
+            is_groupchat_message,
+            is_retracted,
+            model
+        }));
+
+    if (model.collection) {
+        // If the model gets destroyed in the meantime, it no
+        // longer has a collection.
+        model.collection.trigger('rendered', this);
+    }
+    return [...templates, message];
+}
+
+
+class ChatContent extends CustomElement {
+
+    static get properties () {
+        return {
+            messages: { type: Array},
+        }
+    }
+
+    render () {
+        const msgs = this.messages;
+        return html`${ !msgs.length ? tpl_no_msgs : repeat(msgs, m => m.get('id'), m => renderMessage(m)) }`;
+    }
+}
+
+customElements.define('converse-chat-content', ChatContent);

+ 11 - 1
src/components/message.js

@@ -116,12 +116,14 @@ class Message extends CustomElement {
     }
 
     render () {
-        this.pretty_time = dayjs(this.time).format(api.settings.get('time_format'));
+        const format = api.settings.get('time_format');
+        this.pretty_time = dayjs(this.time).format(format);
         if (this.model.get('file') && !this.model.get('oob_url')) {
             return tpl_file_progress(this);
         } else if (['error', 'info'].includes(this.message_type)) {
             return tpl_info(
                 Object.assign(this.model.toJSON(), {
+                    'onRetryClicked': ev => this.onRetryClicked(ev),
                     'extra_classes': this.message_type,
                     'isodate': dayjs(this.model.get('time')).toISOString()
                 })
@@ -131,6 +133,14 @@ class Message extends CustomElement {
         }
     }
 
+    async onRetryClicked () {
+        // FIXME
+        // this.showSpinner();
+        await this.model.error.retry();
+        this.model.destroy();
+        this.parentElement.removeChild(this);
+    }
+
     isFollowup () {
         const messages = this.model.collection.models;
         const idx = messages.indexOf(this.model);

+ 19 - 37
src/converse-chatview.js

@@ -3,8 +3,8 @@
  * @copyright 2020, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
+import "./components/chat_content.js";
 import "converse-chatboxviews";
-import "converse-message-view";
 import "converse-modal";
 import log from "@converse/headless/log";
 import tpl_chatbox from "templates/chatbox.js";
@@ -45,7 +45,6 @@ converse.plugins.add('converse-chatview', {
         "converse-chatboxviews",
         "converse-chat",
         "converse-disco",
-        "converse-message-view",
         "converse-modal"
     ],
 
@@ -56,9 +55,12 @@ converse.plugins.add('converse-chatview', {
         api.settings.update({
             'auto_focus': true,
             'message_limit': 0,
-            'show_send_button': true,
+            'muc_hats_from_vcard': false,
+            'show_images_inline': true,
             'show_retraction_warning': true,
+            'show_send_button': true,
             'show_toolbar': true,
+            'time_format': 'HH:mm',
             'visible_toolbar_buttons': {
                 'call': false,
                 'clear': true,
@@ -189,7 +191,9 @@ converse.plugins.add('converse-chatview', {
             async initialize () {
                 this.initDebounced();
 
-                this.listenTo(this.model.messages, 'add', this.onMessageAdded);
+                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, 'rendered', this.scrollDown);
                 this.model.messages.on('reset', () => {
                     this.msgs_container.innerHTML = '';
@@ -231,7 +235,7 @@ converse.plugins.add('converse-chatview', {
                 this.markScrolled = debounce(this._markScrolled, 100);
             },
 
-            render () {
+            async render () {
                 const result = tpl_chatbox(
                     Object.assign(
                         this.model.toJSON(), {
@@ -245,6 +249,7 @@ converse.plugins.add('converse-chatview', {
                 this.notifications = this.el.querySelector('.chat-content__notifications');
                 this.msgs_container = this.el.querySelector('.chat-content__messages');
                 this.renderChatStateNotification();
+                await this.renderChatContent();
                 this.renderMessageForm();
                 this.renderHeading();
                 return this;
@@ -262,6 +267,12 @@ converse.plugins.add('converse-chatview', {
                 }
             },
 
+            async renderChatContent () {
+                await api.waitUntil('emojisInitialized');
+                const tpl = (o) => html`<converse-chat-content .messages=${o.messages}></converse-chat-content>`;
+                render(tpl({'messages': Array.from(this.model.messages)}), this.msgs_container);
+            },
+
             renderToolbar () {
                 if (!api.settings.get('show_toolbar')) {
                     return this;
@@ -471,7 +482,7 @@ converse.plugins.add('converse-chatview', {
 
             async updateAfterMessagesFetched () {
                 await this.model.messages.fetched;
-                await Promise.all(this.model.messages.map(m => this.onMessageAdded(m)));
+                this.renderChatContent();
                 this.insertIntoDOM();
                 this.scrollDown();
                 this.content.addEventListener('scroll', () => this.markScrolled());
@@ -753,35 +764,6 @@ converse.plugins.add('converse-chatview', {
                 }
             },
 
-            /**
-             * Handler that gets called when a new message object is created.
-             * @private
-             * @method _converse.ChatBoxView#onMessageAdded
-             * @param { object } message - The message object that was added.
-             */
-            async onMessageAdded (message) {
-                const id = message.get('id');
-                if (id && this.get(id)) {
-                    // We already have a view for this message
-                    return;
-                }
-                if (!message.get('dangling_retraction')) {
-                    await this.showMessage(message);
-                }
-                /**
-                 * Triggered once a message has been added to a chatbox.
-                 * @event _converse#messageAdded
-                 * @type {object}
-                 * @property { _converse.Message } message - The message instance
-                 * @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
-                 * @example _converse.api.listen.on('messageAdded', data => { ... });
-                 */
-                api.trigger('messageAdded', {
-                    'message': message,
-                    'chatbox': this.model
-                });
-            },
-
             parseMessageForCommands (text) {
                 const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
                 if (match) {
@@ -1135,8 +1117,8 @@ converse.plugins.add('converse-chatview', {
             },
 
             onPresenceChanged (item) {
-                const show = item.get('show'),
-                      fullname = this.model.getDisplayName();
+                const show = item.get('show');
+                const fullname = this.model.getDisplayName();
 
                 let text;
                 if (u.isVisible(this.el)) {

+ 0 - 162
src/converse-message-view.js

@@ -1,162 +0,0 @@
-/**
- * @module converse-message-view
- * @copyright 2020, the Converse.js contributors
- * @license Mozilla Public License (MPLv2)
- */
-import "./components/message.js";
-import "./utils/html";
-import "@converse/headless/converse-emoji";
-import tpl_message from "templates/message.js";
-import tpl_spinner from "templates/spinner.html";
-import { _converse, api, converse } from  "@converse/headless/converse-core";
-import { debounce } from 'lodash'
-import { render } from "lit-html";
-
-const u = converse.env.utils;
-
-
-converse.plugins.add('converse-message-view', {
-
-    dependencies: ["converse-modal", "converse-chatboxviews"],
-
-    initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by converse.js's plugin machinery.
-         */
-        api.settings.update({
-            'muc_hats_from_vcard': false,
-            'show_images_inline': true,
-            'time_format': 'HH:mm',
-        });
-
-
-        /**
-         * @class
-         * @namespace _converse.MessageView
-         * @memberOf _converse
-         */
-        _converse.MessageView = _converse.ViewWithAvatar.extend({
-            className: 'msg-wrapper',
-            events: {
-                'click .chat-msg__edit-modal': 'showMessageVersionsModal',
-                'click .retry': 'onRetryClicked'
-            },
-
-            initialize () {
-                this.debouncedRender = debounce(() => {
-                    // If the model gets destroyed in the meantime,
-                    // it no longer has a collection
-                    if (this.model.collection) {
-                        this.render();
-                    }
-                }, 50);
-
-                if (this.model.rosterContactAdded) {
-                    this.model.rosterContactAdded.then(() => {
-                        this.listenTo(this.model.contact, 'change:nickname', this.debouncedRender);
-                        this.debouncedRender();
-                    });
-                }
-
-                this.model.occupant && this.addOccupantListeners();
-                this.listenTo(this.model, 'change', this.onChanged);
-                this.listenTo(this.model, 'destroy', this.fadeOut);
-                this.listenTo(this.model, 'occupantAdded', () => {
-                    this.addOccupantListeners();
-                    this.debouncedRender();
-                });
-                this.listenTo(this.model, 'vcard:change', this.debouncedRender);
-            },
-
-            async render () {
-                await api.waitUntil('emojisInitialized');
-                const is_followup = u.hasClass('chat-msg--followup', this.el);
-                const is_retracted = this.model.get('retracted') || this.model.get('moderated') === 'retracted';
-                const is_groupchat_message = this.model.get('type') === 'groupchat';
-
-                let hats = [];
-                if (is_groupchat_message) {
-                    if (api.settings.get('muc_hats_from_vcard')) {
-                        const role = this.model.vcard ? this.model.vcard.get('role') : null;
-                        hats = role ? role.split(',') : [];
-                    } else {
-                        hats = this.model.occupant?.get('hats') || [];
-                    }
-                }
-
-                render(tpl_message(
-                    Object.assign(
-                        this.model.toJSON(), {
-                        'is_me_message': this.model.isMeCommand(),
-                        'model': this.model,
-                        'occupant': this.model.occupant,
-                        'username': this.model.getDisplayName(),
-                        hats,
-                        is_groupchat_message,
-                        is_retracted
-                    })
-                ), this.el);
-
-                if (this.model.collection) {
-                    // If the model gets destroyed in the meantime, it no
-                    // longer has a collection.
-                    this.model.collection.trigger('rendered', this);
-                }
-                is_followup && u.addClass('chat-msg--followup', this.el);
-                this.el.setAttribute('data-isodate', this.model.get('time'));
-                this.el.setAttribute('data-msgid', this.model.get('msgid'));
-                return this.el;
-            },
-
-            async onChanged (item) {
-                // Jot down whether it was edited because the `changed`
-                // attr gets removed when this.render() gets called further down.
-                const edited = item.changed.edited;
-                if (this.model.changed.progress) {
-                    return this.renderFileUploadProgresBar();
-                }
-                await this.debouncedRender();
-                if (edited) {
-                    this.onMessageEdited();
-                }
-            },
-
-            addOccupantListeners () {
-                this.listenTo(this.model.occupant, 'change:affiliation', this.debouncedRender);
-                this.listenTo(this.model.occupant, 'change:hats', this.debouncedRender);
-                this.listenTo(this.model.occupant, 'change:role', this.debouncedRender);
-            },
-
-            fadeOut () {
-                if (api.settings.get('animate')) {
-                    setTimeout(() => this.remove(), 600);
-                    u.addClass('fade-out', this.el);
-                } else {
-                    this.remove();
-                }
-            },
-
-            async onRetryClicked () {
-                this.showSpinner();
-                await this.model.error.retry();
-                this.model.destroy();
-            },
-
-            showSpinner () {
-                this.el.innerHTML = tpl_spinner();
-            },
-
-            onMessageEdited () {
-                if (this.model.get('is_archived')) {
-                    return;
-                }
-                this.el.addEventListener(
-                    'animationend',
-                    () => u.removeClass('onload', this.el),
-                    {'once': true}
-                );
-                u.addClass('onload', this.el);
-            }
-        });
-    }
-});

+ 3 - 2
src/converse-muc-views.js

@@ -461,8 +461,9 @@ converse.plugins.add('converse-muc-views', {
             async initialize () {
                 this.initDebounced();
 
-                this.listenTo(this.model.messages, 'add', this.onMessageAdded);
-                this.listenTo(this.model.messages, 'change:edited', this.onMessageEdited);
+                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, 'rendered', this.scrollDown);
                 this.model.messages.on('reset', () => {
                     this.msgs_container.innerHTML = '';

+ 0 - 1
src/converse.js

@@ -45,7 +45,6 @@ const WHITELISTED_PLUGINS = [
     'converse-emoji-views',
     'converse-fullscreen',
     'converse-mam-views',
-    'converse-message-view',
     'converse-minimize',
     'converse-modal',
     'converse-muc-views',

+ 17 - 52
src/templates/directives/body.js

@@ -1,5 +1,4 @@
 import URI from "urijs";
-import log from '@converse/headless/log';
 import xss from "xss/dist/xss";
 import { _converse, api, converse } from  "@converse/headless/converse-core";
 import { directive, html } from "lit-html";
@@ -88,7 +87,7 @@ class MessageBodyRenderer extends String {
             text = u.addEmoji(text);
             return addMentionsMarkup(text, this.model.get('references'), this.model.collection.chatbox);
         }
-        const list = await Promise.all(addHyperlinks(text));
+        const list = await Promise.all(u.addHyperlinks(text));
         this.list = list.reduce((acc, i) => isString(i) ? [...acc, ...process(i)] : [...acc, i], []);
         /**
          * Synchronous event which provides a hook for transforming a chat message's body text
@@ -124,60 +123,26 @@ const tpl_mention_with_nick = (o) => html`<span class="mention mention--self bad
 const tpl_mention = (o) => html`<span class="mention">${o.mention}</span>`;
 
 
-function addHyperlinks (text) {
-    const objs = [];
-    const parse_options = { 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi };
-    try {
-        URI.withinString(text, (url, start, end) => {
-            objs.push({url, start, end})
-            return url;
-        } , parse_options);
-    } catch (error) {
-        log.debug(error);
-        return [text];
-    }
-
-    const show_images = api.settings.get('show_images_inline');
-
-    let list = [];
-    if (objs.length) {
-        objs.sort((a, b) => b.start - a.start)
-            .forEach(url_obj => {
-                const new_list = [
-                    text.slice(0, url_obj.start),
-                    show_images && u.isImageURL(text) ? u.convertToImageTag(text) : u.convertUrlToHyperlink(text),
-                    text.slice(url_obj.end),
-                    ...list
-                ];
-                list = new_list.filter(i => i);
-                text = text.slice(0, url_obj.start);
+function addMentionsMarkup (text, references, chatbox) {
+    if (chatbox.get('message_type') === 'groupchat' && references.length) {
+        let list = [];
+        const nick = chatbox.get('nick');
+        references
+            .sort((a, b) => b.begin - a.begin)
+            .forEach(ref => {
+                const mention = text.slice(ref.begin, ref.end)
+                chatbox;
+                if (mention === nick) {
+                    list = [text.slice(0, ref.begin), new Markup(mention, tpl_mention_with_nick({mention})), text.slice(ref.end),  ...list];
+                } else {
+                    list = [text.slice(0, ref.begin), new Markup(mention, tpl_mention({mention})), text.slice(ref.end), ...list];
+                }
+                text = text.slice(0, ref.begin);
             });
+        return list;
     } else {
-        list = [text];
-    }
-    return list;
-}
-
-
-function addMentionsMarkup (text, references, chatbox) {
-    if (chatbox.get('message_type') !== 'groupchat') {
         return [text];
     }
-    const nick = chatbox.get('nick');
-    let list = [];
-    references
-        .sort((a, b) => b.begin - a.begin)
-        .forEach(ref => {
-            const mention = text.slice(ref.begin, ref.end)
-            chatbox;
-            if (mention === nick) {
-                list = [text.slice(0, ref.begin), new Markup(mention, tpl_mention_with_nick({mention})), text.slice(ref.end),  ...list];
-            } else {
-                list = [text.slice(0, ref.begin), new Markup(mention, tpl_mention({mention})), text.slice(ref.end), ...list];
-            }
-            text = text.slice(0, ref.begin);
-        });
-    return list;
 }
 
 

+ 9 - 0
src/templates/new_day.js

@@ -0,0 +1,9 @@
+import { html } from "lit-html";
+
+
+export default (o) => html`
+    <div class="message date-separator" data-isodate="${o.time}">
+        <hr class="separator"/>
+        <time class="separator-text" datetime="${o.time}"><span>${o.datestring}</span></time>
+    </div>
+`;

+ 34 - 0
src/utils/html.js

@@ -373,6 +373,40 @@ u.convertUrlToHyperlink = function (url) {
     return url;
 };
 
+u.addHyperlinks = function (text) {
+    const objs = [];
+    const parse_options = { 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi };
+    try {
+        URI.withinString(text, (url, start, end) => {
+            objs.push({url, start, end})
+            return url;
+        } , parse_options);
+    } catch (error) {
+        log.debug(error);
+        return [text];
+    }
+
+    const show_images = api.settings.get('show_images_inline');
+
+    let list = [];
+    if (objs.length) {
+        objs.sort((a, b) => b.start - a.start)
+            .forEach(url_obj => {
+                const new_list = [
+                    text.slice(0, url_obj.start),
+                    show_images && u.isImageURL(text) ? u.convertToImageTag(text) : u.convertUrlToHyperlink(text),
+                    text.slice(url_obj.end),
+                    ...list
+                ];
+                list = new_list.filter(i => i);
+                text = text.slice(0, url_obj.start);
+            });
+    } else {
+        list = [text];
+    }
+    return list;
+}
+
 u.geoUriToHttp = function(text, geouri_replacement) {
     const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
     return text.replace(regex, geouri_replacement);