瀏覽代碼

Don't pass chatview object to `converse-chat-content`

and any child components.

This makes it easier to use these components independently of one
another and the overarching view.
JC Brand 4 年之前
父節點
當前提交
95c14e5a26

+ 4 - 1
spec/corrections.js

@@ -146,6 +146,9 @@ describe("A Chat Message", function () {
             keyCode: 13 // Enter
             keyCode: 13 // Enter
         });
         });
         await u.waitUntil(() => textarea.value === '');
         await u.waitUntil(() => textarea.value === '');
+        await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-msg__text')).filter(
+            m => m.textContent === 'It is the east, and Juliet is the sun.').length);
+
         const messages = view.querySelectorAll('.chat-msg');
         const messages = view.querySelectorAll('.chat-msg');
         expect(messages.length).toBe(3);
         expect(messages.length).toBe(3);
         expect(messages[0].querySelector('.chat-msg__text').textContent)
         expect(messages[0].querySelector('.chat-msg__text').textContent)
@@ -244,9 +247,9 @@ describe("A Chat Message", function () {
         action = view.querySelector('.chat-msg .chat-msg__action');
         action = view.querySelector('.chat-msg .chat-msg__action');
         action.style.opacity = 1;
         action.style.opacity = 1;
         action.click();
         action.click();
-        expect(textarea.value).toBe('');
         expect(view.model.messages.at(0).get('correcting')).toBe(false);
         expect(view.model.messages.at(0).get('correcting')).toBe(false);
         expect(view.querySelectorAll('.chat-msg').length).toBe(1);
         expect(view.querySelectorAll('.chat-msg').length).toBe(1);
+        expect(textarea.value).toBe('');
         await u.waitUntil(() => (u.hasClass('correcting', view.querySelector('.chat-msg')) === false), 500);
         await u.waitUntil(() => (u.hasClass('correcting', view.querySelector('.chat-msg')) === false), 500);
 
 
         // Test that messages from other users don't have the pencil icon
         // Test that messages from other users don't have the pencil icon

+ 31 - 16
src/components/chat_content.js

@@ -1,41 +1,56 @@
 import "./message-history";
 import "./message-history";
-import xss from "xss/dist/xss";
 import { CustomElement } from './element.js';
 import { CustomElement } from './element.js';
+import { _converse, api } from "@converse/headless/core";
 import { html } from 'lit-element';
 import { html } from 'lit-element';
-import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
-import { api } from "@converse/headless/core";
 
 
 export default class ChatContent extends CustomElement {
 export default class ChatContent extends CustomElement {
 
 
     static get properties () {
     static get properties () {
         return {
         return {
-            chatview: { type: Object}
+            jid: { type: String }
         }
         }
     }
     }
 
 
     connectedCallback () {
     connectedCallback () {
         super.connectedCallback();
         super.connectedCallback();
-        const model = this.chatview.model;
-        this.listenTo(model.messages, 'add', this.requestUpdate);
-        this.listenTo(model.messages, 'change', this.requestUpdate);
-        this.listenTo(model.messages, 'remove', this.requestUpdate);
-        this.listenTo(model.messages, 'reset', this.requestUpdate);
-        this.listenTo(model.notifications, 'change', this.requestUpdate);
-        if (model.occupants) {
-            this.listenTo(model.occupants, 'change', this.requestUpdate);
+        this.model = _converse.chatboxes.get(this.jid);
+        this.listenTo(this.model.messages, 'add', this.requestUpdate);
+        this.listenTo(this.model.messages, 'change', this.requestUpdate);
+        this.listenTo(this.model.messages, 'remove', this.requestUpdate);
+        this.listenTo(this.model.messages, 'reset', this.requestUpdate);
+        this.listenTo(this.model.notifications, 'change', this.requestUpdate);
+        if (this.model.occupants) {
+            this.listenTo(this.model.occupants, 'change', this.requestUpdate);
         }
         }
+
+        // We jot down whether we were scrolled down before rendering, because when an
+        // image loads, it triggers 'scroll' and the chat will be marked as scrolled,
+        // which is technically true, but not what we want because the user
+        // didn't initiate the scrolling.
+        this.was_scrolled_up = this.model.get('scrolled');
+        this.addEventListener('imageLoaded', () => {
+            !this.was_scrolled_up && this.scrollDown();
+        });
     }
     }
 
 
     render () {
     render () {
-        const notifications = xss.filterXSS(this.chatview.getNotifications(), {'whiteList': {}});
         return html`
         return html`
             <converse-message-history
             <converse-message-history
-                .chatview=${this.chatview}
-                .messages=${[...this.chatview.model.messages.models]}>
+                .model=${this.model}
+                .messages=${[...this.model.messages.models]}>
             </converse-message-history>
             </converse-message-history>
-            <div class="chat-content__notifications">${unsafeHTML(notifications)}</div>
+            <div class="chat-content__notifications">${this.model.getNotificationsText()}</div>
         `;
         `;
     }
     }
+
+    scrollDown () {
+        if (this.scrollTo) {
+            const behavior = this.scrollTop ? 'smooth' : 'auto';
+            this.scrollTo({ 'top': this.scrollHeight, behavior });
+        } else {
+            this.scrollTop = this.scrollHeight;
+        }
+    }
 }
 }
 
 
 api.elements.define('converse-chat-content', ChatContent);
 api.elements.define('converse-chat-content', ChatContent);

+ 116 - 14
src/components/message-actions.js

@@ -1,15 +1,17 @@
+import log from '@converse/headless/log';
 import { CustomElement } from './element.js';
 import { CustomElement } from './element.js';
 import { __ } from '../i18n';
 import { __ } from '../i18n';
-import { api } from "@converse/headless/core";
+import { _converse, api, converse } from "@converse/headless/core";
 import { html } from 'lit-element';
 import { html } from 'lit-element';
 import { until } from 'lit-html/directives/until.js';
 import { until } from 'lit-html/directives/until.js';
 
 
+const { Strophe, u } = converse.env;
+
 
 
 class MessageActions extends CustomElement {
 class MessageActions extends CustomElement {
 
 
     static get properties () {
     static get properties () {
         return {
         return {
-            chatview: { type: Object },
             model: { type: Object },
             model: { type: Object },
             editable: { type: Boolean },
             editable: { type: Boolean },
             correcting: { type: Boolean },
             correcting: { type: Boolean },
@@ -22,6 +24,16 @@ class MessageActions extends CustomElement {
         return html`${ until(this.renderActions(), '') }`;
         return html`${ until(this.renderActions(), '') }`;
     }
     }
 
 
+    async renderActions () {
+        const buttons = await this.getActionButtons();
+        const items = buttons.map(b => MessageActions.getActionsDropdownItem(b));
+        if (items.length) {
+            return html`<converse-dropdown class="chat-msg__actions" .items=${ items }></converse-dropdown>`;
+        } else {
+            return '';
+        }
+    }
+
     static getActionsDropdownItem (o) {
     static getActionsDropdownItem (o) {
         return html`
         return html`
             <button class="chat-msg__action ${o.button_class}" @click=${o.handler}>
             <button class="chat-msg__action ${o.button_class}" @click=${o.handler}>
@@ -36,12 +48,112 @@ class MessageActions extends CustomElement {
 
 
     onMessageEditButtonClicked (ev) {
     onMessageEditButtonClicked (ev) {
         ev.preventDefault();
         ev.preventDefault();
-        this.chatview.onMessageEditButtonClicked(this.model);
+        const currently_correcting = this.model.collection.findWhere('correcting');
+        // TODO: Use state intead of DOM querying
+        // Then this code can also be put on the model
+        const unsent_text = u.ancestor(this, '.chatbox')?.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 !== this.model) {
+            currently_correcting?.save('correcting', false);
+            this.model.save('correcting', true);
+        } else {
+            this.model.save('correcting', false);
+        }
+    }
+
+    async onDirectMessageRetractButtonClicked () {
+        if (this.model.get('sender') !== 'me') {
+            return log.error("onMessageRetractButtonClicked called for someone else's this.model!");
+        }
+        const retraction_warning = __(
+            'Be aware that other XMPP/Jabber clients (and servers) may ' +
+            'not yet support retractions and that this this.model may not ' +
+            'be removed everywhere.'
+        );
+        const messages = [__('Are you sure you want to retract this this.model?')];
+        if (api.settings.get('show_retraction_warning')) {
+            messages[1] = retraction_warning;
+        }
+        const result = await api.confirm(__('Confirm'), messages);
+        if (result) {
+            const chatbox = this.model.collection.chatbox;
+            chatbox.retractOwnMessage(this.model);
+        }
+    }
+
+    /**
+     * Retract someone else's message in this groupchat.
+     * @private
+     * @param { _converse.Message } message - The message which we're retracting.
+     * @param { string } [reason] - The reason for retracting the message.
+     */
+    async retractOtherMessage (reason) {
+        const chatbox = this.model.collection.chatbox;
+        const result = await chatbox.retractOtherMessage(this.model, reason);
+        if (result === null) {
+            const err_msg = __(`A timeout occurred while trying to retract the message`);
+            api.alert('error', __('Error'), err_msg);
+            log(err_msg, Strophe.LogLevel.WARN);
+        } else if (u.isErrorStanza(result)) {
+            const err_msg = __(`Sorry, you're not allowed to retract this message.`);
+            api.alert('error', __('Error'), err_msg);
+            log(err_msg, Strophe.LogLevel.WARN);
+            log(result, Strophe.LogLevel.WARN);
+        }
+    }
+
+    async onMUCMessageRetractButtonClicked () {
+        const retraction_warning = __(
+            'Be aware that other XMPP/Jabber clients (and servers) may ' +
+            'not yet support retractions and that this this.model may not ' +
+            'be removed everywhere.'
+        );
+
+        if (this.model.mayBeRetracted()) {
+            const messages = [__('Are you sure you want to retract this this.model?')];
+            if (api.settings.get('show_retraction_warning')) {
+                messages[1] = retraction_warning;
+            }
+            if (await api.confirm(__('Confirm'), messages)) {
+                const chatbox = this.model.collection.chatbox;
+                chatbox.retractOwnMessage(this.model);
+            }
+        } else if (await this.model.mayBeModerated()) {
+            if (this.model.get('sender') === 'me') {
+                let messages = [__('Are you sure you want to retract this this.model?')];
+                if (api.settings.get('show_retraction_warning')) {
+                    messages = [messages[0], retraction_warning, messages[1]];
+                }
+                !!(await api.confirm(__('Confirm'), messages)) && this.retractOtherMessage();
+            } else {
+                let messages = [
+                    __('You are about to retract this this.model.'),
+                    __('You may optionally include a this.model, explaining the reason for the retraction.')
+                ];
+                if (api.settings.get('show_retraction_warning')) {
+                    messages = [messages[0], retraction_warning, messages[1]];
+                }
+                const reason = await api.prompt(__('this.model Retraction'), messages, __('Optional reason'));
+                reason !== false && this.retractOtherMessage(reason);
+            }
+        } else {
+            const err_msg = __(`Sorry, you're not allowed to retract this this.model`);
+            api.alert('error', __('Error'), err_msg);
+        }
     }
     }
 
 
     onMessageRetractButtonClicked (ev) {
     onMessageRetractButtonClicked (ev) {
         ev.preventDefault();
         ev.preventDefault();
-        this.chatview.onMessageRetractButtonClicked(this.model);
+        const chatbox = this.model.collection.chatbox;
+        if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
+            this.onMUCMessageRetractButtonClicked();
+        } else {
+            this.onDirectMessageRetractButtonClicked();
+        }
     }
     }
 
 
     async getActionButtons () {
     async getActionButtons () {
@@ -83,16 +195,6 @@ class MessageActions extends CustomElement {
          */
          */
         return api.hook('getMessageActionButtons', this, buttons);
         return api.hook('getMessageActionButtons', this, buttons);
     }
     }
-
-    async renderActions () {
-        const buttons = await this.getActionButtons();
-        const items = buttons.map(b => MessageActions.getActionsDropdownItem(b));
-        if (items.length) {
-            return html`<converse-dropdown class="chat-msg__actions" .items=${ items }></converse-dropdown>`;
-        } else {
-            return '';
-        }
-    }
 }
 }
 
 
 api.elements.define('converse-message-actions', MessageActions);
 api.elements.define('converse-message-actions', MessageActions);

+ 3 - 4
src/components/message-history.js

@@ -11,7 +11,6 @@ const i18n_no_history = __('No message history available.');
 
 
 const tpl_message = (o) => html`
 const tpl_message = (o) => html`
     <converse-chat-message
     <converse-chat-message
-        .chatview=${o.chatview}
         .hats=${o.hats}
         .hats=${o.hats}
         .model=${o.model}
         .model=${o.model}
         ?correcting=${o.correcting}
         ?correcting=${o.correcting}
@@ -108,7 +107,7 @@ export default class MessageHistory extends CustomElement {
 
 
     static get properties () {
     static get properties () {
         return {
         return {
-            chatview: { type: Object},
+            model: { type: Object},
             messages: { type: Array}
             messages: { type: Array}
         }
         }
     }
     }
@@ -129,8 +128,8 @@ export default class MessageHistory extends CustomElement {
         const message = tpl_message(
         const message = tpl_message(
             Object.assign(
             Object.assign(
                 model.toJSON(),
                 model.toJSON(),
-                getDerivedMessageProps(this.chatview.model, model),
-                { 'chatview': this.chatview, model }
+                getDerivedMessageProps(this.model, model),
+                { model }
             )
             )
         );
         );
         return [...templates, message];
         return [...templates, message];

+ 8 - 4
src/components/message.js

@@ -2,8 +2,9 @@ import '../shared/registry';
 import './dropdown.js';
 import './dropdown.js';
 import './message-actions.js';
 import './message-actions.js';
 import './message-body.js';
 import './message-body.js';
-import { getDerivedMessageProps } from './message-history';
 import MessageVersionsModal from '../modals/message-versions.js';
 import MessageVersionsModal from '../modals/message-versions.js';
+import OccupantModal from 'modals/occupant.js';
+import UserDetailsModal from 'modals/user-details.js';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
 import filesize from 'filesize';
 import filesize from 'filesize';
 import tpl_chat_message from '../templates/chat_message.js';
 import tpl_chat_message from '../templates/chat_message.js';
@@ -11,6 +12,7 @@ import tpl_spinner from '../templates/spinner.js';
 import { CustomElement } from './element.js';
 import { CustomElement } from './element.js';
 import { __ } from '../i18n';
 import { __ } from '../i18n';
 import { _converse, api, converse } from  '@converse/headless/core';
 import { _converse, api, converse } from  '@converse/headless/core';
+import { getDerivedMessageProps } from './message-history';
 import { html } from 'lit-element';
 import { html } from 'lit-element';
 import { renderAvatar } from 'templates/directives/avatar';
 import { renderAvatar } from 'templates/directives/avatar';
 
 
@@ -22,7 +24,6 @@ export default class Message extends CustomElement {
 
 
     static get properties () {
     static get properties () {
         return {
         return {
-            chatview: { type: Object},
             correcting: { type: Boolean },
             correcting: { type: Boolean },
             editable: { type: Boolean },
             editable: { type: Boolean },
             edited: { type: String },
             edited: { type: String },
@@ -259,9 +260,12 @@ export default class Message extends CustomElement {
         if (this.model.get('sender') === 'me') {
         if (this.model.get('sender') === 'me') {
             _converse.xmppstatusview.showProfileModal(ev);
             _converse.xmppstatusview.showProfileModal(ev);
         } else if (this.message_type === 'groupchat') {
         } else if (this.message_type === 'groupchat') {
-            this.chatview.showOccupantDetailsModal(ev, this.model);
+            ev.preventDefault();
+            api.modal.show(OccupantModal, { 'model': this.model.occupant }, ev);
         } else {
         } else {
-            this.chatview.showUserDetailsModal(ev, this.model);
+            ev.preventDefault();
+            const chatbox = this.model.collection.chatbox;
+            api.modal.show(UserDetailsModal, { model: chatbox }, ev);
         }
         }
     }
     }
 
 

+ 13 - 0
src/headless/plugins/chat/model.js

@@ -103,6 +103,19 @@ const ChatBox = ModelWithContact.extend({
         this.notifications = new Model();
         this.notifications = new Model();
     },
     },
 
 
+    getNotificationsText () {
+        const { __ } = _converse;
+        if (this.notifications?.get('chat_state') === _converse.COMPOSING) {
+            return __('%1$s is typing', this.getDisplayName());
+        } else if (this.notifications?.get('chat_state') === _converse.PAUSED) {
+            return __('%1$s has stopped typing', this.getDisplayName());
+        } else if (this.notifications?.get('chat_state') === _converse.GONE) {
+            return __('%1$s has gone away', this.getDisplayName());
+        } else {
+            return '';
+        }
+    },
+
     afterMessagesFetched (messages) {
     afterMessagesFetched (messages) {
         this.most_recent_cached_message = messages ? this.getMostRecentMessage(messages) : null;
         this.most_recent_cached_message = messages ? this.getMostRecentMessage(messages) : null;
         /**
         /**

+ 75 - 0
src/headless/plugins/muc/muc.js

@@ -1855,6 +1855,81 @@ const ChatRoomMixin = {
         return false;
         return false;
     },
     },
 
 
+    getNotificationsText () {
+        const { __ } = _converse;
+        const actors_per_state = this.notifications.toJSON();
+
+        const role_changes = api.settings
+            .get('muc_show_info_messages')
+            .filter(role_change => converse.MUC_ROLE_CHANGES_LIST.includes(role_change));
+
+        const join_leave_events = api.settings
+            .get('muc_show_info_messages')
+            .filter(join_leave_event => converse.MUC_TRAFFIC_STATES_LIST.includes(join_leave_event));
+
+        const states = [...converse.CHAT_STATES, ...join_leave_events, ...role_changes];
+
+        return states.reduce((result, state) => {
+            const existing_actors = actors_per_state[state];
+            if (!existing_actors?.length) {
+                return result;
+            }
+            const actors = existing_actors.map(a => this.getOccupant(a)?.getDisplayName() || a);
+            if (actors.length === 1) {
+                if (state === 'composing') {
+                    return `${result}${__('%1$s is typing', actors[0])}\n`;
+                } else if (state === 'paused') {
+                    return `${result}${__('%1$s has stopped typing', actors[0])}\n`;
+                } else if (state === _converse.GONE) {
+                    return `${result}${__('%1$s has gone away', actors[0])}\n`;
+                } else if (state === 'entered') {
+                    return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`;
+                } else if (state === 'exited') {
+                    return `${result}${__('%1$s has left the groupchat', actors[0])}\n`;
+                } else if (state === 'op') {
+                    return `${result}${__('%1$s is now a moderator', actors[0])}\n`;
+                } else if (state === 'deop') {
+                    return `${result}${__('%1$s is no longer a moderator', actors[0])}\n`;
+                } else if (state === 'voice') {
+                    return `${result}${__('%1$s has been given a voice', actors[0])}\n`;
+                } else if (state === 'mute') {
+                    return `${result}${__('%1$s has been muted', actors[0])}\n`;
+                }
+            } else if (actors.length > 1) {
+                let actors_str;
+                if (actors.length > 3) {
+                    actors_str = `${Array.from(actors)
+                        .slice(0, 2)
+                        .join(', ')} and others`;
+                } else {
+                    const last_actor = actors.pop();
+                    actors_str = __('%1$s and %2$s', actors.join(', '), last_actor);
+                }
+
+                if (state === 'composing') {
+                    return `${result}${__('%1$s are typing', actors_str)}\n`;
+                } else if (state === 'paused') {
+                    return `${result}${__('%1$s have stopped typing', actors_str)}\n`;
+                } else if (state === _converse.GONE) {
+                    return `${result}${__('%1$s have gone away', actors_str)}\n`;
+                } else if (state === 'entered') {
+                    return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`;
+                } else if (state === 'exited') {
+                    return `${result}${__('%1$s have left the groupchat', actors_str)}\n`;
+                } else if (state === 'op') {
+                    return `${result}${__('%1$s are now moderators', actors[0])}\n`;
+                } else if (state === 'deop') {
+                    return `${result}${__('%1$s are no longer moderators', actors[0])}\n`;
+                } else if (state === 'voice') {
+                    return `${result}${__('%1$s have been given voices', actors[0])}\n`;
+                } else if (state === 'mute') {
+                    return `${result}${__('%1$s have been muted', actors[0])}\n`;
+                }
+            }
+            return result;
+        }, '');
+    },
+
     /**
     /**
      * @param {String} actor - The nickname of the actor that caused the notification
      * @param {String} actor - The nickname of the actor that caused the notification
      * @param {String|Array<String>} states - The state or states representing the type of notificcation
      * @param {String|Array<String>} states - The state or states representing the type of notificcation

+ 2 - 37
src/plugins/chatview/view.js

@@ -1,6 +1,5 @@
 import BaseChatView from 'shared/chatview.js';
 import BaseChatView from 'shared/chatview.js';
 import UserDetailsModal from 'modals/user-details.js';
 import UserDetailsModal from 'modals/user-details.js';
-import log from '@converse/headless/log';
 import tpl_chatbox from 'templates/chatbox.js';
 import tpl_chatbox from 'templates/chatbox.js';
 import tpl_chatbox_head from 'templates/chatbox_head.js';
 import tpl_chatbox_head from 'templates/chatbox_head.js';
 import { __ } from 'i18n';
 import { __ } from 'i18n';
@@ -45,6 +44,7 @@ export default class ChatView extends BaseChatView {
         this.listenTo(this.model, 'change:hidden', () => !this.model.get('hidden') && this.afterShown());
         this.listenTo(this.model, 'change:hidden', () => !this.model.get('hidden') && this.afterShown());
         this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
         this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
         this.listenTo(this.model, 'vcard:change', this.renderHeading);
         this.listenTo(this.model, 'vcard:change', this.renderHeading);
+        this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
 
 
         if (this.model.contact) {
         if (this.model.contact) {
             this.listenTo(this.model.contact, 'destroy', this.renderHeading);
             this.listenTo(this.model.contact, 'destroy', this.renderHeading);
@@ -77,10 +77,7 @@ export default class ChatView extends BaseChatView {
 
 
     render () {
     render () {
         const result = tpl_chatbox(Object.assign(
         const result = tpl_chatbox(Object.assign(
-            this.model.toJSON(), {
-                'markScrolled': ev => this.markScrolled(ev),
-                'chatview': this
-            })
+            this.model.toJSON(), { 'markScrolled': ev => this.markScrolled(ev) })
         );
         );
         render(result, this);
         render(result, this);
         this.content = this.querySelector('.chat-content');
         this.content = this.querySelector('.chat-content');
@@ -90,18 +87,6 @@ export default class ChatView extends BaseChatView {
         return this;
         return this;
     }
     }
 
 
-    getNotifications () {
-        if (this.model.notifications.get('chat_state') === _converse.COMPOSING) {
-            return __('%1$s is typing', this.model.getDisplayName());
-        } else if (this.model.notifications.get('chat_state') === _converse.PAUSED) {
-            return __('%1$s has stopped typing', this.model.getDisplayName());
-        } else if (this.model.notifications.get('chat_state') === _converse.GONE) {
-            return __('%1$s has gone away', this.model.getDisplayName());
-        } else {
-            return '';
-        }
-    }
-
     getHelpMessages () { // eslint-disable-line class-methods-use-this
     getHelpMessages () { // eslint-disable-line class-methods-use-this
         return [
         return [
             `<strong>/clear</strong>: ${__('Remove messages')}`,
             `<strong>/clear</strong>: ${__('Remove messages')}`,
@@ -385,26 +370,6 @@ export default class ChatView extends BaseChatView {
         this.insertIntoTextArea('', true, false);
         this.insertIntoTextArea('', true, false);
     }
     }
 
 
-    async onMessageRetractButtonClicked (message) {
-        if (message.get('sender') !== 'me') {
-            return log.error("onMessageRetractButtonClicked called for someone else's message!");
-        }
-        const retraction_warning = __(
-            'Be aware that other XMPP/Jabber clients (and servers) may ' +
-                'not yet support retractions and that this message may not ' +
-                'be removed everywhere.'
-        );
-
-        const messages = [__('Are you sure you want to retract this message?')];
-        if (api.settings.get('show_retraction_warning')) {
-            messages[1] = retraction_warning;
-        }
-        const result = await api.confirm(__('Confirm'), messages);
-        if (result) {
-            this.model.retractOwnMessage(message);
-        }
-    }
-
     inputChanged (ev) { // eslint-disable-line class-methods-use-this
     inputChanged (ev) { // eslint-disable-line class-methods-use-this
         const height = ev.target.scrollHeight + 'px';
         const height = ev.target.scrollHeight + 'px';
         if (ev.target.style.height != height) {
         if (ev.target.style.height != height) {

+ 0 - 1
src/plugins/headlines-view/view.js

@@ -51,7 +51,6 @@ class HeadlinesView extends BaseChatView {
         this.setAttribute('id', this.model.get('box_id'));
         this.setAttribute('id', this.model.get('box_id'));
         const result = tpl_chatbox(
         const result = tpl_chatbox(
             Object.assign(this.model.toJSON(), {
             Object.assign(this.model.toJSON(), {
-                chatview: this,
                 show_send_button: false,
                 show_send_button: false,
                 show_toolbar: false,
                 show_toolbar: false,
             })
             })

+ 1 - 139
src/plugins/muc-views/muc.js

@@ -4,7 +4,6 @@ import 'shared/autocomplete/index.js';
 import BaseChatView from 'shared/chatview.js';
 import BaseChatView from 'shared/chatview.js';
 import MUCInviteModal from 'modals/muc-invite.js';
 import MUCInviteModal from 'modals/muc-invite.js';
 import ModeratorToolsModal from 'modals/moderator-tools.js';
 import ModeratorToolsModal from 'modals/moderator-tools.js';
-import OccupantModal from 'modals/occupant.js';
 import RoomDetailsModal from 'modals/muc-details.js';
 import RoomDetailsModal from 'modals/muc-details.js';
 import log from '@converse/headless/log';
 import log from '@converse/headless/log';
 import tpl_chatroom from 'templates/chatroom.js';
 import tpl_chatroom from 'templates/chatroom.js';
@@ -97,6 +96,7 @@ export default class MUCView extends BaseChatView {
         this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
         this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
         this.listenTo(this.model.features, 'change:open', this.renderHeading);
         this.listenTo(this.model.features, 'change:open', this.renderHeading);
         this.listenTo(this.model.messages, 'rendered', this.maybeScrollDown);
         this.listenTo(this.model.messages, 'rendered', this.maybeScrollDown);
+        this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
         this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
         this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
 
 
         // Bind so that we can pass it to addEventListener and removeEventListener
         // Bind so that we can pass it to addEventListener and removeEventListener
@@ -137,7 +137,6 @@ export default class MUCView extends BaseChatView {
         render(
         render(
             tpl_chatroom({
             tpl_chatroom({
                 sidebar_hidden,
                 sidebar_hidden,
-                'chatview': this,
                 'model': this.model,
                 'model': this.model,
                 'occupants': this.model.occupants,
                 'occupants': this.model.occupants,
                 'show_sidebar':
                 'show_sidebar':
@@ -168,80 +167,6 @@ export default class MUCView extends BaseChatView {
         !this.model.get('hidden') && this.show();
         !this.model.get('hidden') && this.show();
     }
     }
 
 
-    getNotifications () {
-        const actors_per_state = this.model.notifications.toJSON();
-
-        const role_changes = api.settings
-            .get('muc_show_info_messages')
-            .filter(role_change => converse.MUC_ROLE_CHANGES_LIST.includes(role_change));
-
-        const join_leave_events = api.settings
-            .get('muc_show_info_messages')
-            .filter(join_leave_event => converse.MUC_TRAFFIC_STATES_LIST.includes(join_leave_event));
-
-        const states = [...converse.CHAT_STATES, ...join_leave_events, ...role_changes];
-
-        return states.reduce((result, state) => {
-            const existing_actors = actors_per_state[state];
-            if (!existing_actors?.length) {
-                return result;
-            }
-            const actors = existing_actors.map(a => this.model.getOccupant(a)?.getDisplayName() || a);
-            if (actors.length === 1) {
-                if (state === 'composing') {
-                    return `${result}${__('%1$s is typing', actors[0])}\n`;
-                } else if (state === 'paused') {
-                    return `${result}${__('%1$s has stopped typing', actors[0])}\n`;
-                } else if (state === _converse.GONE) {
-                    return `${result}${__('%1$s has gone away', actors[0])}\n`;
-                } else if (state === 'entered') {
-                    return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`;
-                } else if (state === 'exited') {
-                    return `${result}${__('%1$s has left the groupchat', actors[0])}\n`;
-                } else if (state === 'op') {
-                    return `${result}${__('%1$s is now a moderator', actors[0])}\n`;
-                } else if (state === 'deop') {
-                    return `${result}${__('%1$s is no longer a moderator', actors[0])}\n`;
-                } else if (state === 'voice') {
-                    return `${result}${__('%1$s has been given a voice', actors[0])}\n`;
-                } else if (state === 'mute') {
-                    return `${result}${__('%1$s has been muted', actors[0])}\n`;
-                }
-            } else if (actors.length > 1) {
-                let actors_str;
-                if (actors.length > 3) {
-                    actors_str = `${Array.from(actors)
-                        .slice(0, 2)
-                        .join(', ')} and others`;
-                } else {
-                    const last_actor = actors.pop();
-                    actors_str = __('%1$s and %2$s', actors.join(', '), last_actor);
-                }
-
-                if (state === 'composing') {
-                    return `${result}${__('%1$s are typing', actors_str)}\n`;
-                } else if (state === 'paused') {
-                    return `${result}${__('%1$s have stopped typing', actors_str)}\n`;
-                } else if (state === _converse.GONE) {
-                    return `${result}${__('%1$s have gone away', actors_str)}\n`;
-                } else if (state === 'entered') {
-                    return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`;
-                } else if (state === 'exited') {
-                    return `${result}${__('%1$s have left the groupchat', actors_str)}\n`;
-                } else if (state === 'op') {
-                    return `${result}${__('%1$s are now moderators', actors[0])}\n`;
-                } else if (state === 'deop') {
-                    return `${result}${__('%1$s are no longer moderators', actors[0])}\n`;
-                } else if (state === 'voice') {
-                    return `${result}${__('%1$s have been given voices', actors[0])}\n`;
-                } else if (state === 'mute') {
-                    return `${result}${__('%1$s have been muted', actors[0])}\n`;
-                }
-            }
-            return result;
-        }, '');
-    }
-
     getHelpMessages () {
     getHelpMessages () {
         const setting = api.settings.get('muc_disable_slash_commands');
         const setting = api.settings.get('muc_disable_slash_commands');
         const disabled_commands = Array.isArray(setting) ? setting : [];
         const disabled_commands = Array.isArray(setting) ? setting : [];
@@ -445,64 +370,6 @@ export default class MUCView extends BaseChatView {
         return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
         return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
     }
     }
 
 
-    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 ' +
-                'be removed everywhere.'
-        );
-
-        if (message.mayBeRetracted()) {
-            const messages = [__('Are you sure you want to retract this message?')];
-            if (api.settings.get('show_retraction_warning')) {
-                messages[1] = retraction_warning;
-            }
-            !!(await api.confirm(__('Confirm'), messages)) && this.model.retractOwnMessage(message);
-        } else if (await message.mayBeModerated()) {
-            if (message.get('sender') === 'me') {
-                let messages = [__('Are you sure you want to retract this message?')];
-                if (api.settings.get('show_retraction_warning')) {
-                    messages = [messages[0], retraction_warning, messages[1]];
-                }
-                !!(await api.confirm(__('Confirm'), messages)) && this.retractOtherMessage(message);
-            } else {
-                let messages = [
-                    __('You are about to retract this message.'),
-                    __('You may optionally include a message, explaining the reason for the retraction.')
-                ];
-                if (api.settings.get('show_retraction_warning')) {
-                    messages = [messages[0], retraction_warning, messages[1]];
-                }
-                const reason = await api.prompt(__('Message Retraction'), messages, __('Optional reason'));
-                reason !== false && this.retractOtherMessage(message, reason);
-            }
-        } else {
-            const err_msg = __(`Sorry, you're not allowed to retract this message`);
-            api.alert('error', __('Error'), err_msg);
-        }
-    }
-
-    /**
-     * Retract someone else's message in this groupchat.
-     * @private
-     * @method _converse.ChatRoomView#retractOtherMessage
-     * @param { _converse.Message } message - The message which we're retracting.
-     * @param { string } [reason] - The reason for retracting the message.
-     */
-    async retractOtherMessage (message, reason) {
-        const result = await this.model.retractOtherMessage(message, reason);
-        if (result === null) {
-            const err_msg = __(`A timeout occurred while trying to retract the message`);
-            api.alert('error', __('Error'), err_msg);
-            log(err_msg, Strophe.LogLevel.WARN);
-        } else if (u.isErrorStanza(result)) {
-            const err_msg = __(`Sorry, you're not allowed to retract this message.`);
-            api.alert('error', __('Error'), err_msg);
-            log(err_msg, Strophe.LogLevel.WARN);
-            log(result, Strophe.LogLevel.WARN);
-        }
-    }
-
     showModeratorToolsModal (affiliation) {
     showModeratorToolsModal (affiliation) {
         if (!this.verifyRoles(['moderator'])) {
         if (!this.verifyRoles(['moderator'])) {
             return;
             return;
@@ -522,11 +389,6 @@ export default class MUCView extends BaseChatView {
         api.modal.show(RoomDetailsModal, { 'model': this.model }, ev);
         api.modal.show(RoomDetailsModal, { 'model': this.model }, ev);
     }
     }
 
 
-    showOccupantDetailsModal (ev, message) { // eslint-disable-line class-methods-use-this
-        ev.preventDefault();
-        api.modal.show(OccupantModal, { 'model': message.occupant }, ev);
-    }
-
     showChatStateNotification (message) {
     showChatStateNotification (message) {
         if (message.get('sender') === 'me') {
         if (message.get('sender') === 'me') {
             return;
             return;

+ 9 - 21
src/shared/chatview.js

@@ -234,7 +234,6 @@ export default class BaseChatView extends ElementView {
                 .reverse()
                 .reverse()
                 .find(m => m.get('editable'));
                 .find(m => m.get('editable'));
         if (message) {
         if (message) {
-            this.insertIntoTextArea(u.prefixMentions(message), true, true);
             message.save('correcting', true);
             message.save('correcting', true);
         }
         }
     }
     }
@@ -309,21 +308,16 @@ export default class BaseChatView extends ElementView {
         this.insertIntoTextArea(emoji, autocompleting, false, ac_position);
         this.insertIntoTextArea(emoji, autocompleting, false, ac_position);
     }
     }
 
 
-    onMessageEditButtonClicked (message) {
-        const currently_correcting = this.model.messages.findWhere('correcting');
-        const unsent_text = this.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) {
-            currently_correcting?.save('correcting', false);
-            message.save('correcting', true);
+    onMessageCorrecting (message) {
+        if (message.get('correcting')) {
             this.insertIntoTextArea(u.prefixMentions(message), true, true);
             this.insertIntoTextArea(u.prefixMentions(message), true, true);
         } else {
         } else {
-            message.save('correcting', false);
-            this.insertIntoTextArea('', true, false);
+            const currently_correcting = this.model.messages.findWhere('correcting');
+            if (currently_correcting && currently_correcting !== message) {
+                this.insertIntoTextArea(u.prefixMentions(message), true, true);
+            } else {
+                this.insertIntoTextArea('', true, false);
+            }
         }
         }
     }
     }
 
 
@@ -415,13 +409,7 @@ export default class BaseChatView extends ElementView {
                 'scrollTop': null
                 'scrollTop': null
             });
             });
         }
         }
-        const msgs_container = this.querySelector('.chat-content__messages');
-        if (msgs_container.scrollTo) {
-            const behavior = msgs_container.scrollTop ? 'smooth' : 'auto';
-            msgs_container.scrollTo({ 'top': msgs_container.scrollHeight, behavior });
-        } else {
-            msgs_container.scrollTop = msgs_container.scrollHeight;
-        }
+        this.querySelector('.chat-content__messages').scrollDown();
         this.onScrolledDown();
         this.onScrolledDown();
     }
     }
 
 

+ 0 - 1
src/templates/chat_message.js

@@ -33,7 +33,6 @@ export default (o) => {
                         ${ o.is_retracted ? o.renderRetraction() : o.renderMessageText() }
                         ${ o.is_retracted ? o.renderRetraction() : o.renderMessageText() }
                     </div>
                     </div>
                     <converse-message-actions
                     <converse-message-actions
-                        .chatview=${o.chatview}
                         .model=${o.model}
                         .model=${o.model}
                         ?correcting="${o.correcting}"
                         ?correcting="${o.correcting}"
                         ?editable="${o.editable}"
                         ?editable="${o.editable}"

+ 1 - 1
src/templates/chatbox.js

@@ -8,7 +8,7 @@ export default (o) => html`
             <div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
             <div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
                 <converse-chat-content
                 <converse-chat-content
                     class="chat-content__messages"
                     class="chat-content__messages"
-                    .chatview=${o.chatview}
+                    jid=${o.jid}
                     @scroll=${o.markScrolled}></converse-chat-content>
                     @scroll=${o.markScrolled}></converse-chat-content>
 
 
                 <div class="chat-content__help"></div>
                 <div class="chat-content__help"></div>

+ 1 - 1
src/templates/chatroom.js

@@ -9,7 +9,7 @@ export default (o) => html`
                 <div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
                 <div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
                     <converse-chat-content
                     <converse-chat-content
                         class="chat-content__messages"
                         class="chat-content__messages"
-                        .chatview=${o.chatview}
+                        jid=${o.model.get('jid')}
                         @scroll=${o.markScrolled}></converse-chat-content>
                         @scroll=${o.markScrolled}></converse-chat-content>
 
 
                     <div class="chat-content__help"></div>
                     <div class="chat-content__help"></div>

+ 5 - 16
src/templates/directives/body.js

@@ -1,30 +1,19 @@
 import { MessageText } from '../../shared/message/text.js';
 import { MessageText } from '../../shared/message/text.js';
-import { api, converse } from  "@converse/headless/core";
+import { api } from  "@converse/headless/core";
 import { directive, html } from "lit-html";
 import { directive, html } from "lit-html";
 import { until } from 'lit-html/directives/until.js';
 import { until } from 'lit-html/directives/until.js';
 
 
 
 
-const u = converse.env.utils;
-
-
 class MessageBodyRenderer {
 class MessageBodyRenderer {
 
 
     constructor (component) {
     constructor (component) {
         this.model = component.model;
         this.model = component.model;
         this.component = component;
         this.component = component;
-        this.chatview = u.ancestor(this.component, 'converse-chat-message')?.chatview;
-        // We jot down whether we were scrolled down before rendering, because when an
-        // image loads, it triggers 'scroll' and the chat will be marked as scrolled,
-        // which is technically true, but not what we want because the user
-        // didn't initiate the scrolling.
-        this.was_scrolled_up = this.chatview.model.get('scrolled');
-        this.text = this.component.model.getMessageText();
+        this.text = this.model.getMessageText();
     }
     }
 
 
-    scrollDownOnImageLoad () {
-        if (!this.was_scrolled_up) {
-            this.chatview.scrollDown();
-        }
+    onImageLoaded () {
+        this.component.dispatchEvent(new CustomEvent('imageLoaded', { detail: this.component }));
     }
     }
 
 
     async transform () {
     async transform () {
@@ -35,7 +24,7 @@ class MessageBodyRenderer {
             this.model,
             this.model,
             offset,
             offset,
             show_images,
             show_images,
-            () => this.scrollDownOnImageLoad(),
+            () => this.onImageLoaded(),
             ev => this.component.showImageModal(ev)
             ev => this.component.showImageModal(ev)
         );
         );
         await text.addTemplates();
         await text.addTemplates();