Browse Source

Refactor message component to handle message editing and retraction

JC Brand 5 years ago
parent
commit
6ad62b1dbe

+ 44 - 41
src/components/chat_content.js

@@ -29,50 +29,11 @@ function getDayIndicator (model) {
 }
 
 
-function 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(), {
-            '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 {
+            chatview: { type: Object},
             messages: { type: Array},
         }
     }
@@ -80,9 +41,51 @@ class ChatContent extends CustomElement {
     render () {
         const msgs = this.messages;
         return msgs.length ?
-            html`${repeat(msgs, m => m.get('id'), m => renderMessage(m)) }` :
+            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,
+            }));
+
+        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];
+    }
+
+
 }
 
 customElements.define('converse-chat-content', ChatContent);

+ 85 - 71
src/components/message.js

@@ -1,14 +1,13 @@
 import "./message-body.js";
+import MessageVersionsModal from '../modals/message-versions.js';
 import dayjs from 'dayjs';
-import tpl_info from "../templates/info.js";
-import tpl_message_versions_modal from "../templates/message_versions_modal.js";
-import { BootstrapModal } from "../converse-modal.js";
 import { CustomElement } from './element.js';
 import { __ } from '@converse/headless/i18n';
 import { _converse, api, converse } from  "@converse/headless/converse-core";
 import { html } from 'lit-element';
 import { renderAvatar } from './../templates/directives/avatar';
 import { renderRetractionLink } from './../templates/directives/retraction';
+import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
 
 const { Strophe } = converse.env;
 const u = converse.env.utils;
@@ -20,69 +19,11 @@ const i18n_show_less = __('Show less');
 const i18n_uploading = __('Uploading file:')
 
 
-const MessageVersionsModal = BootstrapModal.extend({
-    // FIXME: this isn't globally unique
-    id: "message-versions-modal",
-    toHTML () {
-        return tpl_message_versions_modal(this.model.toJSON());
-    }
-});
-
-
-const tpl_file_progress = (o) => html`
-    <div class="message chat-msg">
-        ${ renderAvatar(this) }
-        <div class="chat-msg__content">
-            <span class="chat-msg__text">${i18n_uploading} <strong>${o.filename}</strong>, ${o.filesize}</span>
-            <progress value="${o.progress}"/>
-        </div>
-    </div>
-`;
-
-
-const tpl_chat_message = (o) =>  {
-    const is_groupchat_message = (o.message_type === 'groupchat');
-    return html`
-    <div class="message chat-msg ${o.message_type} ${o.getExtraMessageClasses()}
-            ${ o.is_me_message ? 'chat-msg--action' : '' }
-            ${o.isFollowup() ? 'chat-msg--followup' : ''}"
-            data-from="${o.from}" data-encrypted="${o.is_encrypted}">
-
-        ${ renderAvatar(o) }
-        <div class="chat-msg__content chat-msg__content--${o.sender} ${o.is_me_message ? 'chat-msg__content--action' : ''}">
-            ${o.first_unread ? html`<div class="message date-separator"><hr class="separator"><span class="separator-text">{{{o.__('unread messages')}}}</span></div>` : '' }
-            <span class="chat-msg__heading">
-                ${ (o.is_me_message) ? html`
-                    <time timestamp="${o.time}" class="chat-msg__time">${o.pretty_time}</time>
-                    ${o.hats.map(hat => html`<span class="badge badge-secondary">${hat}</span>`)}
-                ` : '' }
-                <span class="chat-msg__author">${ o.is_me_message ? '**' : ''}${o.username}</span>
-                ${ !o.is_me_message ? o.renderAvatarByline() : '' }
-                ${ o.is_encrypted ? html`<span class="fa fa-lock"></span>` : '' }
-            </span>
-            <div class="chat-msg__body chat-msg__body--${o.message_type} ${o.received ? 'chat-msg__body--received' : '' } ${o.is_delayed ? 'chat-msg__body--delayed' : '' }">
-                <div class="chat-msg__message">
-                    ${ o.is_retracted ? o.renderRetraction() : o.renderMessageText() }
-                </div>
-                ${ (o.received && !o.is_me_message && !is_groupchat_message) ? html`<span class="fa fa-check chat-msg__receipt"></span>` : '' }
-                ${ (o.edited) ? html`<i title="${ i18n_edited }" class="fa fa-edit chat-msg__edit-modal" @click=${o.showMessageVersionsModal}></i>` : '' }
-                <div class="chat-msg__actions">
-                    ${ o.editable ?
-                        html`<button class="chat-msg__action chat-msg__action-edit" title="${i18n_edit_message}">
-                            <fa-icon class="fas fa-pencil-alt" path-prefix="/node_modules" color="var(--text-color-lighten-15-percent)" size="1em"></fa-icon>
-                        </button>` : '' }
-                    ${ renderRetractionLink(o) }
-                </div>
-            </div>
-        </div>
-    </div>
-`};
-
-
 class Message extends CustomElement {
 
     static get properties () {
         return {
+            chatview: { type: Object},
             correcting: { type: Boolean },
             editable: { type: Boolean },
             first_unread: { type: Boolean },
@@ -119,20 +60,83 @@ class Message extends CustomElement {
         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);
+            return this.renderFileProgress();
         } 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()
-                })
-            );
+            return this.renderInfoMessage();
         } else {
-            return tpl_chat_message(this);
+            return this.renderChatMessage();
         }
     }
 
