muc: Render role change messages as ephemeral notifications

This commit is contained in:
JC Brand 2020-04-23 13:36:16 +02:00
parent c6f8ef0c46
commit 958379682a
5 changed files with 82 additions and 99 deletions

View File

@ -235,10 +235,6 @@
font-style: italic;
line-height: var(--line-height-small);
padding: 0 1em 0.3em;
&:before {
content: " ";
}
}
video {

View File

@ -918,7 +918,7 @@ describe("Groupchats", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"some1 and newgirl have entered the groupchat\n newguy has left the groupchat");
"some1 and newgirl have entered the groupchat\nnewguy has left the groupchat");
// When the user immediately joins again, we collapse the
// multiple join/leave messages.
@ -949,7 +949,7 @@ describe("Groupchats", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"some1 and newgirl have entered the groupchat\n newguy has left the groupchat");
"some1 and newgirl have entered the groupchat\nnewguy has left the groupchat");
presence = $pres({
to: 'romeo@montague.lit/_converse.js-29092160',
@ -963,7 +963,7 @@ describe("Groupchats", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"some1, newgirl and nomorenicks have entered the groupchat\n newguy has left the groupchat");
"some1, newgirl and nomorenicks have entered the groupchat\nnewguy has left the groupchat");
presence = $pres({
to: 'romeo@montague.lit/_converse.js-290918392',
@ -977,7 +977,7 @@ describe("Groupchats", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"some1 and newgirl have entered the groupchat\n newguy and nomorenicks have left the groupchat");
"some1 and newgirl have entered the groupchat\nnewguy and nomorenicks have left the groupchat");
presence = $pres({
to: 'romeo@montague.lit/_converse.js-29092160',
@ -991,7 +991,7 @@ describe("Groupchats", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"some1, newgirl and nomorenicks have entered the groupchat\n newguy has left the groupchat");
"some1, newgirl and nomorenicks have entered the groupchat\nnewguy has left the groupchat");
// Test a member joining and leaving
presence = $pres({
@ -1031,7 +1031,7 @@ describe("Groupchats", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"some1, newgirl and nomorenicks have entered the groupchat\n newguy and insider have left the groupchat");
"some1, newgirl and nomorenicks have entered the groupchat\nnewguy and insider have left the groupchat");
expect(view.model.occupants.length).toBe(5);
expect(view.model.occupants.findWhere({'jid': 'insider@montague.lit'}).get('show')).toBe('offline');
@ -1051,7 +1051,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"some1 and nomorenicks have entered the groupchat\n newguy, insider and newgirl have left the groupchat");
"some1 and nomorenicks have entered the groupchat\nnewguy, insider and newgirl have left the groupchat");
expect(view.model.occupants.length).toBe(4);
done();
}));
@ -1102,7 +1102,7 @@ describe("Groupchats", function () {
</presence>`);
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo, fabio and jcbrand have entered the groupchat\n Dele Olajide has left the groupchat");
"romeo, fabio and jcbrand have entered the groupchat\nDele Olajide has left the groupchat");
presence = u.toStanza(
`<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/Dele Olajide">
@ -1134,7 +1134,7 @@ describe("Groupchats", function () {
</presence>`);
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo, fabio and others have entered the groupchat\n fuvuv has left the groupchat");
"romeo, fabio and others have entered the groupchat\nfuvuv has left the groupchat");
presence = u.toStanza(
`<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/fabio">
@ -1145,7 +1145,7 @@ describe("Groupchats", function () {
</presence>`);
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo, jcbrand and Dele Olajide have entered the groupchat\n fuvuv and fabio have left the groupchat");
"romeo, jcbrand and Dele Olajide have entered the groupchat\nfuvuv and fabio have left the groupchat");
presence = u.toStanza(
`<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/fabio">
@ -1157,7 +1157,7 @@ describe("Groupchats", function () {
</presence>`);
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo, jcbrand and others have entered the groupchat\n fuvuv has left the groupchat");
"romeo, jcbrand and others have entered the groupchat\nfuvuv has left the groupchat");
// XXX: hack so that we can test leave/enter of occupants
// who were already in the room when we joined.
@ -1172,7 +1172,7 @@ describe("Groupchats", function () {
</presence>`);
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo, jcbrand and Dele Olajide have entered the groupchat\n fuvuv and fabio have left the groupchat");
"romeo, jcbrand and Dele Olajide have entered the groupchat\nfuvuv and fabio have left the groupchat");
presence = u.toStanza(
`<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/Dele Olajide">
@ -1182,7 +1182,7 @@ describe("Groupchats", function () {
</presence>`);
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo and jcbrand have entered the groupchat\n fuvuv, fabio and Dele Olajide have left the groupchat");
"romeo and jcbrand have entered the groupchat\nfuvuv, fabio and Dele Olajide have left the groupchat");
presence = u.toStanza(
`<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/fabio">
@ -1193,7 +1193,7 @@ describe("Groupchats", function () {
</presence>`);
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo, jcbrand and fabio have entered the groupchat\n fuvuv and Dele Olajide have left the groupchat");
"romeo, jcbrand and fabio have entered the groupchat\nfuvuv and Dele Olajide have left the groupchat");
expect(1).toBe(1);
done();
@ -2802,8 +2802,8 @@ describe("Groupchats", function () {
'role': 'visitor'
});
_converse.connection._dataRecv(mock.createRequest(presence));
const info_msg = await u.waitUntil(() => view.el.querySelector('.chat-info__message'));
expect(info_msg.textContent.trim()).toBe("annoyingGuy has been muted");
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo has entered the groupchat\nannoyingGuy has been muted");
presence = $pres({
'from': 'lounge@montague.lit/annoyingGuy',
@ -2816,10 +2816,8 @@ describe("Groupchats", function () {
'role': 'participant'
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"annoyingGuy has been given a voice"
);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo has entered the groupchat\nannoyingGuy has been given a voice");
// Check that we don't see an info message concerning the role,
// if the affiliation has changed.
@ -3626,10 +3624,10 @@ describe("Groupchats", function () {
'role': 'moderator'
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"trustworthyguy is now a moderator"
);
// Check now that things get restored when the user is given a voice
await u.waitUntil(
() => view.el.querySelector('.chat-content__notifications').textContent.split('\n', 2).pop()?.trim() ===
"trustworthyguy is now a moderator");
// Call now with the correct amount of arguments.
// XXX: Calling onFormSubmitted directly, trying
@ -3669,10 +3667,7 @@ describe("Groupchats", function () {
'role': 'participant'
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"trustworthyguy is no longer a moderator"
);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.includes("trustworthyguy is no longer a moderator"));
done();
}));
@ -3769,10 +3764,7 @@ describe("Groupchats", function () {
'role': 'visitor'
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"annoyingGuy has been muted"
);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.includes("annoyingGuy has been muted"));
// Call now with the correct of arguments.
// XXX: Calling onFormSubmitted directly, trying
@ -3813,10 +3805,7 @@ describe("Groupchats", function () {
'role': 'participant'
});
_converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() =>
Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() ===
"annoyingGuy has been given a voice"
);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.includes("annoyingGuy has been given a voice"));
done();
}));
@ -5180,7 +5169,7 @@ describe("Groupchats", function () {
type: 'groupchat'
}).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await view.model.queueMessage(msg);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() == 'nomorenicks is typing\n newguy has stopped typing');
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() == 'nomorenicks is typing\nnewguy has stopped typing');
done();
}));
});
@ -5301,11 +5290,7 @@ describe("Groupchats", function () {
expect(textarea === null).toBe(false);
// 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);
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "troll has been given a voice");
done();
}));
});

