瀏覽代碼

Refactor message component to require less attributes

JC Brand 4 年之前
父節點
當前提交
3558936b46

+ 2 - 3
spec/http-file-upload.js

@@ -422,14 +422,13 @@ describe("XEP-0363: HTTP File Upload", function () {
                 }));
 
                 it("shows an error message if the file is too large",
-                        mock.initConverse([], {}, async function (done, _converse) {
+                        mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
 
                     const IQ_stanzas = _converse.connection.IQ_stanzas;
                     const IQ_ids =  _converse.connection.IQ_ids;
 
                     await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
-                    await u.waitUntil(() => _.filter(
-                        IQ_stanzas,
+                    await u.waitUntil(() => IQ_stanzas.filter(
                         iq => iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]')).length
                     );
 

+ 2 - 5
spec/mock.js

@@ -42,11 +42,8 @@ window.addEventListener('converse-loaded', () => {
     const { u, sizzle, Strophe, dayjs, $iq, $msg, $pres } = converse.env;
 
     mock.waitUntilDiscoConfirmed = async function (_converse, entity_jid, identities, features=[], items=[], type='info') {
-        const iq = await u.waitUntil(() => {
-            return _converse.connection.IQ_stanzas.filter(
-                iq => sizzle(`iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`, iq).length
-            ).pop();
-        }, 300);
+        const sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`;
+        const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop(), 300);
         const stanza = $iq({
             'type': 'result',
             'from': entity_jid,

+ 3 - 3
src/plugins/chatview/tests/messages.js

@@ -867,7 +867,7 @@ describe("A Chat Message", function () {
         expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
             "A delayed message, sent 5 minutes since we started");
 
-        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(true);
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(false);
         expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
             "Another message 14 minutes since we started");
 
@@ -900,10 +900,10 @@ describe("A Chat Message", function () {
         expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(4)))).toBe(false);
         expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
             "A carbon message 4 minutes later");
-        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(false);
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(true);
         expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
             "A delayed message, sent 5 minutes since we started");
-        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(true);
+        expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(false);
         expect(view.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
             "Another message 14 minutes since we started");
         expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(7)))).toBe(true);

+ 4 - 62
src/shared/chat/message-history.js

@@ -6,48 +6,6 @@ import { _converse, api } from "@converse/headless/core";
 import { html } from 'lit-element';
 import { repeat } from 'lit-html/directives/repeat.js';
 
-const tpl_message = (o) => html`
-    <converse-chat-message
-        .hats=${o.hats}
-        .model=${o.model}
-        ?correcting=${o.correcting}
-        ?editable=${o.editable}
-        ?has_mentions=${o.has_mentions}
-        ?is_delayed=${o.is_delayed}
-        ?is_encrypted=${!!o.is_encrypted}
-        ?is_first_unread=${o.is_first_unread}
-        ?is_me_message=${o.is_me_message}
-        ?is_only_emojis=${o.is_only_emojis}
-        ?is_retracted=${o.is_retracted}
-        ?is_spoiler=${o.is_spoiler}
-        ?is_spoiler_visible=${o.is_spoiler_visible}
-        ?retractable=${o.retractable}
-        edited=${o.edited || ''}
-        error=${o.error || ''}
-        error_text=${o.error_text || ''}
-        filename=${o.filename || ''}
-        filesize=${o.filesize || ''}
-        from=${o.from}
-        message_type=${o.type || ''}
-        moderated_by=${o.moderated_by || ''}
-        moderation_reason=${o.moderation_reason || ''}
-        msgid=${o.msgid}
-        occupant_affiliation=${o.model.occupant ? o.model.occupant.get('affiliation') : ''}
-        occupant_role=${o.model.occupant ? o.model.occupant.get('role') : ''}
-        oob_url=${o.oob_url || ''}
-        pretty_type=${o.pretty_type}
-        progress=${o.progress || 0 }
-        reason=${o.reason || ''}
-        received=${o.received || ''}
-        retry_event_id=${o.retry_event_id || ''}
-        sender=${o.sender}
-        spoiler_hint=${o.spoiler_hint || ''}
-        subject=${o.subject || ''}
-        time=${o.time}
-        unfurl_metadata=${o.unfurl_metadata}
-        username=${o.username}></converse-chat-message>
-`;
-
 
 // Return a TemplateResult indicating a new day if the passed in message is
 // more than a day later than its predecessor.
@@ -88,19 +46,6 @@ _converse.getHats = function (model) {
 }
 
 
-export function getDerivedMessageProps (chatbox, model) {
-    const is_groupchat = model.get('type') === 'groupchat';
-    return {
-        'has_mentions': is_groupchat && model.get('sender') === 'them' && chatbox.isUserMentioned(model),
-        'hats': _converse.getHats(model),
-        'is_first_unread': chatbox.get('first_unread_id') === model.get('id'),
-        'is_me_message': model.isMeCommand(),
-        'is_retracted': model.get('retracted') || model.get('moderated') === 'retracted',
-        'username': model.getDisplayName(),
-    }
-}
-
-
 export default class MessageHistory extends CustomElement {
 
     static get properties () {
@@ -121,13 +66,10 @@ export default class MessageHistory extends CustomElement {
         }
         const day = getDayIndicator(model);
         const templates = day ? [day] : [];
-        const message = tpl_message(
-            Object.assign(
-                model.toJSON(),
-                getDerivedMessageProps(this.model, model),
-                { model }
-            )
-        );
+        const message = html`<converse-chat-message
+            jid="${this.model.get('jid')}"
+            mid="${model.get('id')}"></converse-chat-message>`
+
         return [...templates, message];
     }
 }

+ 105 - 98
src/shared/chat/message.js

@@ -12,7 +12,6 @@ import tpl_spinner from 'templates/spinner.js';
 import { CustomElement } from 'shared/components/element.js';
 import { __ } from 'i18n';
 import { _converse, api, converse } from  '@converse/headless/core';
-import { getDerivedMessageProps } from './message-history';
 import { html } from 'lit-element';
 import { renderAvatar } from 'shared/directives/avatar';
 
@@ -24,53 +23,19 @@ export default class Message extends CustomElement {
 
     static get properties () {
         return {
-            correcting: { type: Boolean },
-            editable: { type: Boolean },
-            edited: { type: String },
-            error: { type: String },
-            error_text: { type: String },
-            from: { type: String },
-            has_mentions: { type: Boolean },
-            hats: { type: Array },
-            is_delayed: { type: Boolean },
-            is_encrypted: { type: Boolean },
-            is_first_unread: { type: Boolean },
-            is_me_message: { type: Boolean },
-            is_only_emojis: { type: Boolean },
-            is_retracted: { type: Boolean },
-            is_spoiler: { type: Boolean },
-            is_spoiler_visible: { type: Boolean },
-            message_type: { type: String },
-            model: { type: Object },
-            moderated_by: { type: String },
-            moderation_reason: { type: String },
-            msgid: { type: String },
-            occupant_affiliation: { type: String },
-            occupant_role: { type: String },
-            oob_url: { type: String },
-            progress: { type: Number },
-            reason: { type: String },
-            received: { type: String },
-            retractable: { type: Boolean },
-            retry_event_id: { type: String },
-            sender: { type: String },
-            show_spinner: { type: Boolean },
-            spoiler_hint: { type: String },
-            subject: { type: String },
-            time: { type: String },
-            unfurl_metadata: { type: String },
-            username: { type: String }
+            jid: { type: String },
+            mid: { type: String }
         }
     }
 
     render () {
         const format = api.settings.get('time_format');
-        this.pretty_time = dayjs(this.edited || this.time).format(format);
+        this.pretty_time = dayjs(this.model.get('edited') || this.model.get('time')).format(format);
         if (this.show_spinner) {
             return tpl_spinner();
         } else if (this.model.get('file') && !this.model.get('oob_url')) {
             return this.renderFileProgress();
-        } else if (['error', 'info'].includes(this.message_type)) {
+        } else if (['error', 'info'].includes(this.model.get('type'))) {
             return this.renderInfoMessage();
         } else {
             return this.renderChatMessage();
@@ -79,24 +44,35 @@ export default class Message extends CustomElement {
 
     connectedCallback () {
         super.connectedCallback();
-        // Listen to changes and update properties (which will trigger a
-        // re-render if necessary).
-        this.listenTo(this.model, 'change', (model) => {
-            const chatbox = this.model.collection.chatbox;
-            Object.assign(this, getDerivedMessageProps(chatbox, this.model));
-            Object.keys(model.changed)
-                .filter(p => Object.keys(Message.properties).includes(p))
-                .forEach(p => (this[p] = model.changed[p]));
-        });
-        const vcard = this.model.vcard;
-        vcard && this.listenTo(vcard, 'change', () => this.requestUpdate());
+        this.chatbox = _converse.chatboxes.get(this.jid);
+        this.model = this.chatbox.messages.get(this.mid);
+
+        this.listenTo(this.model, 'change', () => this.requestUpdate());
+        this.model.vcard && this.listenTo(this.model.vcard, 'change', () => this.requestUpdate());
+
+        if (this.model.get('type') === 'groupchat') {
+            if (this.model.occupant) {
+                this.listenTo(this.model.occupant, 'change', () => this.requestUpdate());
+            } else {
+                this.listenTo(this.model, 'occupantAdded', () => {
+                    this.listenTo(this.model.occupant, 'change', () => this.requestUpdate())
+                });
+            }
+        }
+    }
+
+    getProps () {
+        return Object.assign(
+            this.model.toJSON(),
+            this.getDerivedMessageProps()
+        );
     }
 
     renderInfoMessage () {
         const isodate = dayjs(this.model.get('time')).toISOString();
         const i18n_retry = __('Retry');
         return html`
-            <div class="message chat-info chat-${this.message_type}"
+            <div class="message chat-info chat-${this.model.get('type')}"
                 data-isodate="${isodate}"
                 data-type="${this.data_name}"
                 data-value="${this.data_value}">
@@ -104,11 +80,10 @@ export default class Message extends CustomElement {
                 <div class="chat-info__message">
                     ${ this.model.getMessageText() }
                 </div>
-                ${ this.reason ? html`<q class="reason">${this.reason}</q>` : `` }
-                ${ this.error_text ? html`<q class="reason">${this.error_text}</q>` : `` }
-                ${ this.retry_event_id ? html`<a class="retry" @click=${this.onRetryClicked}>${i18n_retry}</a>` : '' }
-            </div>
-        `;
+                ${ this.model.get('reason') ? html`<q class="reason">${this.model.get('reason')}</q>` : `` }
+                ${ this.model.get('error_text') ? html`<q class="reason">${this.model.get('error_text')}</q>` : `` }
+                ${ this.model.get('retry_event_id') ? html`<a class="retry" @click=${this.onRetryClicked}>${i18n_retry}</a>` : '' }
+            </div>`;
     }
 
     renderFileProgress () {
@@ -120,17 +95,17 @@ export default class Message extends CustomElement {
                 ${ renderAvatar(this.getAvatarData()) }
                 <div class="chat-msg__content">
                     <span class="chat-msg__text">${i18n_uploading} <strong>${filename}</strong>, ${size}</span>
-                    <progress value="${this.progress}"/>
+                    <progress value="${this.model.get('progress')}"/>
                 </div>
             </div>`;
     }
 
     renderChatMessage () {
-        return tpl_message(this);
+        return tpl_message(this, this.getProps());
     }
 
     shouldShowAvatar () {
-        return api.settings.get('show_message_avatar') && !this.is_me_message && this.type !== 'headline';
+        return api.settings.get('show_message_avatar') && !this.model.isMeCommand() && this.type !== 'headline';
     }
 
     getAvatarData () {
@@ -156,7 +131,8 @@ export default class Message extends CustomElement {
 
     async onRetryClicked () {
         this.show_spinner = true;
-        await api.trigger(this.retry_event_id, {'synchronous': true});
+        this.requestUpdate();
+        await api.trigger(this.model.get('retry_event_id'), {'synchronous': true});
         this.model.destroy();
         this.parentElement.removeChild(this);
     }
@@ -168,40 +144,69 @@ export default class Message extends CustomElement {
         if (prev_model === null) {
             return false;
         }
-        const date = dayjs(this.time);
-        return this.from === prev_model.get('from') &&
-            !this.is_me_message &&
+        const date = dayjs(this.model.get('time'));
+        return this.model.get('from') === prev_model.get('from') &&
+            !this.model.isMeCommand() &&
             !prev_model.isMeCommand() &&
-            this.message_type !== 'info' &&
+            this.model.get('type') !== 'info' &&
             prev_model.get('type') !== 'info' &&
             date.isBefore(dayjs(prev_model.get('time')).add(10, 'minutes')) &&
-            !!this.is_encrypted === !!prev_model.get('is_encrypted');
+            !!this.model.get('is_encrypted') === !!prev_model.get('is_encrypted');
+    }
+
+    isRetracted () {
+        return this.model.get('retracted') || this.model.get('moderated') === 'retracted';
+    }
+
+    hasMentions () {
+        const is_groupchat = this.model.get('type') === 'groupchat';
+        return is_groupchat && this.model.get('sender') === 'them' && this.chatbox.isUserMentioned(this.model);
+    }
+
+    getOccupantAffiliation () {
+        return this.model.occupant?.get('affiliation');
+    }
+
+    getOccupantRole () {
+        return this.model.occupant?.get('role');
     }
 
     getExtraMessageClasses () {
         const extra_classes = [
             this.isFollowup() ? 'chat-msg--followup' : null,
-            this.is_delayed ? 'delayed' : null,
-            this.is_me_message ? 'chat-msg--action' : null,
-            this.is_retracted ? 'chat-msg--retracted' : null,
-            this.message_type,
+            this.model.get('is_delayed') ? 'delayed' : null,
+            this.model.isMeCommand() ? 'chat-msg--action' : null,
+            this.isRetracted() ? 'chat-msg--retracted' : null,
+            this.model.get('type'),
             this.shouldShowAvatar() ? 'chat-msg--with-avatar' : null,
         ].map(c => c);
 
-        if (this.message_type === 'groupchat') {
-            this.occupant_role && extra_classes.push(this.occupant_role);
-            this.occupant_affiliation && extra_classes.push(this.occupant_affiliation);
-            if (this.sender === 'them' && this.has_mentions) {
+        if (this.model.get('type') === 'groupchat') {
+            extra_classes.push(this.getOccupantRole() ?? '');
+            extra_classes.push(this.getOccupantAffiliation() ?? '');
+            if (this.model.get('sender') === 'them' && this.hasMentions()) {
                 extra_classes.push('mentioned');
             }
         }
-        this.correcting && extra_classes.push('correcting');
+        this.model.get('correcting') && extra_classes.push('correcting');
         return extra_classes.filter(c => c).join(" ");
     }
 
+    getDerivedMessageProps () {
+        return {
+            'has_mentions': this.hasMentions(),
+            'hats': _converse.getHats(this.model),
+            'is_first_unread': this.chatbox.get('first_unread_id') === this.model.get('id'),
+            'is_me_message': this.model.isMeCommand(),
+            'is_retracted': this.isRetracted(),
+            'username': this.model.getDisplayName(),
+            'should_show_avatar': this.shouldShowAvatar(),
+        }
+    }
+
     getRetractionText () {
-        if (this.message_type === 'groupchat' && this.moderated_by) {
-            const retracted_by_mod = this.moderated_by;
+        if (this.model.get('type') === 'groupchat' && this.model.get('moderated_by')) {
+            const retracted_by_mod = this.model.get('moderated_by');
             const chatbox = this.model.collection.chatbox;
             if (!this.model.mod) {
                 this.model.mod =
@@ -216,60 +221,62 @@ export default class Message extends CustomElement {
     }
 
     renderRetraction () {
-        const retraction_text = this.is_retracted ? this.getRetractionText() : null;
+        const retraction_text = this.isRetracted() ? this.getRetractionText() : null;
         return html`
             <div>${retraction_text}</div>
-            ${ this.moderation_reason ? html`<q class="chat-msg--retracted__reason">${this.moderation_reason}</q>` : '' }
+            ${ this.model.get('moderation_reason') ?
+                    html`<q class="chat-msg--retracted__reason">${this.model.get('moderation_reason')}</q>` : '' }
         `;
     }
 
     renderMessageText () {
         const i18n_edited = __('This message has been edited');
         const i18n_show = __('Show more');
-        const is_groupchat_message = (this.message_type === 'groupchat');
+        const is_groupchat_message = (this.model.get('type') === 'groupchat');
         const i18n_show_less = __('Show less');
 
         const tpl_spoiler_hint = html`
             <div class="chat-msg__spoiler-hint">
-                <span class="spoiler-hint">${this.spoiler_hint}</span>
+                <span class="spoiler-hint">${this.model.get('spoiler_hint')}</span>
                 <a class="badge badge-info spoiler-toggle" href="#" @click=${this.toggleSpoilerMessage}>
-                    <i class="fa ${this.is_spoiler_visible ? 'fa-eye-slash' : 'fa-eye'}"></i>
-                    ${ this.is_spoiler_visible ? i18n_show_less : i18n_show }
+                    <i class="fa ${this.model.get('is_spoiler_visible') ? 'fa-eye-slash' : 'fa-eye'}"></i>
+                    ${ this.model.get('is_spoiler_visible') ? i18n_show_less : i18n_show }
                 </a>
             </div>
         `;
-        const spoiler_classes = this.is_spoiler ? `spoiler ${this.is_spoiler_visible ? '' : 'hidden'}` : '';
+        const spoiler_classes = this.model.get('is_spoiler') ? `spoiler ${this.model.get('is_spoiler_visible') ? '' : 'hidden'}` : '';
+        const text = this.model.getMessageText();
         return html`
-            ${ this.is_spoiler ? tpl_spoiler_hint : '' }
-            ${ this.subject ? html`<div class="chat-msg__subject">${this.subject}</div>` : '' }
+            ${ this.model.get('is_spoiler') ? tpl_spoiler_hint : '' }
+            ${ this.model.get('subject') ? html`<div class="chat-msg__subject">${this.model.get('subject')}</div>` : '' }
             <span>
                 <converse-chat-message-body
-                    class="chat-msg__text ${this.is_only_emojis ? 'chat-msg__text--larger' : ''} ${spoiler_classes}"
+                    class="chat-msg__text ${this.model.get('is_only_emojis') ? 'chat-msg__text--larger' : ''} ${spoiler_classes}"
                     .model="${this.model}"
-                    ?is_me_message="${this.is_me_message}"
-                    ?is_only_emojis="${this.is_only_emojis}"
-                    ?is_spoiler="${this.is_spoiler}"
-                    ?is_spoiler_visible="${this.is_spoiler_visible}"
-                    text="${this.model.getMessageText()}"></converse-chat-message-body>
-                ${ (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>` : '' }
+                    ?is_me_message="${this.model.isMeCommand()}"
+                    ?is_only_emojis="${this.model.get('is_only_emojis')}"
+                    ?is_spoiler="${this.model.get('is_spoiler')}"
+                    ?is_spoiler_visible="${this.model.get('is_spoiler_visible')}"
+                    text="${text}"></converse-chat-message-body>
+                ${ (this.model.get('received') && !this.model.isMeCommand() && !is_groupchat_message) ? html`<span class="fa fa-check chat-msg__receipt"></span>` : '' }
+                ${ (this.model.get('edited')) ? html`<i title="${ i18n_edited }" class="fa fa-edit chat-msg__edit-modal" @click=${this.showMessageVersionsModal}></i>` : '' }
             </span>
-            ${ this.oob_url ? html`<div class="chat-msg__media">${u.getOOBURLMarkup(_converse, this.oob_url)}</div>` : '' }
-            <div class="chat-msg__error">${ this.error_text || this.error }</div>
+            ${ this.model.get('oob_url') ? html`<div class="chat-msg__media">${u.getOOBURLMarkup(_converse, this.model.get('oob_url'))}</div>` : '' }
+            <div class="chat-msg__error">${ this.model.get('error_text') || this.model.get('error') }</div>
         `;
     }
 
     renderAvatarByline () {
         return html`
-            ${ this.hats.map(h => html`<span class="badge badge-secondary">${h.title}</span>`) }
-            <time timestamp="${this.edited || this.time}" class="chat-msg__time">${this.pretty_time}</time>
+            ${ _converse.getHats(this.model).map(h => html`<span class="badge badge-secondary">${h.title}</span>`) }
+            <time timestamp="${this.model.get('edited') || this.model.get('time')}" class="chat-msg__time">${this.pretty_time}</time>
         `;
     }
 
     showUserModal (ev) {
         if (this.model.get('sender') === 'me') {
             _converse.xmppstatusview.showProfileModal(ev);
-        } else if (this.message_type === 'groupchat') {
+        } else if (this.model.get('type') === 'groupchat') {
             ev.preventDefault();
             api.modal.show(OccupantModal, { 'model': this.model.occupant }, ev);
         } else {

+ 13 - 13
src/shared/chat/templates/message.js

@@ -4,11 +4,11 @@ import { html } from "lit-html";
 import { renderAvatar } from 'shared/directives/avatar';
 
 
-export default (o) => {
+export default (el, o) => {
     const i18n_new_messages = __('New messages');
     return html`
         ${ o.is_first_unread ? html`<div class="message separator"><hr class="separator"><span class="separator-text">${ i18n_new_messages }</span></div>` : '' }
-        <div class="message chat-msg ${ o.getExtraMessageClasses() }"
+        <div class="message chat-msg ${ el.getExtraMessageClasses() }"
                 data-isodate="${o.time}"
                 data-msgid="${o.msgid}"
                 data-from="${o.from}"
@@ -17,13 +17,13 @@ export default (o) => {
             <!-- Anchor to allow us to scroll the message into view -->
             <a id="${o.msgid}"></a>
 
-            <a class="show-msg-author-modal" @click=${o.showUserModal}>${ o.shouldShowAvatar() ? renderAvatar(o.getAvatarData()) : '' }</a>
+            <a class="show-msg-author-modal" @click=${el.showUserModal}>${ o.should_show_avatar ? renderAvatar(el.getAvatarData()) : '' }</a>
             <div class="chat-msg__content chat-msg__content--${o.sender} ${o.is_me_message ? 'chat-msg__content--action' : ''}">
 
                 ${ !o.is_me_message ? html`
                     <span class="chat-msg__heading">
-                        <span class="chat-msg__author"><a class="show-msg-author-modal" @click=${o.showUserModal}>${o.username}</a></span>
-                        ${ o.renderAvatarByline() }
+                        <span class="chat-msg__author"><a class="show-msg-author-modal" @click=${el.showUserModal}>${o.username}</a></span>
+                        ${ el.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' : '' }">
@@ -31,23 +31,23 @@ export default (o) => {
                         ${ (o.is_me_message) ? html`
                             <time timestamp="${o.edited || o.time}" class="chat-msg__time">${o.pretty_time}</time>&nbsp;
                             <span class="chat-msg__author">${ o.is_me_message ? '**' : ''}${o.username}</span>&nbsp;` : '' }
-                        ${ o.is_retracted ? o.renderRetraction() : o.renderMessageText() }
+                        ${ o.is_retracted ? el.renderRetraction() : el.renderMessageText() }
                     </div>
                     <converse-message-actions
-                        .model=${o.model}
+                        .model=${el.model}
                         ?correcting="${o.correcting}"
                         ?editable="${o.editable}"
                         ?is_retracted="${o.is_retracted}"
-                        ?hide_url_previews="${o.model.get('hide_url_previews')}"
-                        unfurls="${o.model.get('ogp_metadata')?.length}"
+                        ?hide_url_previews="${el.model.get('hide_url_previews')}"
+                        unfurls="${el.model.get('ogp_metadata')?.length}"
                         message_type="${o.message_type}"></converse-message-actions>
                 </div>
 
-                ${ !o.model.get('hide_url_previews') ? o.model.get('ogp_metadata')?.map(m =>
+                ${ !el.model.get('hide_url_previews') ? el.model.get('ogp_metadata')?.map(m =>
                     html`<converse-message-unfurl
-                        @animationend="${o.onUnfurlAnimationEnd}"
-                        class="${o.model.get('url_preview_transition')}"
-                        jid="${o.model.collection.chatbox?.get('jid')}"
+                        @animationend="${el.onUnfurlAnimationEnd}"
+                        class="${el.model.get('url_preview_transition')}"
+                        jid="${el.chatbox?.get('jid')}"
                         description="${m['og:description'] || ''}"
                         title="${m['og:title'] || ''}"
                         image="${m['og:image'] || ''}"