Explorar o código

Add support for XEP-0317 MUC Hats

JC Brand %!s(int64=5) %!d(string=hai) anos
pai
achega
df9612f937

+ 11 - 0
docs/source/configuration.rst

@@ -959,6 +959,17 @@ VCard is taken, and if that is not set but `muc_nickname_from_jid`_ is set to
 
 If no nickame value is found, then an error will be raised.
 
+muc_hats_from_vcard
+-------------------
+
+* Default: ``false``
+
+Since version 7 Converse now has rudimentary support for `XEP-0317 Hats <https://xmpp.org/extensions/xep-0317.html>`_.
+
+Previously we used a non-standard hack of showing the VCard roles as if they
+were hats. Set this value to ``true`` for the old behaviour.
+
+
 muc_mention_autocomplete_min_chars
 -----------------------------------
 

+ 0 - 1
sass/_messages.scss

@@ -263,7 +263,6 @@
                 margin-top: 0.5em;
                 padding-right: 0.25rem;
                 padding-bottom: 0.25rem;
-                display: flex;
 
                 .chat-msg__author {
                     overflow: hidden;

+ 85 - 0
spec/hats.js

@@ -0,0 +1,85 @@
+(function (root, factory) {
+    define([
+        "jasmine",
+        "mock",
+        "test-utils"
+        ], factory);
+} (this, function (jasmine, mock, test_utils) {
+    "use strict";
+    const u = converse.env.utils;
+
+    describe("A XEP-0317 MUC Hat", function () {
+
+        it("can be included in a presence stanza",
+            mock.initConverse(
+                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                async function (done, _converse) {
+
+            const muc_jid = 'lounge@montague.lit';
+            await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+            const view = _converse.chatboxviews.get(muc_jid);
+            const hat1_id = u.getUniqueId();
+            const hat2_id = u.getUniqueId();
+            _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(`
+                <presence from="${muc_jid}/Terry" id="${u.getUniqueId()}" to="${_converse.jid}">
+                    <x xmlns="http://jabber.org/protocol/muc#user">
+                        <item affiliation="member" role="participant"/>
+                    </x>
+                    <hats xmlns="xmpp:prosody.im/protocol/hats:1">
+                        <hat title="Teacher&apos;s Assistant" id="${hat1_id}"/>
+                        <hat title="Dark Mage" id="${hat2_id}"/>
+                    </hats>
+                </presence>
+            `)));
+            await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
+                "romeo and Terry have entered the groupchat");
+
+            let hats = view.model.getOccupant("Terry").get('hats');
+            expect(hats.length).toBe(2);
+            expect(hats.map(h => h.title).join(' ')).toBe("Teacher's Assistant Dark Mage");
+
+            _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(`
+                <message type="groupchat" from="${muc_jid}/Terry" id="${u.getUniqueId()}" to="${_converse.jid}">
+                    <body>Hello world</body>
+                </message>
+            `)));
+
+            const msg_el = await u.waitUntil(() => view.el.querySelector('.chat-msg'));
+            let badges = Array.from(msg_el.querySelectorAll('.badge'));
+            expect(badges.length).toBe(2);
+            expect(badges.map(b => b.textContent.trim()).join(' ' )).toBe("Teacher's Assistant Dark Mage");
+
+            const hat3_id = u.getUniqueId();
+            _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(`
+                <presence from="${muc_jid}/Terry" id="${u.getUniqueId()}" to="${_converse.jid}">
+                    <x xmlns="http://jabber.org/protocol/muc#user">
+                        <item affiliation="member" role="participant"/>
+                    </x>
+                    <hats xmlns="xmpp:prosody.im/protocol/hats:1">
+                        <hat title="Teacher&apos;s Assistant" id="${hat1_id}"/>
+                        <hat title="Dark Mage" id="${hat2_id}"/>
+                        <hat title="Mad hatter" id="${hat3_id}"/>
+                    </hats>
+                </presence>
+            `)));
+
+            await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 3);
+            hats = view.model.getOccupant("Terry").get('hats');
+            expect(hats.map(h => h.title).join(' ')).toBe("Teacher's Assistant Dark Mage Mad hatter");
+            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .badge').length === 3);
+            badges = Array.from(view.el.querySelectorAll('.chat-msg .badge'));
+            expect(badges.map(b => b.textContent.trim()).join(' ' )).toBe("Teacher's Assistant Dark Mage Mad hatter");
+
+            _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(`
+                <presence from="${muc_jid}/Terry" id="${u.getUniqueId()}" to="${_converse.jid}">
+                    <x xmlns="http://jabber.org/protocol/muc#user">
+                        <item affiliation="member" role="participant"/>
+                    </x>
+                </presence>
+            `)));
+            await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 0);
+            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .badge').length === 0);
+            done();
+        }));
+    })
+}));

+ 18 - 5
src/converse-message-view.js

