MUC: create role/affiliation change message objects

in @converse/headless instead of injecting HTML via the view
This commit is contained in:
JC Brand 2020-04-13 11:10:25 +02:00
parent dcd8c8d418
commit e91a38547a
4 changed files with 157 additions and 101 deletions

View File

@ -2804,8 +2804,8 @@
'role': 'visitor'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
let info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
expect(info_msgs.pop().textContent.trim()).toBe("annoyingGuy has been muted");
const info_msg = await u.waitUntil(() => view.el.querySelector('.chat-info__message'));
expect(info_msg.textContent.trim()).toBe("annoyingGuy has been muted");
presence = $pres({
'from': 'lounge@montague.lit/annoyingGuy',
@ -2818,8 +2818,10 @@
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
expect(info_msgs.pop().textContent.trim()).toBe("annoyingGuy has been given a voice");
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"annoyingGuy has been given a voice"
);
// Check that we don't see an info message concerning the role,
// if the affiliation has changed.
@ -2834,8 +2836,10 @@
'role': 'visitor'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
expect(info_msgs.pop().textContent.trim()).toBe("annoyingGuy is no longer a member of this groupchat");
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"annoyingGuy is no longer a member of this groupchat"
);
done();
}));
@ -3331,7 +3335,10 @@
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(view.el.querySelectorAll('.chat-info')[2].textContent.trim()).toBe("annoyingGuy is now an owner of this groupchat");
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"annoyingGuy is now an owner of this groupchat"
);
done();
}));
@ -3547,7 +3554,6 @@
});
spyOn(view.model, 'setRole').and.callThrough();
spyOn(view, 'showErrorMessage').and.callThrough();
spyOn(view, 'showChatEvent').and.callThrough();
spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
// New user enters the groupchat
@ -3628,8 +3634,11 @@
'role': 'moderator'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
let info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
expect(info_msgs.pop().textContent.trim()).toBe("trustworthyguy is now a moderator");
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"trustworthyguy is now a moderator"
);
// Call now with the correct amount of arguments.
// XXX: Calling onFormSubmitted directly, trying
// again via triggering Event doesn't work for some weird
@ -3638,7 +3647,6 @@
view.onFormSubmitted(new Event('submit'));
expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.showChatEvent.calls.count()).toBe(1);
expect(view.model.setRole).toHaveBeenCalled();
expect(sent_IQ.toLocaleString()).toBe(
`<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@ -3669,8 +3677,10 @@
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
expect(info_msgs.pop().textContent.trim()).toBe("trustworthyguy is no longer a moderator");
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"trustworthyguy is no longer a moderator"
);
done();
}));
@ -3690,7 +3700,6 @@
});
spyOn(view.model, 'setRole').and.callThrough();
spyOn(view, 'showErrorMessage').and.callThrough();
spyOn(view, 'showChatEvent').and.callThrough();
spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
// New user enters the groupchat
@ -3770,8 +3779,10 @@
'role': 'visitor'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
let info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
expect(info_msgs.pop().textContent.trim()).toBe("annoyingGuy has been muted");
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"annoyingGuy has been muted"
);
// Call now with the correct of arguments.
// XXX: Calling onFormSubmitted directly, trying
@ -3781,7 +3792,6 @@
view.onFormSubmitted(new Event('submit'));
expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.showChatEvent.calls.count()).toBe(1);
expect(view.model.setRole).toHaveBeenCalled();
expect(sent_IQ.toLocaleString()).toBe(
`<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@ -3813,8 +3823,10 @@
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
info_msgs = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
expect(info_msgs.pop().textContent.trim()).toBe("annoyingGuy has been given a voice");
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"annoyingGuy has been given a voice"
);
done();
}));
@ -5231,9 +5243,10 @@
expect(bottom_panel.textContent.trim()).toBe("You're not allowed to send messages in this room");
// Check now that things get restored when the user is given a voice
let info_msgs = sizzle('.chat-info', view.el);
expect(info_msgs.length).toBe(1);
expect(info_msgs[0].textContent.trim()).toBe("troll is no longer an owner of this groupchat");
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"troll is no longer an owner of this groupchat"
);
stanza = u.toStanza(`
<presence
@ -5247,16 +5260,18 @@
</x>
</presence>`);
_converse.connection._dataRecv(test_utils.createRequest(stanza));
info_msgs = sizzle('.chat-info', view.el);
bottom_panel = view.el.querySelector('.muc-bottom-panel');
expect(bottom_panel).toBe(null);
textarea = view.el.querySelector('.chat-textarea');
expect(textarea === null).toBe(false);
expect(info_msgs.length).toBe(2);
expect(info_msgs[1].textContent.trim()).toBe("troll has been given a voice");
// Check now that things get restored when the user is given a voice
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"troll has been given a voice"
);
expect(view.el.querySelectorAll('.chat-info__message').length).toBe(2);
done();
}));
});

View File

