瀏覽代碼

Inform user if message couldn't be delivered because they're blocked

JC Brand 6 月之前
父節點
當前提交
0ca5804f70

+ 15 - 0
src/headless/plugins/blocklist/plugin.js

@@ -25,6 +25,21 @@ converse.plugins.add('converse-blocklist', {
 
         api.promises.add(['blocklistInitialized']);
 
+        api.listen.on(
+            'getErrorAttributesForMessage',
+            /**
+             * @param {import('plugins/chat/types').MessageAttributes} attrs
+             * @param {import('plugins/chat/types').MessageErrorAttributes} new_attrs
+             */
+            (attrs, new_attrs) => {
+                if (attrs.errors.find((e) => e.name === 'blocked' && e.xmlns === `${Strophe.NS.BLOCKING}:errors`)) {
+                    const { __ } = _converse;
+                    new_attrs.error = __('You are blocked from sending messages.');
+                }
+                return new_attrs;
+            }
+        );
+
         api.listen.on('connected', () => {
             const connection = api.connection.get();
             connection.addHandler(

+ 50 - 10
src/headless/plugins/blocklist/tests/blocklist.js

@@ -1,7 +1,7 @@
 /*global mock, converse */
 const { u, stx } = converse.env;
 
-fdescribe('A blocklist', function () {
+describe('A blocklist', function () {
     beforeEach(() => {
         jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza });
         window.sessionStorage.removeItem('converse.blocklist-romeo@montague.lit-fetched');
@@ -125,15 +125,18 @@ fdescribe('A blocklist', function () {
             const IQ_stanzas = api.connection.get().IQ_stanzas;
             let sent_stanza = await u.waitUntil(() => IQ_stanzas.find((s) => s.querySelector('iq blocklist')));
 
-            _converse.api.connection.get()._dataRecv(mock.createRequest(
-                stx`<iq xmlns="jabber:client"
+            _converse.api.connection.get()._dataRecv(
+                mock.createRequest(
+                    stx`<iq xmlns="jabber:client"
                         to="${api.connection.get().jid}"
                         type="result"
                         id="${sent_stanza.getAttribute('id')}">
                     <blocklist xmlns='urn:xmpp:blocking'>
                         <item jid='iago@shakespeare.lit'/>
                     </blocklist>
-                </iq>`));
+                </iq>`
+                )
+            );
 
             const blocklist = await api.waitUntil('blocklistInitialized');
             expect(blocklist.length).toBe(1);
@@ -148,9 +151,13 @@ fdescribe('A blocklist', function () {
                     </block>
                 </iq>`);
 
-            _converse.api.connection.get()._dataRecv(mock.createRequest(
-                stx`<iq xmlns="jabber:client" type="result" id="${sent_stanza.getAttribute('id')}"/>`)
-            );
+            _converse.api.connection
+                .get()
+                ._dataRecv(
+                    mock.createRequest(
+                        stx`<iq xmlns="jabber:client" type="result" id="${sent_stanza.getAttribute('id')}"/>`
+                    )
+                );
 
             await u.waitUntil(() => blocklist.length === 2);
             expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['iago@shakespeare.lit', 'juliet@capulet.lit']);
@@ -165,12 +172,45 @@ fdescribe('A blocklist', function () {
                     </unblock>
                 </iq>`);
 
-            _converse.api.connection.get()._dataRecv(mock.createRequest(
-                stx`<iq xmlns="jabber:client" type="result" id="${sent_stanza.getAttribute('id')}"/>`)
-            );
+            _converse.api.connection
+                .get()
+                ._dataRecv(
+                    mock.createRequest(
+                        stx`<iq xmlns="jabber:client" type="result" id="${sent_stanza.getAttribute('id')}"/>`
+                    )
+                );
 
             await u.waitUntil(() => blocklist.length === 1);
             expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['iago@shakespeare.lit']);
         })
     );
 });
+
+describe('A Chat Message', function () {
+    it(
+        "will show an error message if it's rejected due to being banned",
+        mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
+            const { api } = _converse;
+            await mock.waitForRoster(_converse, 'current', 1);
+            const sender_jid = mock.cur_names[0].replace(/ /g, '.').toLowerCase() + '@montague.lit';
+            const chat = await api.chats.open(sender_jid);
+            const msg_text = 'This message will not be sent, due to an error';
+            const message = await chat.sendMessage({ body: msg_text });
+
+            api.connection.get()._dataRecv(mock.createRequest(stx`
+                <message xmlns="jabber:client"
+                    to="${api.connection.get().jid}"
+                    type="error"
+                    id="${message.get('msgid')}"
+                    from="${sender_jid}">
+                    <error type="cancel">
+                        <not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+                        <blocked xmlns='urn:xmpp:blocking:errors'/>
+                    </error>
+                </message>`));
+
+            await u.waitUntil(() => message.get('is_error') === true);
+            expect(message.get('error')).toBe('You are blocked from sending messages.');
+        })
+    );
+});

+ 10 - 6
src/headless/plugins/chat/types.ts

@@ -1,15 +1,20 @@
 import {EncryptionAttrs} from "../../shared/types";
 
-export type MessageAttributes = EncryptionAttrs & {
+export type MessageErrorAttributes = {
+    is_error: boolean; // Whether an error was received for this message
+    error: string; // The error name
+    errors: { name: string; xmlns: string }[];
+    error_condition: string; // The defined error condition
+    error_text: string; // The error text received from the server
+    error_type: string; // The type of error received from the server
+}
+
+export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & {
     body: string; // The contents of the <body> tag of the message stanza
     chat_state: string; // The XEP-0085 chat state notification contained in this message
     contact_jid: string; // The JID of the other person or entity
     editable: boolean; // Is this message editable via XEP-0308?
     edited: string; // An ISO8601 string recording the time that the message was edited per XEP-0308
-    error: string; // The error name
-    error_condition: string; // The defined error condition
-    error_text: string; // The error text received from the server
-    error_type: string; // The type of error received from the server
     from: string; // The sender JID
     message?: string; // Used with info and error messages
     fullname: string; // The full name of the sender
@@ -17,7 +22,6 @@ export type MessageAttributes = EncryptionAttrs & {
     is_carbon: boolean; // Is this message a XEP-0280 Carbon?
     is_delayed: boolean; // Was delivery of this message was delayed as per XEP-0203?
     is_encrypted: boolean; //  Is this message XEP-0384  encrypted?
-    is_error: boolean; // Whether an error was received for this message
     is_headline: boolean; // Is this a "headline" message?
     is_markable: boolean; // Can this message be marked with a XEP-0333 chat marker?
     is_marker: boolean; // Is this message a XEP-0333 Chat Marker?

+ 2 - 2
src/headless/plugins/emoji/plugin.js

@@ -12,8 +12,8 @@ import emojis from './api.js';
 import { isOnlyEmojis } from './utils.js';
 
 converse.emojis = {
-    'initialized': false,
-    'initialized_promise': getOpenPromise(),
+    initialized: false,
+    initialized_promise: getOpenPromise(),
 };
 
 converse.plugins.add('converse-emoji', {

+ 42 - 26
src/headless/shared/model-with-messages.js

@@ -13,7 +13,7 @@ import { isNewMessage } from '../plugins/chat/utils.js';
 import _converse from './_converse.js';
 import { MethodNotImplementedError } from './errors.js';
 import { sendMarker, sendReceiptStanza, sendRetractionMessage } from './actions.js';
-import {parseMessage} from '../plugins/chat/parsers';
+import { parseMessage } from '../plugins/chat/parsers';
 
 const { Strophe, $msg, u } = converse.env;
 
@@ -274,6 +274,8 @@ export default function ModelWithMessages(BaseModel) {
          *  chat.sendMessage({'body': 'hello world'});
          */
         async sendMessage(attrs) {
+            await converse.emojis?.initialized_promise;
+
             if (!this.canPostMessages()) {
                 log.warn('sendMessage was called but canPostMessages is false');
                 return;
@@ -748,11 +750,48 @@ export default function ModelWithMessages(BaseModel) {
             }
         }
 
+        /**
+         * @param {Message} message
+         * @param {MessageAttributes} attrs
+         */
+        async getErrorAttributesForMessage(message, attrs) {
+            const { __ } = _converse;
+            const new_attrs = {
+                editable: false,
+                error: attrs.error,
+                error_condition: attrs.error_condition,
+                error_text: attrs.error_text,
+                error_type: attrs.error_type,
+                is_error: true,
+            };
+            if (attrs.msgid === message.get('retraction_id')) {
+                // The error message refers to a retraction
+                new_attrs.retraction_id = undefined;
+                if (!attrs.error) {
+                    if (attrs.error_condition === 'forbidden') {
+                        new_attrs.error = __("You're not allowed to retract your message.");
+                    } else {
+                        new_attrs.error = __('Sorry, an error occurred while trying to retract your message.');
+                    }
+                }
+            } else if (!attrs.error) {
+                if (attrs.error_condition === 'forbidden') {
+                    new_attrs.error = __("You're not allowed to send a message.");
+                } else {
+                    new_attrs.error = __('Sorry, an error occurred while trying to send your message.');
+                }
+            }
+            /**
+             * *Hook* which allows plugins to add application-specific attributes
+             * @event _converse#getErrorAttributesForMessage
+             */
+            return await api.hook('getErrorAttributesForMessage', attrs, new_attrs);
+        }
+
         /**
          * @param {Element} stanza
          */
         async handleErrorMessageStanza(stanza) {
-            const { __ } = _converse;
             const attrs_or_error = await parseMessage(stanza);
             if (u.isErrorObject(attrs_or_error)) {
                 const { stanza, message } = /** @type {StanzaParseError} */ (attrs_or_error);
@@ -767,30 +806,7 @@ export default function ModelWithMessages(BaseModel) {
 
             const message = this.getMessageReferencedByError(attrs);
             if (message) {
-                const new_attrs = {
-                    'error': attrs.error,
-                    'error_condition': attrs.error_condition,
-                    'error_text': attrs.error_text,
-                    'error_type': attrs.error_type,
-                    'editable': false,
-                };
-                if (attrs.msgid === message.get('retraction_id')) {
-                    // The error message refers to a retraction
-                    new_attrs.retraction_id = undefined;
-                    if (!attrs.error) {
-                        if (attrs.error_condition === 'forbidden') {
-                            new_attrs.error = __("You're not allowed to retract your message.");
-                        } else {
-                            new_attrs.error = __('Sorry, an error occurred while trying to retract your message.');
-                        }
-                    }
-                } else if (!attrs.error) {
-                    if (attrs.error_condition === 'forbidden') {
-                        new_attrs.error = __("You're not allowed to send a message.");
-                    } else {
-                        new_attrs.error = __('Sorry, an error occurred while trying to send your message.');
-                    }
-                }
+                const new_attrs = await this.getErrorAttributesForMessage(message, attrs);
                 message.save(new_attrs);
             } else {
                 this.createMessage(attrs);

+ 5 - 4
src/headless/shared/parsers.js

@@ -259,10 +259,11 @@ export function getErrorAttributes (stanza) {
         const error = stanza.querySelector('error');
         const text = sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop();
         return {
-            'is_error': true,
-            'error_text': text?.textContent,
-            'error_type': error.getAttribute('type'),
-            'error_condition': error.firstElementChild.nodeName
+            is_error: true,
+            error_text: text?.textContent,
+            error_type: error.getAttribute('type'),
+            error_condition: error.firstElementChild.nodeName,
+            errors: Array.from(error.children).map((e) => ({ name: e.nodeName, xmlns: e.getAttribute('xmlns') })),
         };
     }
     return {};

+ 1 - 0
src/headless/types/plugins/chat/model.d.ts

@@ -54,6 +54,7 @@ declare const ChatBox_base: {
         };
         sendMarkerForMessage(msg: import("./message.js").default, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
         handleUnreadMessage(message: import("./message.js").default): void;
+        getErrorAttributesForMessage(message: import("./message.js").default, attrs: import("./types").MessageAttributes): Promise<any>;
         handleErrorMessageStanza(stanza: Element): Promise<void>;
         incrementUnreadMsgsCounter(message: import("./message.js").default): void;
         clearUnreadMsgCounter(): void;

+ 12 - 6
src/headless/types/plugins/chat/types.d.ts

@@ -1,14 +1,21 @@
 import { EncryptionAttrs } from "../../shared/types";
-export type MessageAttributes = EncryptionAttrs & {
+export type MessageErrorAttributes = {
+    is_error: boolean;
+    error: string;
+    errors: {
+        name: string;
+        xmlns: string;
+    }[];
+    error_condition: string;
+    error_text: string;
+    error_type: string;
+};
+export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & {
     body: string;
     chat_state: string;
     contact_jid: string;
     editable: boolean;
     edited: string;
-    error: string;
-    error_condition: string;
-    error_text: string;
-    error_type: string;
     from: string;
     message?: string;
     fullname: string;
@@ -16,7 +23,6 @@ export type MessageAttributes = EncryptionAttrs & {
     is_carbon: boolean;
     is_delayed: boolean;
     is_encrypted: boolean;
-    is_error: boolean;
     is_headline: boolean;
     is_markable: boolean;
     is_marker: boolean;

+ 1 - 0
src/headless/types/plugins/muc/muc.d.ts

@@ -54,6 +54,7 @@ declare const MUC_base: {
         };
         sendMarkerForMessage(msg: import("../chat/message.js").default, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
         handleUnreadMessage(message: import("../chat/message.js").default): void;
+        getErrorAttributesForMessage(message: import("../chat/message.js").default, attrs: import("../chat/types").MessageAttributes): Promise<any>;
         handleErrorMessageStanza(stanza: Element): Promise<void>;
         incrementUnreadMsgsCounter(message: import("../chat/message.js").default): void;
         clearUnreadMsgCounter(): void;

+ 1 - 0
src/headless/types/plugins/muc/occupant.d.ts

@@ -54,6 +54,7 @@ declare const MUCOccupant_base: {
         };
         sendMarkerForMessage(msg: import("../chat").Message, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
         handleUnreadMessage(message: import("../chat").Message): void;
+        getErrorAttributesForMessage(message: import("../chat").Message, attrs: import("../chat/types").MessageAttributes): Promise<any>;
         handleErrorMessageStanza(stanza: Element): Promise<void>;
         incrementUnreadMsgsCounter(message: import("../chat").Message): void;
         clearUnreadMsgCounter(): void;

+ 1 - 0
src/headless/types/shared/chatbox.d.ts

@@ -53,6 +53,7 @@ declare const ChatBoxBase_base: {
         };
         sendMarkerForMessage(msg: import("../index.js").Message, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
         handleUnreadMessage(message: import("../index.js").Message): void;
+        getErrorAttributesForMessage(message: import("../index.js").Message, attrs: import("../plugins/chat/types.js").MessageAttributes): Promise<any>;
         handleErrorMessageStanza(stanza: Element): Promise<void>;
         incrementUnreadMsgsCounter(message: import("../index.js").Message): void;
         clearUnreadMsgCounter(): void;

+ 5 - 0
src/headless/types/shared/model-with-messages.d.ts

@@ -187,6 +187,11 @@ export default function ModelWithMessages<T extends import("./types").ModelExten
          * @param {Message} message
          */
         handleUnreadMessage(message: import("../plugins/chat/message").default): void;
+        /**
+         * @param {Message} message
+         * @param {MessageAttributes} attrs
+         */
+        getErrorAttributesForMessage(message: import("../plugins/chat/message").default, attrs: import("../plugins/chat/types.ts").MessageAttributes): Promise<any>;
         /**
          * @param {Element} stanza
          */

+ 5 - 0
src/headless/types/shared/parsers.d.ts

@@ -68,11 +68,16 @@ export function getErrorAttributes(stanza: Element): {
     error_text: string;
     error_type: string;
     error_condition: string;
+    errors: {
+        name: string;
+        xmlns: string;
+    }[];
 } | {
     is_error?: undefined;
     error_text?: undefined;
     error_type?: undefined;
     error_condition?: undefined;
+    errors?: undefined;
 };
 /**
  * Given a message stanza, find and return any XEP-0372 references

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

@@ -1115,7 +1115,7 @@ describe("A Chat Message", function () {
                         .t(error_txt);
                 api.connection.get()._dataRecv(mock.createRequest(stanza));
 
-                let ui_error_txt = `Message delivery failed: "${error_txt}"`;
+                let ui_error_txt = `Message delivery failed.\n${error_txt}`;
                 await u.waitUntil(() => view.querySelector('.chat-msg__error')?.textContent.trim() === ui_error_txt);
 
                 const other_error_txt = 'Server-to-server connection failed: Connecting failed: connection timeout';
@@ -1131,7 +1131,7 @@ describe("A Chat Message", function () {
                         .t(other_error_txt);
                 api.connection.get()._dataRecv(mock.createRequest(stanza));
 
-                ui_error_txt = `Message delivery failed: "${other_error_txt}"`;
+                ui_error_txt = `Message delivery failed.\n${other_error_txt}`;
                 await u.waitUntil(() =>
                     view.querySelector('converse-chat-message:last-child .chat-msg__error')?.textContent.trim() === ui_error_txt);
 

+ 1 - 1
src/plugins/muc-views/tests/muc-messages.js

@@ -39,7 +39,7 @@ describe("A Groupchat Message", function () {
                     <body>hello world</body>
                 </message>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(error));
-            const ui_error_txt = `Message delivery failed: "${err_msg_text}"`;
+            const ui_error_txt = `Message delivery failed.\n${err_msg_text}`;
             expect(await u.waitUntil(() => view.querySelector('.chat-msg__error')?.textContent?.trim())).toBe(ui_error_txt);
             expect(view.model.messages.length).toBe(1);
             const message = view.model.messages.at(0);

+ 1 - 1
src/plugins/muc-views/tests/muc-private-messages.js

@@ -234,7 +234,7 @@ describe('MUC Private Messages', () => {
                     );
 
                     expect(await u.waitUntil(() => view.querySelector('.chat-msg__error')?.textContent?.trim())).toBe(
-                        `Message delivery failed: "${err_msg_text}"`
+                        `Message delivery failed.\n${err_msg_text}`
                     );
                 })
             );

+ 2 - 2
src/plugins/muc-views/tests/mute.js

@@ -29,7 +29,7 @@ describe("Groupchats", function () {
             await u.waitUntil(() => view.querySelector('.chat-msg__error')?.textContent.trim(), 1000);
 
             expect(view.querySelector('.chat-msg__error').textContent.trim()).toBe(
-                `Message delivery failed: "Your message was not delivered because you weren't allowed to send it."`);
+                `Message delivery failed.\nYour message was not delivered because you weren't allowed to send it.`);
 
             textarea.value = 'Hello again';
             message_form.onFormSubmitted(new Event('submit'));
@@ -50,7 +50,7 @@ describe("Groupchats", function () {
             await u.waitUntil(() => view.querySelectorAll('.chat-msg__error').length === 2);
             const sel = 'converse-message-history converse-chat-message:last-child .chat-msg__error';
             await u.waitUntil(() => view.querySelector(sel)?.textContent.trim());
-            expect(view.querySelector(sel).textContent.trim()).toBe(`Message delivery failed: "Thou shalt not!"`);
+            expect(view.querySelector(sel).textContent.trim()).toBe(`Message delivery failed.\nThou shalt not!`);
         }));
 
         it("will see an explanatory message instead of a textarea",

+ 2 - 2
src/plugins/muc-views/tests/retractions.js

@@ -712,7 +712,7 @@ describe("Message Retractions", function () {
             expect(view.model.messages.at(0).get('editable')).toBe(false);
 
             const errmsg = view.querySelector('.chat-msg__error');
-            expect(errmsg.textContent.trim()).toBe(`Message delivery failed: "You're not allowed to retract your message."`);
+            expect(errmsg.textContent.trim()).toBe(`Message delivery failed.\nYou're not allowed to retract your message.`);
         }));
 
         it("can be retracted by its author, causing a timeout error in response",
@@ -744,7 +744,7 @@ describe("Message Retractions", function () {
             const error_messages = view.querySelectorAll('.chat-msg__error');
             expect(error_messages.length).toBe(1);
             expect(error_messages[0].textContent.trim()).toBe(
-                'Message delivery failed: "A timeout happened while while trying to retract your message."');
+                'Message delivery failed.\nA timeout happened while while trying to retract your message.');
         }));
 
 

+ 1 - 1
src/shared/chat/templates/message-text.js

@@ -17,7 +17,7 @@ export default (el) => {
     const is_groupchat_message = (el.model.get('type') === 'groupchat');
     const i18n_show_less = __('Show less');
     const error_text = el.model.get('error_text') || el.model.get('error');
-    const i18n_error = __('Message delivery failed: "%1$s"', error_text);
+    const i18n_error = `${__('Message delivery failed.')}\n${error_text}`;
 
     const tplSpoilerHint = html`
         <div class="chat-msg__spoiler-hint">

+ 1 - 0
src/shared/styles/messages.scss

@@ -217,6 +217,7 @@
 
             .chat-msg__error {
                 color: var(--error-color);
+                white-space: pre-wrap;
             }
 
             .chat-msg__media {