+    renderInfoMessage () {
+        const isodate = dayjs(this.model.get('time')).toISOString();
+        const i18n_retry = __('Retry');
+        return html`
+            <div class="message chat-info ${this.message_type}"
+                data-isodate="${isodate}"
+                data-type="${this.data_name}"
+                data-value="${this.data_value}">
+
+                ${ this.render_message ? unsafeHTML(this.message) : this.message }
+                ${ this.retry ? html`<a class="retry" @click=${this.onRetryClicked}>${i18n_retry}</a>` : '' }
+            </div>
+        `;
+    }
+
+
+    renderFileProgress () {
+        return html`
+            <div class="message chat-msg">
+                ${ renderAvatar(this) }
+                <div class="chat-msg__content">
+                    <span class="chat-msg__text">${i18n_uploading} <strong>${this.filename}</strong>, ${this.filesize}</span>
+                    <progress value="${this.progress}"/>
+                </div>
+            </div>`;
+    }
+
+    renderChatMessage () {
+        const is_groupchat_message = (this.message_type === 'groupchat');
+        return html`
+            <div class="message chat-msg ${this.message_type} ${this.getExtraMessageClasses()}
+                    ${ this.is_me_message ? 'chat-msg--action' : '' }
+                    ${this.isFollowup() ? 'chat-msg--followup' : ''}"
+                    data-isodate="${this.time}" data-msgid="${this.msgid}" data-from="${this.from}" data-encrypted="${this.is_encrypted}">
+
+                ${ renderAvatar(this) }
+                <div class="chat-msg__content chat-msg__content--${this.sender} ${this.is_me_message ? 'chat-msg__content--action' : ''}">
+                    ${this.first_unread ? html`<div class="message date-separator"><hr class="separator"><span class="separator-text">{{{this.__('unread messages')}}}</span></div>` : '' }
+                    <span class="chat-msg__heading">
+                        ${ (this.is_me_message) ? html`
+                            <time timestamp="${this.time}" class="chat-msg__time">${this.pretty_time}</time>
+                            ${this.hats.map(hat => html`<span class="badge badge-secondary">${hat}</span>`)}
+                        ` : '' }
+                        <span class="chat-msg__author">${ this.is_me_message ? '**' : ''}${this.username}</span>
+                        ${ !this.is_me_message ? this.renderAvatarByline() : '' }
+                        ${ this.is_encrypted ? html`<span class="fa fa-lock"></span>` : '' }
+                    </span>
+                    <div class="chat-msg__body chat-msg__body--${this.message_type} ${this.received ? 'chat-msg__body--received' : '' } ${this.is_delayed ? 'chat-msg__body--delayed' : '' }">
+                        <div class="chat-msg__message">
+                            ${ this.is_retracted ? this.renderRetraction() : this.renderMessageText() }
+                        </div>
+                        ${ (this.received && !this.is_me_message && !is_groupchat_message) ? html`<span class="fa fa-check chat-msg__receipt"></span>` : '' }
+                        ${ (this.edited) ? html`<i title="${ i18n_edited }" class="fa fa-edit chat-msg__edit-modal" @click=${this.showMessageVersionsModal}></i>` : '' }
+                        <div class="chat-msg__actions">
+                            ${ this.editable ?
+                                    html`<button
+                                        class="chat-msg__action chat-msg__action-edit"
+                                        title="${i18n_edit_message}"
+                                        @click=${this.onMessageEditButtonClicked}
+                                    >
+                                    <fa-icon class="fas fa-pencil-alt" path-prefix="/node_modules" color="var(--text-color-lighten-15-percent)" size="1em"></fa-icon>
+                                </button>` : '' }
+                            ${ renderRetractionLink(this) }
+                        </div>
+                    </div>
+                </div>
+            </div>`;
+    }
+
     async onRetryClicked () {
         // FIXME
         // this.showSpinner();
@@ -141,6 +145,16 @@ class Message extends CustomElement {
         this.parentElement.removeChild(this);
     }
 
+    onMessageRetractButtonClicked (ev) {
+        ev.preventDefault();
+        this.chatview.onMessageRetractButtonClicked(this.model);
+    }
+
+    onMessageEditButtonClicked (ev) {
+        ev.preventDefault();
+        this.chatview.onMessageEditButtonClicked(this.model);
+    }
+
     isFollowup () {
         const messages = this.model.collection.models;
         const idx = messages.indexOf(this.model);

+ 8 - 24
src/converse-chatview.js

@@ -171,8 +171,6 @@ converse.plugins.add('converse-chatview', {
 
             events: {
                 'change input.fileupload': 'onFileSelection',
-                'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
-                'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
                 'click .chatbox-navback': 'showControlBox',
                 'click .new-msgs-indicator': 'viewUnreadMessages',
                 'click .send-button': 'onFormSubmitted',
@@ -269,7 +267,7 @@ converse.plugins.add('converse-chatview', {
 
             async renderChatContent () {
                 await api.waitUntil('emojisInitialized');
-                const tpl = (o) => html`<converse-chat-content .messages=${o.messages}></converse-chat-content>`;
+                const tpl = (o) => html`<converse-chat-content .chatview=${this} .messages=${o.messages}></converse-chat-content>`;
                 render(tpl({'messages': Array.from(this.model.messages)}), this.msgs_container);
             },
 
@@ -907,14 +905,9 @@ converse.plugins.add('converse-chatview', {
                 this.insertIntoTextArea('', true, false);
             },
 
-            async onMessageRetractButtonClicked (ev) {
-                ev.preventDefault();
-                const msg_el = u.ancestor(ev.target, '.message');
-                const msgid = msg_el.getAttribute('data-msgid');
-                const time = msg_el.getAttribute('data-isodate');
-                const message = this.model.messages.findWhere({msgid, time});
+            async onMessageRetractButtonClicked (message) {
                 if (message.get('sender') !== 'me') {
-                    return log.error("onMessageEditButtonClicked called for someone else's message!");
+                    return log.error("onMessageRetractButtonClicked called for someone else's message!");
                 }
                 const retraction_warning =
                     __("Be aware that other XMPP/Jabber clients (and servers) may "+
@@ -931,26 +924,17 @@ converse.plugins.add('converse-chatview', {
                 }
             },
 
-            onMessageEditButtonClicked (ev) {
-                ev.preventDefault();
-
-                const idx = this.model.messages.findLastIndex('correcting'),
-                      currently_correcting = idx >=0 ? this.model.messages.at(idx) : null,
-                      message_el = u.ancestor(ev.target, '.chat-msg'),
-                      message = this.model.messages.findWhere({'msgid': message_el.getAttribute('data-msgid')});
-
-                const textarea = this.el.querySelector('.chat-textarea');
-                if (textarea.value &&
-                        ((currently_correcting === null) || currently_correcting.get('message') !== textarea.value)) {
+            onMessageEditButtonClicked (message) {
+                const currently_correcting = this.model.messages.findWhere('correcting');
+                const unsent_text = this.el.querySelector('.chat-textarea')?.value;
+                if (unsent_text && (!currently_correcting || currently_correcting.get('message') !== unsent_text)) {
                     if (! confirm(__("You have an unsent message which will be lost if you continue. Are you sure?"))) {
                         return;
                     }
                 }
 
                 if (currently_correcting !== message) {
-                    if (currently_correcting !== null) {
-                        currently_correcting.save('correcting', false);
-                    }
+                    currently_correcting?.save('correcting', false);
                     message.save('correcting', true);
                     this.insertIntoTextArea(u.prefixMentions(message), true, true);
                 } else {

+ 1 - 8
src/converse-muc-views.js

@@ -437,8 +437,6 @@ converse.plugins.add('converse-muc-views', {
             is_chatroom: true,
             events: {
                 'change input.fileupload': 'onFileSelection',
-                'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
-                'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
                 'click .chatbox-navback': 'showControlBox',
                 'click .hide-occupants': 'hideOccupants',
                 'click .new-msgs-indicator': 'viewUnreadMessages',
@@ -784,12 +782,7 @@ converse.plugins.add('converse-muc-views', {
                 return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
             },
 
-            async onMessageRetractButtonClicked (ev) {
-                ev.preventDefault();
-                const msg_el = u.ancestor(ev.target, '.message');
-                const msgid = msg_el.getAttribute('data-msgid');
-                const time = msg_el.getAttribute('data-isodate');
-                const message = this.model.messages.findWhere({msgid, time});
+            async onMessageRetractButtonClicked (message) {
                 const retraction_warning =
                     __("Be aware that other XMPP/Jabber clients (and servers) may "+
                         "not yet support retractions and that this message may not "+

+ 11 - 0
src/modals/message-versions.js

@@ -0,0 +1,11 @@
+import { BootstrapModal } from "../converse-modal.js";
+import tpl_message_versions_modal from "../templates/message_versions_modal.js";
+
+
+export default BootstrapModal.extend({
+    // FIXME: this isn't globally unique
+    id: "message-versions-modal",
+    toHTML () {
+        return tpl_message_versions_modal(this.model.toJSON());
+    }
+});

+ 3 - 3
src/templates/directives/retraction.js

@@ -3,8 +3,8 @@ import { __ } from '@converse/headless/i18n';
 
 
 const i18n_retract_message = __('Retract this message');
-const tpl_retract = html`
-    <button class="chat-msg__action chat-msg__action-retract" title="${i18n_retract_message}">
+const tpl_retract = (o) => html`
+    <button class="chat-msg__action chat-msg__action-retract" title="${i18n_retract_message}" @click=${o.onMessageRetractButtonClicked}>
         <fa-icon class="fas fa-trash-alt" path-prefix="/node_modules" color="var(--text-color-lighten-15-percent)" size="1em"></fa-icon>
     </button>
 `;
@@ -15,7 +15,7 @@ export const renderRetractionLink = directive(o => async part => {
     const retractable = !o.is_retracted && (o.model.mayBeRetracted() || may_be_moderated);
 
     if (retractable) {
-        part.setValue(tpl_retract);
+        part.setValue(tpl_retract(o));
     } else {
         part.setValue('');
     }

+ 0 - 17
src/templates/info.js

@@ -1,17 +0,0 @@
-import { html } from "lit-html";
-import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
-import { __ } from '@converse/headless/i18n';
-
-const i18n_retry = __('Retry');
-
-
-export default (o) => html`
-    <div class="message chat-info ${o.extra_classes}"
-         data-isodate="${o.isodate}"
-         data-type="${o.data_name}"
-         data-value="${o.data_value}">
-
-        ${ o.render_message ? unsafeHTML(o.message) : o.message }
-        ${ o.retry ? html`<a class="retry" @click=${o.onRetryClicked}>${i18n_retry}</a>` : '' }
-    </div>
-`;

+ 3 - 2
src/templates/message.js

@@ -3,11 +3,12 @@ import { html } from 'lit-element';
 
 export default (o) => html`
     <converse-chat-message
-        .model=${o.model}
+        .chatview=${o.chatview}
         .hats=${o.hats}
-        ?retractable=${o.retractable}
+        .model=${o.model}
         ?correcting=${o.model.get('correcting')}
         ?editable=${o.model.get('editable')}
+        ?retractable=${o.retractable}
         ?has_mentions=${o.has_mentions}
         ?is_delayed=${o.model.get('is_delayed')}
         ?is_encrypted=${o.model.get('is_encrypted')}