瀏覽代碼

Initial work on message retractions

JC Brand 5 年之前
父節點
當前提交
b9fc223dae
共有 5 個文件被更改,包括 91 次插入8 次删除
  1. 53 0
      spec/muc_messages.js
  2. 27 7
      src/converse-muc-views.js
  3. 3 0
      src/headless/converse-core.js
  4. 6 1
      src/templates/message.html
  5. 2 0
      src/utils/html.js

+ 53 - 0
spec/muc_messages.js

@@ -11,6 +11,59 @@
 
     describe("A Groupchat Message", function () {
 
+        it("can be retracted by a moderator",
+            mock.initConverse(
+                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                async function (done, _converse) {
+
+            const muc_jid = 'lounge@montague.lit';
+            await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+            const view = _converse.api.chatviews.get(muc_jid);
+            const occupant = view.model.getOwnOccupant();
+            expect(occupant.get('role')).toBe('moderator');
+
+            const received_stanza = u.toStanza(`
+                <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
+                    <body>Visit this site to get free Bitcoin!</body>
+                </message>
+            `);
+            await view.model.onMessage(received_stanza);
+            await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+            const reason = "This content is inappropriate for this forum!"
+            spyOn(window, 'prompt').and.callFake(() => reason);
+            view.el.querySelector('.chat-msg__content .chat-msg__action-retract').click();
+            const sent_IQs = _converse.connection.IQ_stanzas;
+            const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
+            expect(window.prompt).toHaveBeenCalled();
+            expect(Strophe.serialize(stanza)).toBe(
+                `<iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
+                    `<apply-to 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>`);
+
+            // The server responds with a retraction message
+
+            const message = view.model.messages.at(0);
+            const stanza_id = message.get(`stanza-id ${message.get('from')}`);
+            const retraction = u.toStanza(`
+                <message type="groupchat" id='retraction-id-1' from='room@muc.example.com' to="juliet@capulet.example/balcony">
+                    <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>`);
+            _converse.connection._dataRecv(test_utils.createRequest(retraction));
+            await u.waitUntil(() => message.get('retracted'));
+            done();
+        }));
+
+
         it("is rejected if it's an unencapsulated forwarded message",
             mock.initConverse(
                 ['rosterGroupsFetched', 'chatBoxesFetched'], {},

+ 27 - 7
src/converse-muc-views.js

@@ -629,6 +629,7 @@ converse.plugins.add('converse-muc-views', {
             events: {
                 'change input.fileupload': 'onFileSelection',
                 'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
+                'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
                 'click .chatbox-navback': 'showControlBox',
                 'click .close-chatbox-button': 'close',
                 'click .configure-chatroom-button': 'getAndRenderConfigurationForm',
@@ -770,6 +771,27 @@ converse.plugins.add('converse-muc-views', {
                 return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
             },
 
+            onMessageRetractButtonClicked (ev) {
+                ev.preventDefault();
+                const reason = prompt(__('You are about to retract this message. '+
+                                         'You may optionally include a message, '+
+                                         'explaining the reason for the invitation.'));
+                if (reason !== null) {
+                    const msg_el = u.ancestor(ev.target, '.message');
+                    const msgid = msg_el.getAttribute('data-msgid');
+                    const time = msg_el.getAttribute('data-isodate');
+                    const message = this.model.messages.findWhere({msgid, time});
+                    const iq = $iq({'to': this.model.get('jid'), 'type': "set"})
+                        .c("apply-to", {
+                            'id': message.get(`stanza-id ${message.get('from')}`),
+                            'xmlns': Strophe.NS.FASTEN
+                        }).c('moderate', {xmlns: Strophe.NS.MODERATE})
+                            .c('retract', {xmlns: Strophe.NS.RETRACT}).up()
+                            .c('reason').t(reason);
+                    _converse.api.sendIQ(iq)
+                }
+            },
+
             showModeratorToolsModal (affiliation) {
                 if (!this.verifyRoles(['moderator'])) {
                     return;
@@ -1365,17 +1387,15 @@ converse.plugins.add('converse-muc-views', {
                 const message = this.model.get('password_validation_message');
                 this.model.save('password_validation_message', undefined);
 
-                if (!this.password_form) {
+                if (this.password_form) {
+                    this.password_form.model.set('validation_message', message);
+                } else {
                     this.password_form = new _converse.MUCPasswordForm({
-                        'model': new Backbone.Model({
-                            'validation_message': message
-                        }),
-                        'chatroomview': this,
+                        'model': new Backbone.Model({'validation_message': message}),
+                        'chatroomview': this
                     });
                     const container_el = this.el.querySelector('.chatroom-body');
                     container_el.insertAdjacentElement('beforeend', this.password_form.el);
-                } else {
-                    this.password_form.model.set('validation_message', message);
                 }
                 u.showElement(this.password_form.el);
                 this.model.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED);

+ 3 - 0
src/headless/converse-core.js

@@ -35,16 +35,19 @@ Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2');
 Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
 Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
 Strophe.addNamespace('DELAY', 'urn:xmpp:delay');
+Strophe.addNamespace('FASTEN', 'urn:xmpp:fasten:0');
 Strophe.addNamespace('FORWARD', 'urn:xmpp:forward:0');
 Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
 Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0');
 Strophe.addNamespace('IDLE', 'urn:xmpp:idle:1');
 Strophe.addNamespace('MAM', 'urn:xmpp:mam:2');
+Strophe.addNamespace('MODERATE', 'urn:xmpp:message-moderate:0');
 Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
 Strophe.addNamespace('OMEMO', 'eu.siacs.conversations.axolotl');
 Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob');
 Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
 Strophe.addNamespace('REGISTER', 'jabber:iq:register');
+Strophe.addNamespace('RETRACT', 'urn:xmpp:message-retract:0');
 Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx');
 Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
 Strophe.addNamespace('SID', 'urn:xmpp:sid:0');

+ 6 - 1
src/templates/message.html

@@ -33,7 +33,12 @@
             {[ if (o.edited) { ]} <i title="{{{o.__('This message has been edited')}}}" class="fa fa-edit chat-msg__edit-modal"></i> {[ } ]}
             {[ if (o.editable) { ]}
                 <div class="chat-msg__actions">
-                    <button class="chat-msg__action chat-msg__action-edit fa fa-pencil-alt" title="{{{o.__('Edit this message')}}}"></button>
+                    {[ if (o.sender === 'me') { ]}
+                        <button class="chat-msg__action chat-msg__action-edit fa fa-pencil-alt" title="{{{o.__('Edit this message')}}}"></button>
+                    {[ } ]}
+                    {[ if (o.sender === 'me' || o.is_groupchat_message && true) { ]}
+                        <button class="chat-msg__action chat-msg__action-retract fa fa-trash-alt" title="{{{o.__('Retract this message')}}}"></button>
+                    {[ } ]}
                 </div>
             {[ } ]}
         </div>

+ 2 - 0
src/utils/html.js

@@ -293,6 +293,8 @@ u.ancestor = function (el, selector) {
  * Return the element's siblings until one matches the selector.
  * @private
  * @method u#nextUntil
+ * @param { HTMLElement } el
+ * @param { String } selector
  */
 u.nextUntil = function (el, selector) {
     const matches = [];