Show join/leave messages in chat rooms. Updates #365

This commit is contained in:
JC Brand 2017-02-24 10:54:54 +00:00
parent e140eb84c8
commit 0d48929bb3
5 changed files with 173 additions and 67 deletions

View File

@ -50,6 +50,9 @@
- Bugfix. `TypeError: this.sendConfiguration(...).then is not a function` when
an instant room is created. [jcbrand]
- Ensure consistent behavior from `show_controlbox_by_default` [jcbrand]
- #365 Show join/leave messages for chat rooms.
New configuration setting:
[muc_show_join_leave](https://conversejs.org/docs/html/configuration.html#muc-show-join-leave)
- #366 Show the chat room occupant's JID in the tooltip (if you're allowed to see it). [jcbrand]
- #610, #785 Add presence priority handling [w3host, jcbrand]
- #694 The `notification_option` wasn't being used consistently. [jcbrand]

View File

@ -791,6 +791,14 @@ automatically be "john". If now john@differentdomain.com tries to join the
room, his nickname will be "john-2", and if john@somethingelse.com joins, then
his nickname will be "john-3", and so forth.
muc_show_join_leave
-------------------
* Default; ``true``
Determines whether Converse.js will show info messages inside a chat room
whenever a user joins or leaves it.
notify_all_room_messages
------------------------

View File

@ -345,6 +345,76 @@
describe("A Chat Room", function () {
it("shows join/leave messages when users enter or exit a room", mock.initConverse(function (_converse) {
test_utils.openChatRoom(_converse, "coven", 'chat.shakespeare.lit', 'some1');
var view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
var $chat_content = view.$el.find('.chat-content');
/* We don't show join/leave messages for existing occupants. We
* know about them because we receive their presences before we
* receive our own.
*/
presence = $pres({
to: 'dummy@localhost/_converse.js-29092160',
from: 'coven@chat.shakespeare.lit/oldguy'
}).c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'oldguy@localhost/_converse.js-290929789',
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect($chat_content.find('div.chat-info').length).toBe(0);
/* <presence to="dummy@localhost/_converse.js-29092160"
* from="coven@chat.shakespeare.lit/some1">
* <x xmlns="http://jabber.org/protocol/muc#user">
* <item affiliation="owner" jid="dummy@localhost/_converse.js-29092160" role="moderator"/>
* <status code="110"/>
* </x>
* </presence></body>
*/
var presence = $pres({
to: 'dummy@localhost/_converse.js-29092160',
from: 'coven@chat.shakespeare.lit/some1'
}).c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'owner',
'jid': 'dummy@localhost/_converse.js-29092160',
'role': 'moderator'
}).up()
.c('status', {code: '110'});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect($chat_content.find('div.chat-info:first').html()).toBe("some1 has joined the room");
presence = $pres({
to: 'dummy@localhost/_converse.js-29092160',
from: 'coven@chat.shakespeare.lit/newguy'
}).c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'newguy@localhost/_converse.js-290929789',
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect($chat_content.find('div.chat-info').length).toBe(2);
expect($chat_content.find('div.chat-info:last').html()).toBe("newguy has joined the room");
presence = $pres({
to: 'dummy@localhost/_converse.js-29092160',
type: 'unavailable',
from: 'coven@chat.shakespeare.lit/newguy'
}).c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'newguy@localhost/_converse.js-290929789',
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect($chat_content.find('div.chat-info').length).toBe(3);
expect($chat_content.find('div.chat-info:last').html()).toBe("newguy has left the room");
}));
it("shows its description in the chat heading", mock.initConverse(function (_converse) {
var sent_IQ, IQ_id;
var sendIQ = _converse.connection.sendIQ;
@ -1036,8 +1106,7 @@
_converse.connection._dataRecv(test_utils.createRequest(stanza));
var view = _converse.chatboxviews.get('jdev@conference.jabber.org');
var $chat_content = view.$el.find('.chat-content');
expect($chat_content.find('.chat-info').length).toBe(1);
expect($chat_content.find('.chat-info').text()).toBe('Topic set by ralphm to: '+text);
expect($chat_content.find('.chat-info:last').text()).toBe('Topic set by ralphm to: '+text);
}));
it("escapes the subject before rendering it, to avoid JS-injection attacks", mock.initConverse(function (_converse) {
@ -1047,8 +1116,7 @@
var view = _converse.chatboxviews.get('jdev@conference.jabber.org');
view.setChatRoomSubject('ralphm', subject);
var $chat_content = view.$el.find('.chat-content');
expect($chat_content.find('.chat-info').length).toBe(1);
expect($chat_content.find('.chat-info').text()).toBe('Topic set by ralphm to: '+subject);
expect($chat_content.find('.chat-info:last').text()).toBe('Topic set by ralphm to: '+subject);
}));
it("informs users if their nicknames has been changed.", mock.initConverse(function (_converse) {
@ -1114,8 +1182,9 @@
expect($occupants.children().length).toBe(1);
expect($occupants.children().first(0).text()).toBe("oldnick");
expect($chat_content.find('div.chat-info').length).toBe(1);
expect($chat_content.find('div.chat-info').html()).toBe(__(_converse.muc.new_nickname_messages["210"], "oldnick"));
expect($chat_content.find('div.chat-info').length).toBe(2);
expect($chat_content.find('div.chat-info:first').html()).toBe("oldnick has joined the room");
expect($chat_content.find('div.chat-info:last').html()).toBe(__(_converse.muc.new_nickname_messages["210"], "oldnick"));
presence = $pres().attrs({
from:'lounge@localhost/oldnick',
@ -1134,7 +1203,7 @@
.c('status').attrs({code:'110'}).nodeTree;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect($chat_content.find('div.chat-info').length).toBe(2);
expect($chat_content.find('div.chat-info').length).toBe(3);
expect($chat_content.find('div.chat-info').last().html()).toBe(__(_converse.muc.new_nickname_messages["303"], "newnick"));
$occupants = view.$('.occupant-list');
@ -1154,8 +1223,9 @@
.c('status').attrs({code:'110'}).nodeTree;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect($chat_content.find('div.chat-info').length).toBe(2);
expect($chat_content.find('div.chat-info').last().html()).toBe(__(_converse.muc.new_nickname_messages["303"], "newnick"));
expect($chat_content.find('div.chat-info').length).toBe(4);
expect($chat_content.find('div.chat-info').get(2).textContent).toBe(__(_converse.muc.new_nickname_messages["303"], "newnick"));
expect($chat_content.find('div.chat-info').last().html()).toBe("newnick has joined the room");
$occupants = view.$('.occupant-list');
expect($occupants.children().length).toBe(1);
expect($occupants.children().first(0).text()).toBe("newnick");
@ -1400,7 +1470,7 @@
describe("Each chat room can take special commands", function () {
it("to set the room subject", mock.initConverse(function (_converse) {
it("to set the room topic", mock.initConverse(function (_converse) {
var sent_stanza;
test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy');
var view = _converse.chatboxviews.get('lounge@localhost');

View File

@ -50,10 +50,10 @@
it("can be used to replay conversations", mock.initConverse(function (_converse) {
/*
test_utils.openChatRoom("discuss", 'conference.conversejs.org', 'jc');
test_utils.openChatRoom("dummy", 'rooms.localhost', 'jc');
test_utils.openChatRoom("prosody", 'conference.prosody.im', 'jc');
test_utils.openChatRoom(_converse, "dummy", 'rooms.localhost', 'jc');
test_utils.openChatRoom(_converse, "prosody", 'conference.prosody.im', 'jc');
*/
test_utils.openChatRoom(_converse, "discuss", 'conference.conversejs.org', 'ee');
spyOn(_converse, 'areDesktopNotificationsEnabled').andReturn(true);
_.each(transcripts, function (transcript) {
var text = transcript();

View File

@ -73,6 +73,13 @@
Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
var ROOMSTATUS = {
CONNECTED: 0,
CONNECTING: 1,
DISCONNECTED: 2,
ENTERED: 3
};
converse.plugins.add('converse-muc', {
/* Optional dependencies are other plugins which might be
* overridden or relied upon, if they exist, otherwise they're ignored.
@ -274,6 +281,7 @@
muc_history_max_stanzas: undefined,
muc_instant_rooms: true,
muc_nickname_from_jid: false,
muc_show_join_leave: true,
visible_toolbar_buttons: {
'toggle_occupants': true
},
@ -287,7 +295,7 @@
return _converse.chatboxviews.showChat(
_.extend({
'affiliation': null,
'connection_status': Strophe.Status.DISCONNECTED,
'connection_status': ROOMSTATUS.DISCONNECTED,
'description': '',
'features_fetched': false,
'hidden': false,
@ -349,9 +357,9 @@
// Which for some reason doesn't work.
// So working around that fact here:
this.$el.find('.chat-content').on('scroll', this.markScrolled.bind(this));
this.registerHandlers();
if (this.model.get('connection_status') !== Strophe.Status.CONNECTED) {
if (this.model.get('connection_status') !== ROOMSTATUS.ENTERED) {
this.getRoomFeatures().always(function () {
that.join();
that.fetchMessages();
@ -443,7 +451,7 @@
},
afterConnected: function () {
if (this.model.get('connection_status') === Strophe.Status.CONNECTED) {
if (this.model.get('connection_status') === ROOMSTATUS.ENTERED) {
this.setChatState(_converse.ACTIVE);
this.scrollDown();
this.focus();
@ -665,7 +673,7 @@
members,
_.partial(this.sendAffiliationIQ, this.model.get('jid'), affiliation)
);
return $.when.apply($, promises);
return $.when.apply($, promises);
},
setAffiliations: function (members, onSuccess, onError) {
@ -807,7 +815,7 @@
* as taken from the 'chat_state' attribute of the chat box.
* See XEP-0085 Chat State Notifications.
*/
if (this.model.get('connection_status') !== Strophe.Status.CONNECTED) {
if (this.model.get('connection_status') !== ROOMSTATUS.ENTERED) {
return;
}
var chat_state = this.model.get('chat_state');
@ -1102,7 +1110,7 @@
if (!nick) {
return this.checkForReservedNick();
}
if (this.model.get('connection_status') === Strophe.Status.CONNECTED) {
if (this.model.get('connection_status') === ROOMSTATUS.ENTERED) {
// We have restored a chat room from session storage,
// so we don't send out a presence stanza again.
return this;
@ -1115,13 +1123,13 @@
if (password) {
stanza.cnode(Strophe.xmlElement("password", [], password));
}
this.model.save('connection_status', Strophe.Status.CONNECTING);
this.model.save('connection_status', ROOMSTATUS.CONNECTING);
_converse.connection.send(stanza);
return this;
},
cleanup: function () {
this.model.save('connection_status', Strophe.Status.DISCONNECTED);
this.model.save('connection_status', ROOMSTATUS.DISCONNECTED);
this.removeHandlers();
_converse.ChatBoxView.prototype.close.apply(this, arguments);
},
@ -1137,7 +1145,7 @@
this.occupantsview.model.reset();
this.occupantsview.model.browserStorage._clear();
if (!_converse.connection.connected ||
this.model.get('connection_status') === Strophe.Status.DISCONNECTED) {
this.model.get('connection_status') === ROOMSTATUS.DISCONNECTED) {
// Don't send out a stanza if we're not connected.
this.cleanup();
return;
@ -1568,26 +1576,23 @@
* current user.
* (XMLElement) stanza: The original stanza received.
*/
var code = stat.getAttribute('code'),
from_nick;
if (is_self && code === "210") {
from_nick = Strophe.unescapeNode(Strophe.getResourceFromJid(stanza.getAttribute('from')));
return __(_converse.muc.new_nickname_messages[code], from_nick);
} else if (is_self && code === "303") {
return __(
_converse.muc.new_nickname_messages[code],
stanza.querySelector('x item').getAttribute('nick')
);
} else if (!is_self && (code in _converse.muc.action_info_messages)) {
from_nick = Strophe.unescapeNode(Strophe.getResourceFromJid(stanza.getAttribute('from')));
return __(_converse.muc.action_info_messages[code], from_nick);
} else if (code in _converse.muc.info_messages) {
var code = stat.getAttribute('code'), nick;
if (code === '110') { return; }
if (code in _converse.muc.info_messages) {
return _converse.muc.info_messages[code];
} else if (code !== '110') {
if (stat.textContent) {
// Sometimes the status contains human readable text and not a code.
return stat.textContent;
}
if (!is_self) {
if (code in _converse.muc.action_info_messages) {
nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
return __(_converse.muc.action_info_messages[code], nick);
}
} else if (code in _converse.muc.new_nickname_messages) {
if (is_self && code === "210") {
nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
} else if (is_self && code === "303") {
nick = stanza.querySelector('x item').getAttribute('nick');
}
return __(_converse.muc.new_nickname_messages[code], nick);
}
return;
},
@ -1622,9 +1627,11 @@
// 1. Get notification messages based on the <status> elements.
var statuses = x.querySelectorAll('status');
var mapper = _.partial(this.getMessageFromStatus, _, stanza, is_self);
var notification = {
'messages': _.reject(_.map(statuses, mapper), _.isUndefined),
};
var notification = {};
var messages = _.reject(_.map(statuses, mapper), _.isUndefined);
if (messages.length) {
notification.messages = messages;
}
// 2. Get disconnection messages based on the <status> elements
var codes = _.invokeMap(statuses, Element.prototype.getAttribute, 'code');
var disconnection_codes = _.intersection(codes, _.keys(_converse.muc.disconnect_messages));
@ -1666,7 +1673,7 @@
if (notification.reason) {
this.showDisconnectMessage(__(___('The reason given is: <em>"%1$s"</em>.'), notification.reason));
}
this.model.save('connection_status', Strophe.Status.DISCONNECTED);
this.model.save('connection_status', ROOMSTATUS.DISCONNECTED);
return;
}
_.each(notification.messages, function (message) {
@ -1680,6 +1687,25 @@
}
},
getJoinLeaveMessages: function (stanza) {
/* Parse the given stanza and return notification messages
* for join/leave events.
*/
// XXX: some mangling required to make the returned
// result look like the structure returned by
// parseXUserElement. Not nice...
var nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
if (stanza.getAttribute('type') === 'unavailable') {
var stat = stanza.querySelector('status');
if (!_.isNull(stat) && stat.textContent) {
return [{'messages': [__(nick+' has left the room. "'+stat.textContent+'"')]}];
} else {
return [{'messages': [__(nick+' has left the room')]}];
}
}
return [{'messages': [__(nick+' has joined the room')]}];
},
showStatusMessages: function (stanza) {
/* Check for status codes and communicate their purpose to the user.
* See: http://xmpp.org/registrar/mucstatus.html
@ -1688,12 +1714,17 @@
* (XMLElement) stanza: The message or presence stanza
* containing the status codes.
*/
var is_self = stanza.querySelectorAll("status[code='110']").length;
var elements = sizzle('x[xmlns="'+Strophe.NS.MUC_USER+'"]', stanza);
var notifications = _.map(
elements,
_.partial(this.parseXUserElement.bind(this), _, stanza, is_self)
);
var is_self = stanza.querySelectorAll("status[code='110']").length;
var iteratee = _.partial(this.parseXUserElement.bind(this), _, stanza, is_self);
var notifications = _.reject(_.map(elements, iteratee), _.isEmpty);
if (_.isEmpty(notifications) &&
_converse.muc_show_join_leave &&
stanza.nodeName === 'presence' &&
this.model.get('connection_status') === ROOMSTATUS.ENTERED
) {
notifications = this.getJoinLeaveMessages(stanza);
}
_.each(notifications, this.displayNotificationsforUser.bind(this));
return stanza;
},
@ -1767,11 +1798,10 @@
* (XMLElement) pres: The stanza
*/
if (pres.getAttribute('type') === 'error') {
this.model.save('connection_status', Strophe.Status.DISCONNECTED);
this.model.save('connection_status', ROOMSTATUS.DISCONNECTED);
this.showErrorMessage(pres);
return true;
}
var show_status_messages = true;
var is_self = pres.querySelector("status[code='110']");
var locked_room = pres.querySelector("status[code='201']");
if (is_self) {
@ -1784,15 +1814,14 @@
} else {
this.configureChatRoom();
if (!this.model.get('auto_configure')) {
// We don't show status messages if the
// configuration form is being shown.
show_status_messages = false;
return;
}
}
}
this.model.save('connection_status', ROOMSTATUS.ENTERED);
}
if (!locked_room && !this.model.get('features_fetched') &&
this.model.get('connection_status') !== Strophe.Status.CONNECTED) {
this.model.get('connection_status') !== ROOMSTATUS.CONNECTED) {
// The features for this room weren't fetched yet, perhaps
// because it's a new room without locking (in which
// case Prosody doesn't send a 201 status).
@ -1800,12 +1829,11 @@
// so a good time to fetch the features.
this.getRoomFeatures();
}
if (show_status_messages) {
this.hideSpinner().showStatusMessages(pres);
}
this.hideSpinner().showStatusMessages(pres);
this.occupantsview.updateOccupantsOnPresence(pres);
if (this.model.get('role') !== 'none') {
this.model.save('connection_status', Strophe.Status.CONNECTED);
if (this.model.get('role') !== 'none' &&
this.model.get('connection_status') === ROOMSTATUS.CONNECTING) {
this.model.save('connection_status', ROOMSTATUS.CONNECTED);
}
return true;
},
@ -2425,10 +2453,7 @@
'box_id': b64_sha1(room_jid),
'password': $x.attr('password')
});
if (!_.includes(
[Strophe.Status.CONNECTING, Strophe.Status.CONNECTED],
chatroom.get('connection_status'))
) {
if (chatroom.get('connection_status') === ROOMSTATUS.DISCONNECTED) {
_converse.chatboxviews.get(room_jid).join();
}
}
@ -2549,7 +2574,7 @@
*/
_converse.chatboxviews.each(function (view) {
if (view.model.get('type') === 'chatroom') {
view.model.save('connection_status', Strophe.Status.DISCONNECTED);
view.model.save('connection_status', ROOMSTATUS.DISCONNECTED);
view.join();
}
});
@ -2563,7 +2588,7 @@
*/
_converse.chatboxes.each(function (model) {
if (model.get('type') === 'chatroom') {
model.save('connection_status', Strophe.Status.DISCONNECTED);
model.save('connection_status', ROOMSTATUS.DISCONNECTED);
}
});
};