Add support for XEP-0317 MUC Hats

This commit is contained in:
JC Brand 2020-04-13 15:19:21 +02:00
parent e2a7045e22
commit df9612f937
8 changed files with 124 additions and 7 deletions

View File

@ -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
-----------------------------------

View File

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

85
spec/hats.js Normal file
View File

@ -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();
}));
})
}));

View File

@ -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()
})

View File

@ -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': []
},

View File

@ -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;

View File

@ -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>{[ } ]}

View File

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