Show join/leave messages in chat rooms. Updates #365
This commit is contained in:
parent
e140eb84c8
commit
0d48929bb3
@ -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]
|
||||
|
@ -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
|
||||
------------------------
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
@ -351,7 +359,7 @@
|
||||
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();
|
||||
@ -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.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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user