@@ -67,6 +67,7 @@ converse.plugins.add('converse-message-view', {
 
 
         api.settings.update({
+            'muc_hats_from_vcard': false,
             'show_images_inline': true,
             'time_format': 'HH:mm',
         });
@@ -107,8 +108,9 @@ converse.plugins.add('converse-message-view', {
                 }
 
                 if (this.model.occupant) {
-                    this.listenTo(this.model.occupant, 'change:role', this.debouncedRender);
                     this.listenTo(this.model.occupant, 'change:affiliation', this.debouncedRender);
+                    this.listenTo(this.model.occupant, 'change:hats', this.debouncedRender);
+                    this.listenTo(this.model.occupant, 'change:role', this.debouncedRender);
                     this.debouncedRender();
                 }
 
@@ -228,25 +230,36 @@ converse.plugins.add('converse-message-view', {
             async renderChatMessage () {
                 await api.waitUntil('emojisInitialized');
                 const time = dayjs(this.model.get('time'));
-                const role = this.model.vcard ? this.model.vcard.get('role') : null;
-                const roles = role ? role.split(',') : [];
                 const is_retracted = this.model.get('retracted') || this.model.get('moderated') === 'retracted';
                 const may_be_moderated = this.model.get('type') === 'groupchat' && await this.model.mayBeModerated();
                 const retractable= !is_retracted && (this.model.mayBeRetracted() || may_be_moderated);
+                const is_groupchat_message = this.model.get('type') === 'groupchat';
+
+                let hats = [];
+                if (is_groupchat_message) {
+                    if (api.settings.get('muc_hats_from_vcard')) {
+                        const role = this.model.vcard ? this.model.vcard.get('role') : null;
+                        hats = role ? role.split(',') : [];
+                    } else {
+                        const o = this.model.occupant;
+                        hats = o && o.get('hats').map(h => h.title).filter(h => h) || [];
+                    }
+                }
+
                 const msg = u.stringToElement(tpl_message(
                     Object.assign(
                         this.model.toJSON(), {
                          __,
+                        hats,
+                        is_groupchat_message,
                         is_retracted,
                         retractable,
                         'extra_classes': this.getExtraMessageClasses(),
-                        'is_groupchat_message': this.model.get('type') === 'groupchat',
                         'is_me_message': this.model.isMeCommand(),
                         'label_show': __('Show more'),
                         'occupant': this.model.occupant,
                         'pretty_time': time.format(api.settings.get('time_format')),
                         'retraction_text': is_retracted ? this.getRetractionText() : null,
-                        'roles': roles,
                         'time': time.toISOString(),
                         'username': this.model.getDisplayName()
                     })

+ 2 - 0
src/headless/converse-muc.js

@@ -33,6 +33,7 @@ Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + "#owner");
 Strophe.addNamespace('MUC_REGISTER', "jabber:iq:register");
 Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
 Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
+Strophe.addNamespace('MUC_HATS', "xmpp:prosody.im/protocol/hats:1");
 
 converse.MUC_NICK_CHANGED_CODE = "303";
 
@@ -2423,6 +2424,7 @@ converse.plugins.add('converse-muc', {
         _converse.ChatRoomOccupant = Model.extend({
 
             defaults: {
+                'hats': [],
                 'show': 'offline',
                 'states': []
             },

+ 6 - 0
src/headless/utils/stanza.js

@@ -367,6 +367,7 @@ const stanza_utils = {
             'nick': Strophe.getResourceFromJid(from),
             'type': type,
             'states': [],
+            'hats': [],
             'show': type !== 'unavailable' ? 'online' : 'offline'
         };
         Array.from(stanza.children).forEach(child => {
@@ -387,6 +388,11 @@ const stanza_utils = {
                 });
             } else if (child.matches('x') && child.getAttribute('xmlns') === Strophe.NS.VCARDUPDATE) {
                 data.image_hash = child.querySelector('photo')?.textContent;
+            } else if (child.matches('hats') && child.getAttribute('xmlns') === Strophe.NS.MUC_HATS) {
+                data['hats'] = Array.from(child.children).map(c => c.matches('hat') && {
+                    'title': c.getAttribute('title'),
+                    'id': c.getAttribute('id')
+                });
             }
         });
         return data;

+ 1 - 1
src/templates/message.html

@@ -8,7 +8,7 @@
             {[ if (o.is_me_message) { ]}<time timestamp="{{{o.isodate}}}" class="chat-msg__time">{{{o.pretty_time}}}</time>{[ } ]}
             <span class="chat-msg__author">{[ if (o.is_me_message) { ]}**{[ }; ]}{{{o.username}}}</span>
             {[ if (!o.is_me_message) { ]}
-                {[o.roles.forEach(function (role) { ]} <span class="badge badge-secondary">{{{role}}}</span> {[ }); ]}
+                {[o.hats.forEach(function (hat) { ]} <span class="badge badge-secondary">{{{hat}}}</span> {[ }); ]}
                 <time timestamp="{{{o.isodate}}}" class="chat-msg__time">{{{o.pretty_time}}}</time>
             {[ } ]}
             {[ if (o.is_encrypted) { ]}<span class="fa fa-lock"></span>{[ } ]}

+ 1 - 0
tests/runner.js

@@ -64,6 +64,7 @@ var specs = [
     "spec/notification",
     "spec/login",
     "spec/register",
+    "spec/hats",
     "spec/http-file-upload",
     "spec/emojis",
     "spec/xss"