View File

@ -648,7 +648,7 @@ describe("Message Retractions", function () {
_converse.connection._dataRecv(mock.createRequest(reflection));
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
expect(view.model.messages.length).toBe(2);
expect(view.model.messages.length).toBe(1);
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);
@ -670,14 +670,11 @@ describe("Message Retractions", function () {
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 u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.includes("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(2);
expect(view.model.messages.length).toBe(1);
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');
@ -700,14 +697,14 @@ describe("Message Retractions", function () {
_converse.connection._dataRecv(mock.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(3);
expect(view.model.messages.at(1).get('retracted')).toBeFalsy();
expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy();
expect(view.model.messages.at(1).get('editable')).toBeTruthy();
expect(view.model.messages.length).toBe(2);
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();
const err_msg = "Sorry, something went wrong while trying to retract your message."
expect(view.model.messages.at(2).get('message')).toBe(err_msg);
expect(view.model.messages.at(2).get('type')).toBe('error');
expect(view.model.messages.at(1).get('message')).toBe(err_msg);
expect(view.model.messages.at(1).get('type')).toBe('error');
expect(view.el.querySelectorAll('.chat-error').length).toBe(1);
const errmsg = view.el.querySelector('.chat-error');
@ -729,14 +726,11 @@ describe("Message Retractions", function () {
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 u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.includes("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(2);
expect(view.model.messages.length).toBe(1);
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');
@ -744,10 +738,10 @@ describe("Message Retractions", function () {
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(4);
expect(view.model.messages.at(1).get('retracted')).toBeFalsy();
expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy();
expect(view.model.messages.at(1).get('editable')).toBeTruthy();
expect(view.model.messages.length).toBe(3);
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();
const error_messages = view.el.querySelectorAll('.chat-error');
expect(error_messages.length).toBe(2);

View File

@ -725,7 +725,7 @@ converse.plugins.add('converse-muc-views', {
renderNotifications () {
const actors_per_state = this.model.notifications.toJSON();
const states = api.settings.get('muc_show_join_leave') ?
[...converse.CHAT_STATES, ...converse.MUC_TRAFFIC_STATES] :
[...converse.CHAT_STATES, ...converse.MUC_TRAFFIC_STATES, ...converse.MUC_ROLE_CHANGES] :
converse.CHAT_STATES;
const message = states.reduce((result, state) => {
@ -736,15 +736,23 @@ converse.plugins.add('converse-muc-views', {
const actors = existing_actors.map(a => this.model.getOccupant(a)?.getDisplayName() || a);
if (actors.length === 1) {
if (state === 'composing') {
return `${result} ${__('%1$s is typing', actors[0])}\n`;
return `${result}${__('%1$s is typing', actors[0])}\n`;
} else if (state === 'paused') {
return `${result} ${__('%1$s has stopped typing', actors[0])}\n`;
return `${result}${__('%1$s has stopped typing', actors[0])}\n`;
} else if (state === _converse.GONE) {
return `${result} ${__('%1$s has gone away', actors[0])}\n`;
return `${result}${__('%1$s has gone away', actors[0])}\n`;
} else if (state === 'entered') {
return `${result} ${__('%1$s has entered the groupchat', actors[0])}\n`;
return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`;
} else if (state === 'exited') {
return `${result} ${__('%1$s has left the groupchat', actors[0])}\n`;
return `${result}${__('%1$s has left the groupchat', actors[0])}\n`;
} else if (state === 'op') {
return `${result}${__("%1$s is now a moderator", actors[0])}\n`;
} else if (state === 'deop') {
return `${result}${__("%1$s is no longer a moderator", actors[0])}\n`;
} else if (state === 'voice') {
return `${result}${__("%1$s has been given a voice", actors[0])}\n`;
} else if (state === 'mute') {
return `${result}${__("%1$s has been muted", actors[0])}\n`;
}
} else if (actors.length > 1) {
let actors_str;
@ -756,15 +764,23 @@ converse.plugins.add('converse-muc-views', {
}
if (state === 'composing') {
return `${result} ${__('%1$s are typing', actors_str)}\n`;
return `${result}${__('%1$s are typing', actors_str)}\n`;
} else if (state === 'paused') {
return `${result} ${__('%1$s have stopped typing', actors_str)}\n`;
return `${result}${__('%1$s have stopped typing', actors_str)}\n`;
} else if (state === _converse.GONE) {
return `${result} ${__('%1$s have gone away', actors_str)}\n`;
return `${result}${__('%1$s have gone away', actors_str)}\n`;
} else if (state === 'entered') {
return `${result} ${__('%1$s have entered the groupchat', actors_str)}\n`;
return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`;
} else if (state === 'exited') {
return `${result} ${__('%1$s have left the groupchat', actors_str)}\n`;
return `${result}${__('%1$s have left the groupchat', actors_str)}\n`;
} else if (state === 'op') {
return `${result}${__("%1$s are now moderators", actors[0])}\n`;
} else if (state === 'deop') {
return `${result}${__("%1$s are no longer moderator", actors[0])}\n`;
} else if (state === 'voice') {
return `${result}${__("%1$s have been given voices", actors[0])}\n`;
} else if (state === 'mute') {
return `${result}${__("%1$s have been muted", actors[0])}\n`;
}
}
return result;

View File

@ -17,6 +17,7 @@ import st from "./utils/stanza";
import u from "./utils/form";
converse.MUC_TRAFFIC_STATES = ['entered', 'exited'];
converse.MUC_ROLE_CHANGES = ['op', 'deop', 'voice', 'mute'];
const MUC_ROLE_WEIGHTS = {
'moderator': 1,
@ -1938,7 +1939,12 @@ converse.plugins.add('converse-muc', {
};
const actors_per_chat_state = converse.CHAT_STATES.reduce(reducer, {});
const actors_per_traffic_state = converse.MUC_TRAFFIC_STATES.reduce(reducer, {});
this.notifications.set(Object.assign(actors_per_chat_state, actors_per_traffic_state));
const actors_per_role_change = converse.MUC_ROLE_CHANGES.reduce(reducer, {});
this.notifications.set(Object.assign(
actors_per_chat_state,
actors_per_traffic_state,
actors_per_role_change
));
window.setTimeout(() => this.removeNotification(actor, state), 10000);
},
@ -2109,31 +2115,17 @@ converse.plugins.add('converse-muc', {
}
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'))
});
this.updateNotifications(occupant.get('nick'), 'deop');
} else if (previous_role === 'visitor') {
this.updateNotifications(occupant.get('nick'), 'voice');
}
if (occupant.get('role') === 'visitor') {
this.createMessage({
'type': 'info',
'message': __("%1$s has been muted", occupant.get('nick'))
});
}
if (occupant.get('role') === 'moderator') {
this.updateNotifications(occupant.get('nick'), 'mute');
} else 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'))
});
this.updateNotifications(occupant.get('nick'), 'op');
}
}
},