@ -12,8 +12,8 @@
async function sendAndThenRetractMessage (_converse, view) {
view.model.sendMessage('hello world');
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
const msg_obj = view.model.messages.at(0);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 1);
const msg_obj = view.model.messages.last();
const reflection_stanza = u.toStanza(`
<message xmlns="jabber:client"
from="${msg_obj.get('from')}"
@ -625,7 +625,7 @@
const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
const msg_obj = view.model.messages.at(0);
const msg_obj = view.model.messages.last();
expect(Strophe.serialize(retraction_stanza)).toBe(
`<message id="${retraction_stanza.getAttribute('id')}" to="${muc_jid}" type="groupchat" xmlns="jabber:client">`+
`<store xmlns="urn:xmpp:hints"/>`+
@ -634,7 +634,7 @@
`</apply-to>`+
`</message>`);
const message = view.model.messages.at(0);
const message = view.model.messages.last();
expect(message.get('retracted')).toBeTruthy();
expect(message.get('is_ephemeral')).toBe(false);
expect(message.get('editable')).toBeFalsy();
@ -654,10 +654,10 @@
_converse.connection._dataRecv(test_utils.createRequest(reflection));
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
expect(view.model.messages.at(0).get('editable')).toBe(false);
expect(view.model.messages.length).toBe(2);
expect(view.model.messages.last().get('retracted')).toBeTruthy();
expect(view.model.messages.last().get('is_ephemeral')).toBe(false);
expect(view.model.messages.last().get('editable')).toBe(false);
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
expect(el.textContent).toBe('romeo has removed this message');
@ -676,15 +676,19 @@
const occupant = view.model.getOwnOccupant();
expect(occupant.get('role')).toBe('moderator');
occupant.save('role', 'member');
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"romeo is no longer a moderator"
);
const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
expect(view.model.messages.length).toBe(2);
expect(view.model.messages.last().get('retracted')).toBeTruthy();
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
expect(el.textContent.trim()).toBe('romeo has removed this message');
const message = view.model.messages.at(0);
const message = view.model.messages.last();
const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
// The server responds with an error message
const error = u.toStanza(`
@ -702,10 +706,10 @@
_converse.connection._dataRecv(test_utils.createRequest(error));
await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length === 1);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
expect(view.model.messages.at(0).get('editable')).toBeTruthy();
expect(view.model.messages.length).toBe(2);
expect(view.model.messages.last().get('retracted')).toBeFalsy();
expect(view.model.messages.last().get('is_ephemeral')).toBeFalsy();
expect(view.model.messages.last().get('editable')).toBeTruthy();
expect(view.el.querySelectorAll('.chat-error').length).toBe(1);
const errmsg = view.el.querySelector('.chat-error');
@ -713,7 +717,7 @@
done();
}));
it("can be retracted by its author, causing an timeout error in response",
it("can be retracted by its author, causing a timeout error in response",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
@ -727,21 +731,25 @@
const occupant = view.model.getOwnOccupant();
expect(occupant.get('role')).toBe('moderator');
occupant.save('role', 'member');
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"romeo is no longer a moderator"
);
await sendAndThenRetractMessage(_converse, view);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
expect(view.model.messages.length).toBe(2);
expect(view.model.messages.last().get('retracted')).toBeTruthy();
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
expect(el.textContent.trim()).toBe('romeo has removed this message');
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
expect(view.model.messages.at(0).get('editable')).toBeTruthy();
expect(view.model.messages.length).toBe(2);
expect(view.model.messages.last().get('retracted')).toBeFalsy();
expect(view.model.messages.last().get('is_ephemeral')).toBeFalsy();
expect(view.model.messages.last().get('editable')).toBeTruthy();
const error_messages = view.el.querySelectorAll('.chat-error');
expect(error_messages.length).toBe(2);

View File

@ -1123,60 +1123,12 @@ converse.plugins.add('converse-muc-views', {
if (occupant.get('jid') === _converse.bare_jid) {
this.renderHeading();
}
this.informOfOccupantsAffiliationChange(occupant);
},
informOfOccupantsAffiliationChange (occupant) {
const previous_affiliation = occupant._previousAttributes.affiliation;
const current_affiliation = occupant.get('affiliation');
if (previous_affiliation === 'admin') {
this.showChatEvent(__("%1$s is no longer an admin of this groupchat", occupant.get('nick')))
} else if (previous_affiliation === 'owner') {
this.showChatEvent(__("%1$s is no longer an owner of this groupchat", occupant.get('nick')))
} else if (previous_affiliation === 'outcast') {
this.showChatEvent(__("%1$s is no longer banned from this groupchat", occupant.get('nick')))
}
if (current_affiliation === 'none' && previous_affiliation === 'member') {
this.showChatEvent(__("%1$s is no longer a member of this groupchat", occupant.get('nick')))
} if (current_affiliation === 'member') {
this.showChatEvent(__("%1$s is now a member of this groupchat", occupant.get('nick')))
} else if (current_affiliation === 'admin' || current_affiliation == 'owner') {
// For example: AppleJack is now an (admin|owner) of this groupchat
this.showChatEvent(__('%1$s is now an %2$s of this groupchat', occupant.get('nick'), current_affiliation))
}
},
onOccupantRoleChanged (occupant, changed) {
onOccupantRoleChanged (occupant) {
if (occupant.get('jid') === _converse.bare_jid) {
this.renderBottomPanel();
}
this.informOfOccupantsRoleChange(occupant, changed);
},
informOfOccupantsRoleChange (occupant, changed) {
if (changed === "none" || occupant.changed.affiliation) {
// We don't inform of role changes if they accompany affiliation changes.
return;
}
const previous_role = occupant._previousAttributes.role;
if (previous_role === 'moderator') {
this.showChatEvent(__("%1$s is no longer a moderator", occupant.get('nick')))
}
if (previous_role === 'visitor') {
this.showChatEvent(__("%1$s has been given a voice", occupant.get('nick')))
}
if (occupant.get('role') === 'visitor') {
this.showChatEvent(__("%1$s has been muted", occupant.get('nick')))
}
if (occupant.get('role') === 'moderator') {
if (!['owner', 'admin'].includes(occupant.get('affiliation'))) {
// We only show this message if the user isn't already
// an admin or owner, otherwise this isn't new
// information.
this.showChatEvent(__("%1$s is now a moderator", occupant.get('nick')))
}
}
},
/**

View File

@ -383,6 +383,8 @@ converse.plugins.add('converse-muc', {
this.listenTo(this.occupants, 'add', this.onOccupantAdded);
this.listenTo(this.occupants, 'remove', this.onOccupantRemoved);
this.listenTo(this.occupants, 'change:show', this.onOccupantShowChanged);
this.listenTo(this.occupants, 'change:affiliation', this.createAffiliationChangeMessage);
this.listenTo(this.occupants, 'change:role', this.createRoleChangeMessage);
const restored = await this.restoreFromCache()
if (!restored) {
@ -2109,6 +2111,86 @@ converse.plugins.add('converse-muc', {
}
},
createAffiliationChangeMessage (occupant) {
const previous_affiliation = occupant._previousAttributes.affiliation;
const current_affiliation = occupant.get('affiliation');
if (previous_affiliation === 'admin') {
this.createMessage({
'type': 'info',
'message': __("%1$s is no longer an admin of this groupchat", occupant.get('nick'))
});
} else if (previous_affiliation === 'owner') {
this.createMessage({
'type': 'info',
'message': __("%1$s is no longer an owner of this groupchat", occupant.get('nick'))
});
} else if (previous_affiliation === 'outcast') {
this.createMessage({
'type': 'info',
'message': __("%1$s is no longer banned from this groupchat", occupant.get('nick'))
});
}
if (current_affiliation === 'none' && previous_affiliation === 'member') {
this.createMessage({
'type': 'info',
'message': __("%1$s is no longer a member of this groupchat", occupant.get('nick'))
});
}
if (current_affiliation === 'member') {
this.createMessage({
'type': 'info',
'message': __("%1$s is now a member of this groupchat", occupant.get('nick'))
});
} else if (current_affiliation === 'admin' || current_affiliation == 'owner') {
// For example: AppleJack is now an (admin|owner) of this groupchat
this.createMessage({
'type': 'info',
'message': __(
'%1$s is now an %2$s of this groupchat',
occupant.get('nick'),
current_affiliation
)
});
}
},
createRoleChangeMessage (occupant, changed) {
if (changed === "none" || occupant.changed.affiliation) {
// We don't inform of role changes if they accompany affiliation changes.
return;
}
const previous_role = occupant._previousAttributes.role;
if (previous_role === 'moderator') {
this.createMessage({
'type': 'info',
'message': __("%1$s is no longer a moderator", occupant.get('nick'))
});
}
if (previous_role === 'visitor') {
this.createMessage({
'type': 'info',
'message': __("%1$s has been given a voice", occupant.get('nick'))
});
}
if (occupant.get('role') === 'visitor') {
this.createMessage({
'type': 'info',
'message': __("%1$s has been muted", occupant.get('nick'))
});
}
if (occupant.get('role') === 'moderator') {
if (!['owner', 'admin'].includes(occupant.get('affiliation'))) {
// Oly show this message if the user isn't already
// an admin or owner, otherwise this isn't new information.
this.createMessage({
'type': 'info',
'message': __("%1$s is now a moderator", occupant.get('nick'))
});
}
}
},
/**
* Create info messages based on a received presence or message stanza
@ -2122,10 +2204,9 @@ converse.plugins.add('converse-muc', {
if (!x) {
return;
}
const codes = sizzle('status', x).map(s => s.getAttribute('code'));
codes.forEach(code => {
sizzle('status', x).map(s => s.getAttribute('code')).forEach(code => {
const data = {
'type': 'info'
'type': 'info',
};
if (code === '110' || (code === '100' && !is_self)) {
return;