Prechádzať zdrojové kódy

feat: Update XEP-0425 implementation to latest spec version

JC Brand 6 mesiacov pred
rodič
commit
77e9e6789e

+ 3 - 0
CHANGES.md

@@ -49,6 +49,9 @@
 - Fix: renaming getEmojisByAtrribute to getEmojisByAttribute.
 
 ### Changes and features
+- Upgrade to the latest versions of XEP-0424 and XEP-0425 (Message Retraction and Message Moderation).
+  Converse loses the ability to retract or moderate messages in the older format,
+  so you might need to upgrade your XMPP server's implementation of these as well.
 - Embed the Spotify player for links to Spotify tracks. New config option [embed_3rd_party_media_players](https://conversejs.org/docs/html/configuration.html#embed-3rd-party-media-players).
 - Add support for XEP-0191 Blocking Command
 - Upgrade to Bootstrap 5

+ 3 - 5
src/headless/plugins/muc/message.js

@@ -41,11 +41,10 @@ class MUCMessage extends Message {
     /**
      * Determines whether this messsage may be moderated,
      * based on configuration settings and server support.
-     * @async
      * @method _converse.ChatRoomMessages#mayBeModerated
-     * @returns {boolean}
+     * @returns {Promise<boolean>}
      */
-    mayBeModerated () {
+    async mayBeModerated () {
         if (typeof this.get('from_muc')  === 'undefined') {
             // If from_muc is not defined, then this message hasn't been
             // reflected yet, which means we won't have a XEP-0359 stanza id.
@@ -53,8 +52,7 @@ class MUCMessage extends Message {
         }
         return (
             ['all', 'moderator'].includes(api.settings.get('allow_message_retraction')) &&
-            this.get(`stanza_id ${this.get('from_muc')}`) &&
-            this.chatbox.canModerateMessages()
+            this.get(`stanza_id ${this.get('from_muc')}`) && await this.chatbox.canModerateMessages()
         );
     }
 

+ 8 - 11
src/headless/plugins/muc/muc.js

@@ -887,16 +887,13 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
      * @param {string} [reason] - The reason for retracting the message.
      */
     sendRetractionIQ (message, reason) {
-        const iq = $iq({ 'to': this.get('jid'), 'type': 'set' })
-            .c('apply-to', {
-                'id': message.get(`stanza_id ${this.get('jid')}`),
-                'xmlns': Strophe.NS.FASTEN,
-            })
-            .c('moderate', { xmlns: Strophe.NS.MODERATE })
-            .c('retract', { xmlns: Strophe.NS.RETRACT0 })
-            .up()
-            .c('reason')
-            .t(reason || '');
+        const iq = stx`
+            <iq to="${this.get('jid')}" type="set" xmlns="jabber:client">
+                <moderate id="${message.get(`stanza_id ${this.get('jid')}`)}" xmlns="${Strophe.NS.MODERATE}">
+                    <retract xmlns="${Strophe.NS.RETRACT}"/>
+                    ${reason ? stx`<reason>${reason}</reason>` : ''}
+                </moderate>
+            </iq>`;
         return api.sendIQ(iq, null, false);
     }
 
@@ -2332,8 +2329,8 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
             this.handleMUCPrivateMessage(attrs) ||
             this.handleMetadataFastening(attrs) ||
             this.handleMEPNotification(attrs) ||
-            (await this.handleRetraction(attrs)) ||
             (await this.handleModeration(attrs)) ||
+            (await this.handleRetraction(attrs)) ||
             (await this.handleSubjectChange(attrs))
         ) {
             attrs.nick && this.removeNotification(attrs.nick, ['composing', 'paused']);

+ 26 - 2
src/headless/plugins/muc/parsers.js

@@ -85,7 +85,7 @@ function getDeprecatedModerationAttributes(stanza) {
     const fastening = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
     if (fastening) {
         const applies_to_id = fastening.getAttribute('id');
-        const moderated = sizzle(`moderated[xmlns="${Strophe.NS.MODERATE}"]`, fastening).pop();
+        const moderated = sizzle(`moderated[xmlns="${Strophe.NS.MODERATE0}"]`, fastening).pop();
         if (moderated) {
             const retracted = sizzle(`retract[xmlns="${Strophe.NS.RETRACT0}"]`, moderated).pop();
             if (retracted) {
@@ -99,7 +99,7 @@ function getDeprecatedModerationAttributes(stanza) {
             }
         }
     } else {
-        const tombstone = sizzle(`> moderated[xmlns="${Strophe.NS.MODERATE}"]`, stanza).pop();
+        const tombstone = sizzle(`> moderated[xmlns="${Strophe.NS.MODERATE0}"]`, stanza).pop();
         if (tombstone) {
             const retracted = sizzle(`retracted[xmlns="${Strophe.NS.RETRACT0}"]`, tombstone).pop();
             if (retracted) {
@@ -122,6 +122,30 @@ function getDeprecatedModerationAttributes(stanza) {
  * @returns {Object}
  */
 function getModerationAttributes(stanza) {
+    const retract = sizzle(`> retract[xmlns="${Strophe.NS.RETRACT}"]`, stanza).pop();
+    if (retract) {
+        const moderated = sizzle(`moderated[xmlns="${Strophe.NS.MODERATE}"]`, retract).pop();
+        if (moderated) {
+            return {
+                editable: false,
+                moderated: 'retracted',
+                moderated_by: moderated.getAttribute('by'),
+                moderated_id: retract.getAttribute('id'),
+                moderation_reason: retract.querySelector('reason')?.textContent,
+            };
+        }
+    } else {
+        const tombstone = sizzle(`retracted[xmlns="${Strophe.NS.RETRACT}"]`, stanza).pop();
+        if (tombstone) {
+            return {
+                editable: false,
+                is_tombstone: true,
+                moderated_by: tombstone.getAttribute('by'),
+                retracted: tombstone.getAttribute('stamp'),
+                moderation_reason: tombstone.querySelector('reason')?.textContent,
+            };
+        }
+    }
     return getDeprecatedModerationAttributes(stanza);
 }
 

+ 2 - 1
src/headless/shared/constants.js

@@ -85,7 +85,8 @@ Strophe.addNamespace('MAM', 'urn:xmpp:mam:2');
 Strophe.addNamespace('MARKERS', 'urn:xmpp:chat-markers:0');
 Strophe.addNamespace('MENTIONS', 'urn:xmpp:mmn:0');
 Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
-Strophe.addNamespace('MODERATE', 'urn:xmpp:message-moderate:0');
+Strophe.addNamespace('MODERATE', 'urn:xmpp:message-moderate:1');
+Strophe.addNamespace('MODERATE0', 'urn:xmpp:message-moderate:0');
 Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
 Strophe.addNamespace('OCCUPANTID', 'urn:xmpp:occupant-id:0');
 Strophe.addNamespace('OMEMO', 'eu.siacs.conversations.axolotl');

+ 6 - 21
src/headless/shared/model-with-messages.js

@@ -347,7 +347,7 @@ export default function ModelWithMessages(BaseModel) {
         }
 
         /**
-         * @param {File[]} files
+         * @param {File[]} files'
          */
         async sendFiles(files) {
             const { __, session } = _converse;
@@ -831,7 +831,7 @@ export default function ModelWithMessages(BaseModel) {
             if (this.get('num_unread') > 0) {
                 this.sendMarkerForMessage(this.messages.last());
             }
-            u.safeSave(this, { 'num_unread': 0 });
+            u.safeSave(this, { num_unread: 0 });
         }
 
         /**
@@ -844,10 +844,9 @@ export default function ModelWithMessages(BaseModel) {
         async handleRetraction(attrs) {
             const RETRACTION_ATTRIBUTES = ['retracted', 'retracted_id', 'editable'];
             if (attrs.retracted) {
-                if (attrs.is_tombstone) {
-                    return false;
-                }
-                const message = this.messages.findWhere({ 'origin_id': attrs.retracted_id, 'from': attrs.from });
+                if (attrs.is_tombstone) return false;
+
+                const message = this.messages.findWhere({ origin_id: attrs.retracted_id, from: attrs.from });
                 if (!message) {
                     attrs['dangling_retraction'] = true;
                     await this.createMessage(attrs);
@@ -855,26 +854,12 @@ export default function ModelWithMessages(BaseModel) {
                 }
                 message.save(pick(attrs, RETRACTION_ATTRIBUTES));
                 return true;
-            } else if (attrs.retracted_id) {
-                // Handle direct retract element
-                const message = this.messages.findWhere({ 'origin_id': attrs.retracted_id, 'from': attrs.from });
-                if (!message) {
-                    attrs['dangling_retraction'] = true;
-                    await this.createMessage(attrs);
-                    return true;
-                }
-                message.save({
-                    'retracted': new Date().toISOString(),
-                    'retracted_id': attrs.retracted_id,
-                    'editable': false
-                });
-                return true;
             } else {
                 // Check if we have dangling retraction
                 const message = this.findDanglingRetraction(attrs);
                 if (message) {
                     const retraction_attrs = pick(message.attributes, RETRACTION_ATTRIBUTES);
-                    const new_attrs = Object.assign({ 'dangling_retraction': false }, attrs, retraction_attrs);
+                    const new_attrs = Object.assign({ dangling_retraction: false }, attrs, retraction_attrs);
                     delete new_attrs['id']; // Delete id, otherwise a new cache entry gets created
                     message.save(new_attrs);
                     return true;

+ 2 - 3
src/headless/types/plugins/muc/message.d.ts

@@ -4,11 +4,10 @@ declare class MUCMessage extends Message {
     /**
      * Determines whether this messsage may be moderated,
      * based on configuration settings and server support.
-     * @async
      * @method _converse.ChatRoomMessages#mayBeModerated
-     * @returns {boolean}
+     * @returns {Promise<boolean>}
      */
-    mayBeModerated(): boolean;
+    mayBeModerated(): Promise<boolean>;
     checkValidity(): any;
     onOccupantRemoved(): void;
     /**

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

@@ -87,7 +87,7 @@ export default function ModelWithMessages<T extends import("./types").ModelExten
          */
         retractOwnMessage(message: import("../plugins/chat/message").default): void;
         /**
-         * @param {File[]} files
+         * @param {File[]} files'
          */
         sendFiles(files: File[]): Promise<void>;
         /**

+ 1 - 1
src/plugins/chatview/tests/message-images.js

@@ -178,7 +178,7 @@ describe("A Chat Message", function () {
         spyOn(view.model, 'sendMessage').and.callThrough();
         await mock.sendMessage(view, message);
         expect(view.model.sendMessage).toHaveBeenCalled();
-        await u.waitUntil(() => view.querySelector('.chat-content .chat-msg'), 1000);
+        await u.waitUntil(() => view.querySelector('.chat-content .chat-msg'));
         const msg = view.querySelector('.chat-content .chat-msg .chat-msg__text');
         await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '').trim() ==
             `<a target="_blank" rel="noopener" href="https://pbs.twimg.com/media/string?format=jpg&amp;name=small">https://pbs.twimg.com/media/string?format=jpg&amp;name=small</a>`, 1000);

+ 8 - 179
src/plugins/muc-views/tests/deprecated-retractions.js

@@ -2,13 +2,15 @@
 const { Strophe, u, stx } = converse.env;
 
 describe("Deprecated Message Retractions", function () {
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     describe("A groupchat message retraction", function () {
 
         it("is not applied if it's not from the right author",
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
             const muc_jid = 'lounge@montague.lit';
-            const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
+            const features = [...mock.default_muc_features, Strophe.NS.MODERATE0];
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
 
             const received_stanza = stx`
@@ -58,7 +60,7 @@ describe("Deprecated Message Retractions", function () {
 
             const date = (new Date()).toISOString();
             const muc_jid = 'lounge@montague.lit';
-            const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
+            const features = [...mock.default_muc_features, Strophe.NS.MODERATE0];
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
 
             const retraction_stanza = stx`
@@ -119,7 +121,7 @@ describe("Deprecated Message Retractions", function () {
 
             const date = (new Date()).toISOString();
             const muc_jid = 'lounge@montague.lit';
-            const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
+            const features = [...mock.default_muc_features, Strophe.NS.MODERATE0];
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
             const retraction_stanza = stx`
                 <message xmlns="jabber:client" from="${muc_jid}" type="groupchat" id="retraction-id-1">
@@ -294,7 +296,7 @@ describe("Deprecated Message Retractions", function () {
 
         it("can be followed up by a retraction by the author", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
             const muc_jid = 'lounge@montague.lit';
-            const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
+            const features = [...mock.default_muc_features, Strophe.NS.MODERATE0];
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
 
             const received_stanza = stx`
@@ -336,102 +338,6 @@ describe("Deprecated Message Retractions", function () {
             expect(msg_el.querySelector('.chat-msg--retracted q')).toBe(null);
         }));
 
-
-        it("can be retracted by a moderator, with the IQ response received before the retraction message",
-                mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
-
-            const muc_jid = 'lounge@montague.lit';
-            const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
-            await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
-
-            const view = _converse.chatboxviews.get(muc_jid);
-            const occupant = view.model.getOwnOccupant();
-            expect(occupant.get('role')).toBe('moderator');
-
-            const received_stanza = stx`
-                <message to="${_converse.jid}"
-                        from="${muc_jid}/mallory"
-                        type="groupchat"
-                        id="${_converse.api.connection.get().getUniqueId()}"
-                        xmlns="jabber:client">
-                    <body>Visit this site to get free Bitcoin!</body>
-                    <stanza-id xmlns="urn:xmpp:sid:0" id="stanza-id-1" by="${muc_jid}"/>
-                </message>`;
-            await view.model.handleMessageStanza(received_stanza);
-            await u.waitUntil(() => view.model.messages.length === 1);
-            expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
-
-            const reason = "This content is inappropriate for this forum!"
-            const retract_button = await u.waitUntil(() => view.querySelector('.chat-msg__content .chat-msg__action-retract'));
-            retract_button.click();
-
-            await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
-
-            const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]');
-            reason_input.value = 'This content is inappropriate for this forum!';
-            const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
-            submit_button.click();
-
-            const sent_IQs = _converse.api.connection.get().IQ_stanzas;
-            const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
-            const message = view.model.messages.at(0);
-            const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
-
-            expect(Strophe.serialize(stanza)).toBe(
-                `<iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
-                    `<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">`+
-                        `<moderate xmlns="urn:xmpp:message-moderate:0">`+
-                            `<retract xmlns="urn:xmpp:message-retract:0"/>`+
-                            `<reason>This content is inappropriate for this forum!</reason>`+
-                        `</moderate>`+
-                    `</apply-to>`+
-                `</iq>`);
-
-            const result_iq = stx`
-                <iq from="${muc_jid}"
-                    id="${stanza.getAttribute('id')}"
-                    to="${_converse.bare_jid}"
-                    type="result"
-                    xmlns="jabber:client"/>`;
-            _converse.api.connection.get()._dataRecv(mock.createRequest(result_iq));
-
-            // We opportunistically save the message as retracted, even before receiving the retraction message
-            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1);
-            expect(view.model.messages.length).toBe(1);
-            expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
-            expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
-            expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
-            expect(view.model.messages.at(0).get('editable')).toBe(false);
-            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
-
-            const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message');
-            expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message');
-
-            const qel = msg_el.querySelector('q');
-            expect(qel.textContent.trim()).toBe('This content is inappropriate for this forum!');
-
-            // The server responds with a retraction message
-            const retraction = stx`
-                <message type="groupchat"
-                        id="retraction-id-1"
-                        from="${muc_jid}"
-                        to="${muc_jid}/romeo"
-                        xmlns="jabber:client">
-                    <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
-                        <moderated by="${_converse.bare_jid}" xmlns="urn:xmpp:message-moderate:0">
-                        <retract xmlns="urn:xmpp:message-retract:0" />
-                        <reason>${reason}</reason>
-                        </moderated>
-                    </apply-to>
-                </message>`;
-            await view.model.handleMessageStanza(retraction);
-            expect(view.model.messages.length).toBe(1);
-            expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
-            expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
-            expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
-            expect(view.model.messages.at(0).get('editable')).toBe(false);
-        }));
-
         it("can not be retracted if the MUC doesn't support message moderation",
                 mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
 
@@ -456,83 +362,6 @@ describe("Deprecated Message Retractions", function () {
             const result = await view.model.canModerateMessages();
             expect(result).toBe(false);
         }));
-
-
-        it("can be retracted by a moderator, with the retraction message received before the IQ response",
-                mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
-
-            const muc_jid = 'lounge@montague.lit';
-            const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
-            await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
-            const view = _converse.chatboxviews.get(muc_jid);
-            const occupant = view.model.getOwnOccupant();
-            expect(occupant.get('role')).toBe('moderator');
-
-            const received_stanza = stx`
-                <message to="${_converse.jid}"
-                        from="${muc_jid}/mallory"
-                        type="groupchat"
-                        id="${_converse.api.connection.get().getUniqueId()}"
-                        xmlns="jabber:client">
-                    <body>Visit this site to get free Bitcoin!</body>
-                    <stanza-id xmlns="urn:xmpp:sid:0" id="stanza-id-1" by="${muc_jid}"/>
-                </message>`;
-            await view.model.handleMessageStanza(received_stanza);
-            await u.waitUntil(() => view.model.messages.length === 1);
-            expect(view.model.messages.length).toBe(1);
-
-            const retract_button = await u.waitUntil(() => view.querySelector('.chat-msg__content .chat-msg__action-retract'));
-            retract_button.click();
-            await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
-
-            const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]');
-            const reason = "This content is inappropriate for this forum!"
-            reason_input.value = reason;
-            const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
-            submit_button.click();
-
-            const sent_IQs = _converse.api.connection.get().IQ_stanzas;
-            const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
-            const message = view.model.messages.at(0);
-            const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
-            // The server responds with a retraction message
-            const retraction = stx`
-                <message type="groupchat"
-                        id='retraction-id-1'
-                        from="${muc_jid}"
-                        to="${muc_jid}/romeo"
-                        xmlns="jabber:client">
-                    <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
-                        <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
-                            <retract xmlns='urn:xmpp:message-retract:0' />
-                            <reason>${reason}</reason>
-                        </moderated>
-                    </apply-to>
-                </message>`;
-            await view.model.handleMessageStanza(retraction);
-
-            await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1);
-            expect(view.model.messages.length).toBe(1);
-            expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
-            expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
-            const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
-            expect(msg_el.textContent).toBe('romeo has removed this message');
-            const qel = view.querySelector('.chat-msg--retracted .chat-msg__message q');
-            expect(qel.textContent).toBe('This content is inappropriate for this forum!');
-
-            const result_iq = stx`
-                <iq from="${muc_jid}"
-                    id="${stanza.getAttribute('id')}"
-                    to="${_converse.bare_jid}"
-                    type="result"
-                    xmlns="jabber:client"/>`;
-            _converse.api.connection.get()._dataRecv(mock.createRequest(result_iq));
-            expect(view.model.messages.length).toBe(1);
-            expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
-            expect(view.model.messages.at(0).get('moderated_by')).toBe(_converse.bare_jid);
-            expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
-            expect(view.model.messages.at(0).get('editable')).toBe(false);
-        }));
     });
 
 
@@ -632,7 +461,7 @@ describe("Deprecated Message Retractions", function () {
                 async function (_converse) {
 
             const muc_jid = 'lounge@montague.lit';
-            const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
+            const features = [...mock.default_muc_features, Strophe.NS.MODERATE0];
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
             const view = _converse.chatboxviews.get(muc_jid);
 
@@ -709,7 +538,7 @@ describe("Deprecated Message Retractions", function () {
                 async function (_converse) {
 
             const muc_jid = 'lounge@montague.lit';
-            const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
+            const features = [...mock.default_muc_features, Strophe.NS.MODERATE0];
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
             const view = _converse.chatboxviews.get(muc_jid);
 

+ 12 - 18
src/plugins/muc-views/tests/mep.js

@@ -1,8 +1,8 @@
 /*global mock, converse */
-
 const { u, Strophe, stx } = converse.env;
 
 describe("A XEP-0316 MEP notification", function () {
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
 
     it("is rendered as an info message",
             mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
@@ -216,19 +216,16 @@ describe("A XEP-0316 MEP notification", function () {
         submit_button.click();
 
         const sent_IQs = _converse.api.connection.get().IQ_stanzas;
-        const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
+        const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq moderate')).pop());
         const message = view.model.messages.at(0);
         const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
 
-        expect(Strophe.serialize(stanza)).toBe(
-            `<iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
-                `<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">`+
-                    `<moderate xmlns="urn:xmpp:message-moderate:0">`+
-                        `<retract xmlns="urn:xmpp:message-retract:0"/>`+
-                        `<reason></reason>`+
-                    `</moderate>`+
-                `</apply-to>`+
-            `</iq>`);
+        expect(stanza).toEqualStanza(stx`
+            <iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">
+                <moderate id="${stanza_id}" xmlns="urn:xmpp:message-moderate:1">
+                    <retract xmlns="urn:xmpp:message-retract:1"/>
+                </moderate>
+            </iq>`);
 
         // The server responds with a retraction message
         const retraction = stx`
@@ -237,17 +234,14 @@ describe("A XEP-0316 MEP notification", function () {
                     from="${muc_jid}"
                     to="${muc_jid}/${nick}"
                     xmlns="jabber:client">
-                <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
-                    <moderated by="${_converse.bare_jid}" xmlns="urn:xmpp:message-moderate:0">
-                        <retract xmlns="urn:xmpp:message-retract:0" />
-                        <reason></reason>
-                    </moderated>
-                </apply-to>
+                <retract id="${stanza_id}" xmlns="urn:xmpp:message-retract:1">
+                    <moderated by="${_converse.bare_jid}" xmlns="urn:xmpp:message-moderate:1" />
+                </retract>
             </message>`;
         await view.model.handleMessageStanza(retraction);
         expect(view.model.messages.length).toBe(1);
         expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
-        expect(view.model.messages.at(0).get('moderation_reason')).toBe('');
+        expect(view.model.messages.at(0).get('moderation_reason')).toBeUndefined;
         expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
         expect(view.model.messages.at(0).get('editable')).toBe(false);
         const msg_el = view.querySelector('.chat-msg--retracted .chat-info__message div');

+ 45 - 57
src/plugins/muc-views/tests/retractions.js

@@ -64,9 +64,8 @@ describe("Message Retractions", function () {
                         from="${muc_jid}/mallory"
                         to="${muc_jid}/romeo"
                         xmlns="jabber:client">
-                    <apply-to id="stanza-id-1" xmlns="urn:xmpp:fasten:0">
-                        <retract xmlns="urn:xmpp:message-retract:0" />
-                    </apply-to>
+                    <retract id="stanza-id-1" xmlns="urn:xmpp:message-retract:1"/>
+                    <body>/me retracted a message</body>
                 </message>
             `;
             spyOn(view.model, 'handleRetraction').and.callThrough();
@@ -98,11 +97,11 @@ describe("Message Retractions", function () {
                         from="${muc_jid}/eve"
                         to="${muc_jid}/romeo"
                         xmlns="jabber:client">
-                    <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
-                        <retract by="${muc_jid}/eve" xmlns="urn:xmpp:message-retract:0" />
-                    </apply-to>
-                </message>
-            `;
+                    <retract id="origin-id-1" xmlns="urn:xmpp:message-retract:1"/>
+                    <fallback xmlns="urn:xmpp:fallback:0" for='urn:xmpp:message-retract:1'/>
+                    <body>/me retracted a message</body>
+                    <store xmlns="urn:xmpp:hints"/>
+                </message>`;
             const view = _converse.chatboxviews.get(muc_jid);
             spyOn(converse.env.log, 'warn');
             spyOn(view.model, 'handleRetraction').and.callThrough();
@@ -125,8 +124,7 @@ describe("Message Retractions", function () {
                     <delay xmlns="urn:xmpp:delay" stamp="${date}"/>
                     <stanza-id xmlns="urn:xmpp:sid:0" id="stanza-id-1" by="${muc_jid}"/>
                     <origin-id xmlns="urn:xmpp:sid:0" id="origin-id-1"/>
-                </message>
-            `;
+                </message>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(received_stanza));
             await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
             await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1, 1000);
@@ -154,12 +152,10 @@ describe("Message Retractions", function () {
             await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
             const retraction_stanza = stx`
                 <message xmlns="jabber:client" from="${muc_jid}" type="groupchat" id="retraction-id-1">
-                    <apply-to xmlns="urn:xmpp:fasten:0" id="stanza-id-1">
-                        <moderated xmlns="urn:xmpp:message-moderate:0" by="${muc_jid}/madison">
-                            <retract xmlns="urn:xmpp:message-retract:0"/>
-                            <reason>Insults</reason>
-                        </moderated>
-                    </apply-to>
+                    <retract id="stanza-id-1" xmlns='urn:xmpp:message-retract:1'>
+                        <moderated xmlns="urn:xmpp:message-moderate:1" by="${muc_jid}/madison"/>
+                        <reason>Insults</reason>
+                    </retract>
                 </message>
             `;
             const view = _converse.chatboxviews.get(muc_jid);
@@ -397,9 +393,11 @@ describe("Message Retractions", function () {
                         from="${muc_jid}/eve"
                         to="${muc_jid}/romeo"
                         xmlns="jabber:client">
-                    <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
-                        <retract by="${muc_jid}/eve" xmlns="urn:xmpp:message-retract:0" />
-                    </apply-to>
+
+                    <retract id="origin-id-1" xmlns='urn:xmpp:message-retract:1'/>
+                    <fallback xmlns="urn:xmpp:fallback:0" for='urn:xmpp:message-retract:1'/>
+                    <body>/me retracted a previous message, but it's unsupported by your client.</body>
+                    <store xmlns="urn:xmpp:hints"/>
                 </message>`;
             _converse.api.connection.get()._dataRecv(mock.createRequest(retraction_stanza));
 
@@ -451,19 +449,18 @@ describe("Message Retractions", function () {
             submit_button.click();
 
             const sent_IQs = _converse.api.connection.get().IQ_stanzas;
-            const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
+            const stanza = await u.waitUntil(
+                () => sent_IQs.filter(iq => iq.querySelector('iq retract')).pop());
             const message = view.model.messages.at(0);
             const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
 
-            expect(Strophe.serialize(stanza)).toBe(
-                `<iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
-                    `<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">`+
-                        `<moderate xmlns="urn:xmpp:message-moderate:0">`+
-                            `<retract xmlns="urn:xmpp:message-retract:0"/>`+
-                            `<reason>This content is inappropriate for this forum!</reason>`+
-                        `</moderate>`+
-                    `</apply-to>`+
-                `</iq>`);
+            expect(stanza).toEqualStanza(stx`
+                <iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">
+                    <moderate id="${stanza_id}" xmlns="urn:xmpp:message-moderate:1">
+                        <retract xmlns="urn:xmpp:message-retract:1"/>
+                        <reason>This content is inappropriate for this forum!</reason>
+                    </moderate>
+                </iq>`);
 
             const result_iq = stx`
                 <iq from="${muc_jid}"
@@ -495,12 +492,10 @@ describe("Message Retractions", function () {
                         from="${muc_jid}"
                         to="${muc_jid}/romeo"
                         xmlns="jabber:client">
-                    <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
-                        <moderated by="${_converse.bare_jid}" xmlns="urn:xmpp:message-moderate:0">
-                        <retract xmlns="urn:xmpp:message-retract:0" />
+                    <moderated by="${_converse.bare_jid}" xmlns="urn:xmpp:message-moderate:0">
+                        <retract id="${stanza_id}" xmlns="urn:xmpp:message-retract:1"/>
                         <reason>${reason}</reason>
-                        </moderated>
-                    </apply-to>
+                    </moderated>
                 </message>`;
             await view.model.handleMessageStanza(retraction);
             expect(view.model.messages.length).toBe(1);
@@ -570,7 +565,7 @@ describe("Message Retractions", function () {
             submit_button.click();
 
             const sent_IQs = _converse.api.connection.get().IQ_stanzas;
-            const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
+            const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq retract')).pop());
             const message = view.model.messages.at(0);
             const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
             // The server responds with a retraction message
@@ -580,12 +575,10 @@ describe("Message Retractions", function () {
                         from="${muc_jid}"
                         to="${muc_jid}/romeo"
                         xmlns="jabber:client">
-                    <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
-                        <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
-                            <retract xmlns='urn:xmpp:message-retract:0' />
-                            <reason>${reason}</reason>
-                        </moderated>
-                    </apply-to>
+                    <retract id="${stanza_id}" xmlns='urn:xmpp:message-retract:1'>
+                        <moderated by="${_converse.bare_jid}" xmlns='urn:xmpp:message-moderate:1' />
+                        <reason>${reason}</reason>
+                    </retract>
                 </message>`;
             await view.model.handleMessageStanza(retraction);
 
@@ -847,17 +840,14 @@ describe("Message Retractions", function () {
             submit_button.click();
 
             const sent_IQs = _converse.api.connection.get().IQ_stanzas;
-            const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
-
-            expect(Strophe.serialize(stanza)).toBe(
-                `<iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
-                    `<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">`+
-                        `<moderate xmlns="urn:xmpp:message-moderate:0">`+
-                            `<retract xmlns="urn:xmpp:message-retract:0"/>`+
-                            `<reason></reason>`+
-                        `</moderate>`+
-                    `</apply-to>`+
-                `</iq>`);
+            const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq moderate')).pop());
+
+            expect(stanza).toEqualStanza(stx`
+                <iq type="set" to="${muc_jid}" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
+                    <moderate id="${stanza_id}" xmlns="urn:xmpp:message-moderate:1">
+                        <retract xmlns="urn:xmpp:message-retract:1"/>
+                    </moderate>
+                </iq>`);
 
             const result_iq = stx`
                 <iq from="${muc_jid}"
@@ -887,11 +877,9 @@ describe("Message Retractions", function () {
                         from="${muc_jid}"
                         to="${muc_jid}/romeo"
                         xmlns="jabber:client">
-                    <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
-                        <moderated by="${_converse.bare_jid}" xmlns="urn:xmpp:message-moderate:0">
-                        <retract xmlns="urn:xmpp:message-retract:0" />
-                        </moderated>
-                    </apply-to>
+                    <retract id="${stanza_id}" xmlns="urn:xmpp:message-retract:1">
+                        <moderated by="${_converse.bare_jid}" xmlns="urn:xmpp:message-moderate:0"/>
+                    </retract>
                 </message>`;
             await view.model.handleMessageStanza(retraction);
             expect(view.model.messages.length).toBe(1);

+ 1 - 1
src/shared/styles/themes/cyberpunk.scss

@@ -9,7 +9,7 @@
     --blue: #1ba2f6;
     --cyan: #32fbe2;
     --green: #3cf281;
-    --indigo: #955df0;
+    --indigo: #6610f2;
     --orange: #f1b633;
     --pink: #ea39b8;
     --purple: #6f42c1;