Refactoring in MUC

What started as an attempt to fix a bug in parseXUserElement, turned into
another large refactoring of MUC code, and it's not clear how to break this up
into multiple atomic commits. So I'm just pushing it all.

At least there are two new tests added to the suite.
This commit is contained in:
JC Brand 2016-12-02 18:41:05 +01:00
parent 8bbd88ba86
commit 910e9bddcd
12 changed files with 231 additions and 69 deletions

View File

@ -135,7 +135,9 @@ require.config({
"chatbox_minimize": "src/templates/chatbox_minimize",
"chatroom": "src/templates/chatroom",
"chatroom_bookmark_form": "src/templates/chatroom_bookmark_form",
"chatroom_bookmark_toggle": "src/templates/chatroom_bookmark_toggle",
"chatroom_form": "src/templates/chatroom_form",
"chatroom_head": "src/templates/chatroom_head",
"chatroom_nickname_form": "src/templates/chatroom_nickname_form",
"chatroom_password_form": "src/templates/chatroom_password_form",
"chatroom_sidebar": "src/templates/chatroom_sidebar",
@ -147,6 +149,7 @@ require.config({
"contacts_tab": "src/templates/contacts_tab",
"controlbox": "src/templates/controlbox",
"controlbox_toggle": "src/templates/controlbox_toggle",
"dragresize": "src/templates/dragresize",
"field": "src/templates/field",
"form_captcha": "src/templates/form_captcha",
"form_checkbox": "src/templates/form_checkbox",

View File

@ -155,6 +155,36 @@
expect(view instanceof converse.ChatRoomView).toBe(true);
}));
it("can be configured if you're its owner", mock.initConverse(function (converse) {
converse_api.rooms.open('room@conference.example.org', {'nick': 'some1'});
var view = converse.chatboxviews.get('room@conference.example.org');
spyOn(view, 'showConfigureButtonIfRoomOwner').andCallThrough();
/* <presence to="dummy@localhost/converse.js-29092160"
* from="room@conference.example.org/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: 'room@conference.example.org/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(view.showConfigureButtonIfRoomOwner).toHaveBeenCalled();
expect(view.$('.configure-chatroom-button').is(':visible')).toBeTruthy();
expect(view.$('.toggle-chatbox-button').is(':visible')).toBeTruthy();
expect(view.$('.toggle-bookmark').is(':visible')).toBeTruthy();
}));
it("shows users currently present in the room", mock.initConverse(function (converse) {
test_utils.openAndEnterChatRoom(converse, 'lounge', 'localhost', 'dummy');
var name;
@ -575,6 +605,43 @@
expect($occupants.children().first(0).text()).toBe("newnick");
}));
it("indicates when a room is no longer anonymous", mock.initConverse(function (converse) {
converse_api.rooms.open('room@conference.example.org', {
'nick': 'some1',
'roomconfig': {
'changesubject': false,
'membersonly': true,
'persistentroom': true,
'publicroom': true,
'roomdesc': 'Welcome to this room',
'whois': 'anyone'
}
});
/* <message xmlns="jabber:client"
* type="groupchat"
* to="dummy@localhost/converse.js-27854181"
* from="room@conference.example.org">
* <x xmlns="http://jabber.org/protocol/muc#user">
* <status code="104"/>
* <status code="172"/>
* </x>
* </message>
*/
var message = $msg({
type:'groupchat',
to: 'dummy@localhost/converse.js-27854181',
from: 'room@conference.example.org'
}).c('x', {xmlns: Strophe.NS.MUC_USER})
.c('status', {code: '104'}).up()
.c('status', {code: '172'});
converse.connection._dataRecv(test_utils.createRequest(message));
var view = converse.chatboxviews.get('room@conference.example.org');
var $chat_body = view.$('.chatroom-body');
expect($chat_body.html().trim().indexOf(
'<div class="chat-info">This room is now no longer anonymous</div>'
)).not.toBe(-1);
}));
it("informs users if they have been kicked out of the chat room", mock.initConverse(function (converse) {
/* <presence
* from='harfleur@chat.shakespeare.lit/pistol'

View File

@ -20,6 +20,7 @@
"converse-api",
"converse-muc",
"tpl!chatroom_bookmark_form",
"tpl!chatroom_bookmark_toggle",
"tpl!bookmark",
"tpl!bookmarks_list"
],
@ -28,6 +29,7 @@
$, _, moment, strophe, utils,
converse, converse_api, muc,
tpl_chatroom_bookmark_form,
tpl_chatroom_bookmark_toggle,
tpl_bookmark,
tpl_bookmarks_list
) {
@ -40,6 +42,7 @@
// Add new HTML templates.
converse.templates.chatroom_bookmark_form = tpl_chatroom_bookmark_form;
converse.templates.chatroom_bookmark_toggle = tpl_chatroom_bookmark_toggle;
converse.templates.bookmark = tpl_bookmark;
converse.templates.bookmarks_list = tpl_bookmarks_list;
@ -69,16 +72,24 @@
this.setBookmarkState();
},
render: function (options) {
this.__super__.render.apply(this, arguments);
generateHeadingHTML: function () {
var html = this.__super__.generateHeadingHTML.apply(this, arguments);
if (converse.allow_bookmarks) {
var label_bookmark = _('Bookmark this room');
var button = '<a class="chatbox-btn toggle-bookmark icon-pushpin '+
(this.model.get('bookmarked') ? 'button-on"' : '"') +
'title="'+label_bookmark+'"></a>';
this.$el.find('.chat-head-chatroom .icon-wrench').before(button);
var div = document.createElement('div');
div.innerHTML = html;
var bookmark_button = converse.templates.chatroom_bookmark_toggle(
_.extend(
this.model.toJSON(),
{
info_toggle_bookmark: __('Bookmark this room'),
bookmarked: this.model.get('bookmarked')
}
));
var close_button = div.querySelector('.close-chatbox-button');
close_button.insertAdjacentHTML('afterend', bookmark_button);
return div.innerHTML;
}
return this;
return html;
},
checkForReservedNick: function () {

View File

@ -10,14 +10,16 @@
define("converse-dragresize", [
"converse-core",
"converse-api",
"tpl!dragresize",
"converse-chatview",
"converse-muc", // XXX: would like to remove this
"converse-controlbox"
], factory);
}(this, function (converse, converse_api) {
}(this, function (converse, converse_api, tpl_dragresize) {
"use strict";
var $ = converse_api.env.jQuery,
_ = converse_api.env._;
converse.templates.dragresize = tpl_dragresize;
converse_api.plugins.add('converse-dragresize', {
@ -260,13 +262,23 @@
render: function () {
var result = this.__super__.render.apply(this, arguments);
this.renderDragResizeHandles();
this.setWidth();
return result;
},
renderDragResizeHandles: function () {
var flyout = this.el.querySelector('.box-flyout');
var div = document.createElement('div');
div.innerHTML = converse.templates.dragresize();
flyout.insertBefore(
div.firstChild,
flyout.firstChild
);
}
}
},
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.

View File

@ -98,7 +98,6 @@
title: this.model.get('fullname'),
unread_msgs: __('You have unread messages'),
info_close: __('Close this box'),
info_minimize: __('Minimize this box'),
label_personal_message: ''
}
)

View File

@ -181,6 +181,18 @@
this.hide();
}
return result;
},
generateHeadingHTML: function () {
var html = this.__super__.generateHeadingHTML.apply(this, arguments);
var div = document.createElement('div');
div.innerHTML = html;
var el = converse.templates.chatbox_minimize(
{info_minimize: __('Minimize this chat box')}
);
var button = div.querySelector('.close-chatbox-button');
button.insertAdjacentHTML('afterend', el);
return div.innerHTML;
}
},
@ -497,7 +509,7 @@
// Inserts a "minimize" button in the chatview's header
var $el = view.$el.find('.toggle-chatbox-button');
var $new_el = converse.templates.chatbox_minimize(
_.extend({info_minimize: __('Minimize this chat box')})
{info_minimize: __('Minimize this chat box')}
);
if ($el.length) {
$el.replaceWith($new_el);
@ -506,7 +518,6 @@
}
};
converse.on('chatBoxOpened', renderMinimizeButton);
converse.on('chatRoomOpened', renderMinimizeButton);
converse.on('controlBoxOpened', function (evt, chatbox) {
// Wrapped in anon method because at scan time, chatboxviews

View File

@ -20,6 +20,7 @@
"tpl!chatroom_password_form",
"tpl!chatroom_sidebar",
"tpl!chatroom_toolbar",
"tpl!chatroom_head",
"tpl!chatrooms_tab",
"tpl!info",
"tpl!occupant",
@ -39,6 +40,7 @@
tpl_chatroom_password_form,
tpl_chatroom_sidebar,
tpl_chatroom_toolbar,
tpl_chatroom_head,
tpl_chatrooms_tab,
tpl_info,
tpl_occupant,
@ -53,6 +55,7 @@
converse.templates.chatroom_nickname_form = tpl_chatroom_nickname_form;
converse.templates.chatroom_password_form = tpl_chatroom_password_form;
converse.templates.chatroom_sidebar = tpl_chatroom_sidebar;
converse.templates.chatroom_head = tpl_chatroom_head;
converse.templates.chatrooms_tab = tpl_chatrooms_tab;
converse.templates.info = tpl_info;
converse.templates.occupant = tpl_occupant;
@ -109,7 +112,7 @@
104: __('Non-privacy-related room configuration has changed'),
170: __('Room logging is now enabled'),
171: __('Room logging is now disabled'),
172: __('This room is now non-anonymous'),
172: __('This room is now no longer anonymous'),
173: __('This room is now semi-anonymous'),
174: __('This room is now fully-anonymous'),
201: __('A new room has been created')
@ -293,6 +296,14 @@
},
});
converse.createChatRoom = function (settings) {
return converse.chatboxviews.showChat(
_.extend(settings, {
'type': 'chatroom',
'affiliation': undefined
})
);
};
converse.ChatRoomView = converse.ChatBoxView.extend({
/* Backbone View which renders a chat room, based upon the view
@ -320,6 +331,8 @@
this.model.on('show', this.show, this);
this.model.on('destroy', this.hide, this);
this.model.on('change:chat_state', this.sendChatState, this);
this.model.on('change:affiliation', this.renderHeading, this);
this.model.on('change:name', this.renderHeading, this);
this.occupantsview = new converse.ChatRoomOccupantsView({
model: new converse.ChatRoomOccupants({nick: this.model.get('nick')})
@ -358,16 +371,25 @@
render: function () {
this.$el.attr('id', this.model.get('box_id'))
.html(converse.templates.chatroom(
_.extend(this.model.toJSON(), {
info_close: __('Close and leave this room'),
info_configure: __('Configure this room'),
})));
.html(converse.templates.chatroom());
this.renderHeading();
this.renderChatArea();
utils.refreshWebkit();
return this;
},
generateHeadingHTML: function () {
return converse.templates.chatroom_head(
_.extend(this.model.toJSON(), {
info_close: __('Close and leave this room'),
info_configure: __('Configure this room'),
}));
},
renderHeading: function () {
this.el.querySelector('.chat-head-chatroom').innerHTML = this.generateHeadingHTML();
},
renderChatArea: function () {
if (!this.$('.chat-area').length) {
this.$('.chatroom-body').empty()
@ -1044,13 +1066,25 @@
this.$('.chatroom-body').append($('<p>'+msg+'</p>'));
},
getMessageFromStatus: function (stat, is_self, from_nick, item) {
var code = stat.getAttribute('code');
getMessageFromStatus: function (stat, stanza, is_self) {
/* Parameters:
* (XMLElement) stat: A <status> element.
* (Boolean) is_self: Whether the element refers to the
* 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], item.getAttribute('nick'));
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) {
return converse.muc.info_messages[code];
@ -1063,37 +1097,42 @@
return;
},
parseXUserElement: function (x, is_self, from_nick) {
showConfigureButtonIfRoomOwner: function (pres) {
/* Show the configure button if the user is the room owner.
*
* Parameters:
* (XMLElement) pres: A <presence> stanza.
*/
// XXX: For some inexplicable reason, the following line of
// code works in tests, but not with live data, even though
// the passed in stanza looks exactly the same to me:
// var item = pres.querySelector('x[xmlns="'+Strophe.NS.MUC_USER+'"] item');
// If we want to eventually get rid of jQuery altogether,
// then the Sizzle selector library might still be needed
// here.
var item = $(pres).find('x[xmlns="'+Strophe.NS.MUC_USER+'"] item').get(0);
if (_.isUndefined(item)) {
return;
}
var jid = item.getAttribute('jid');
var affiliation = item.getAttribute('affiliation');
if (Strophe.getBareJidFromJid(jid) === converse.bare_jid && affiliation) {
this.model.save({'affiliation': affiliation});
}
},
parseXUserElement: function (x, stanza, is_self) {
/* Parse the passed-in <x xmlns='http://jabber.org/protocol/muc#user'>
* element and construct a map containing relevant
* information.
*/
// By using querySelector, we assume here there is one
// <item> per <x xmlns='http://jabber.org/protocol/muc#user'>
// element. This appears to be a safe assumption, since
// each <x/> element pertains to a single user.
var item = x.querySelector('item');
// Show the configure button if user is the room owner.
var jid = item.getAttribute('jid');
var affiliation = item.getAttribute('affiliation');
if (Strophe.getBareJidFromJid(jid) === converse.bare_jid && affiliation === 'owner') {
this.$el.find('a.configure-chatroom-button').show();
}
// Extract notification messages, reasons and
// disconnection messages from the <x/> node.
// 1. Get notification messages based on the <status> elements.
var statuses = x.querySelectorAll('status');
var mapper = _.partial(this.getMessageFromStatus, _, is_self, from_nick, item);
var mapper = _.partial(this.getMessageFromStatus, _, stanza, is_self);
var notification = {
'messages': _.reject(_.map(statuses, mapper), _.isUndefined),
};
var reason = item.querySelector('reason');
if (reason) {
notification.reason = reason ? reason.textContent : undefined;
}
var actor = item.querySelector('actor');
if (actor) {
notification.actor = actor ? actor.getAttribute('nick') : undefined;
}
// 2. Get disconnection messages based on the <status> elements
var codes = _.map(statuses, function (stat) { return stat.getAttribute('code'); });
var disconnection_codes = _.intersection(codes, _.keys(converse.muc.disconnect_messages));
var disconnected = is_self && disconnection_codes.length > 0;
@ -1101,6 +1140,22 @@
notification.disconnected = true;
notification.disconnection_message = converse.muc.disconnect_messages[disconnection_codes[0]];
}
// 3. Find the reason and actor from the <item> element
var item = x.querySelector('item');
// By using querySelector above, we assume here there is
// one <item> per <x xmlns='http://jabber.org/protocol/muc#user'>
// element. This appears to be a safe assumption, since
// each <x/> element pertains to a single user.
if (!_.isNull(item)) {
var reason = item.querySelector('reason');
if (reason) {
notification.reason = reason ? reason.textContent : undefined;
}
var actor = item.querySelector('actor');
if (actor) {
notification.actor = actor ? actor.getAttribute('nick') : undefined;
}
}
return notification;
},
@ -1132,22 +1187,18 @@
}
},
showStatusMessages: function (presence, is_self) {
showStatusMessages: function (stanza, is_self) {
/* Check for status codes and communicate their purpose to the user.
* Allows user to configure chat room if they are the owner.
* See: http://xmpp.org/registrar/mucstatus.html
*/
var from_nick = Strophe.unescapeNode(Strophe.getResourceFromJid(presence.getAttribute('from')));
// XXX: Unfortunately presence.querySelectorAll('x[xmlns="'+Strophe.NS.MUC_USER+'"]') returns []
var elements = _.filter(presence.querySelectorAll('x'), function (x) {
return x.getAttribute('xmlns') === Strophe.NS.MUC_USER;
});
var elements = stanza.querySelectorAll('x[xmlns="'+Strophe.NS.MUC_USER+'"]');
var notifications = _.map(
elements,
_.partial(this.parseXUserElement.bind(this), _, is_self, from_nick)
_.partial(this.parseXUserElement.bind(this), _, stanza, is_self)
);
_.each(notifications, this.displayNotificationsforUser.bind(this));
return presence;
return stanza;
},
showErrorMessage: function (presence) {
@ -1221,6 +1272,7 @@
this.configureChatRoom();
} else {
this.hideSpinner().showStatusMessages(pres, is_self);
this.showConfigureButtonIfRoomOwner(pres);
}
}
}
@ -1702,7 +1754,7 @@
return;
}
}
converse.chatboxviews.showChat({
converse.createChatRoom({
'id': jid,
'jid': jid,
'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
@ -1750,7 +1802,7 @@
}
}
if (result === true) {
var chatroom = converse.chatboxviews.showChat({
var chatroom = converse.createChatRoom({
'id': room_jid,
'jid': room_jid,
'name': Strophe.unescapeNode(Strophe.getNodeFromJid(room_jid)),
@ -1837,16 +1889,15 @@
if (_.isUndefined(attrs.maximize)) {
attrs.maximize = false;
}
var fetcher = converse.chatboxviews.showChat.bind(converse.chatboxviews);
if (!attrs.nick && converse.muc_nickname_from_jid) {
attrs.nick = Strophe.getNodeFromJid(converse.bare_jid);
}
if (typeof jids === "undefined") {
throw new TypeError('rooms.open: You need to provide at least one JID');
} else if (typeof jids === "string") {
return _transform(jids, attrs, fetcher);
return _transform(jids, attrs, converse.createChatRoom);
}
return _.map(jids, _.partial(_transform, _, attrs, fetcher));
return _.map(jids, _.partial(_transform, _, attrs, converse.createChatRoom));
},
'get': function (jids, attrs, create) {
if (typeof attrs === "string") {

View File

@ -1,14 +1,4 @@
<div class="flyout box-flyout">
<div class="dragresize dragresize-top"></div>
<div class="dragresize dragresize-topleft"></div>
<div class="dragresize dragresize-left"></div>
<div class="chat-head chat-head-chatroom">
<a class="chatbox-btn close-chatbox-button icon-close" title="{{info_close}}"></a>
<a class="chatbox-btn configure-chatroom-button icon-wrench" title="{{info_configure}} "style="display:none"></a>
<div class="chat-title">
{{ _.escape(name) }}
<p class="chatroom-topic"><p/>
</div>
</div>
<div class="chat-head chat-head-chatroom"></div>
<div class="chat-body chatroom-body"><span class="spinner centered"/></div>
</div>

View File

@ -0,0 +1,4 @@
<a class="chatbox-btn toggle-bookmark icon-pushpin
{[ if (bookmarked) {]}
button-on
{[ } ]}" title="{{info_toggle_bookmark}}"></a>

View File

@ -0,0 +1,3 @@
<div class="dragresize dragresize-top"></div>
<div class="dragresize dragresize-topleft"></div>
<div class="dragresize dragresize-left"></div>

View File

@ -0,0 +1,8 @@
<a class="chatbox-btn close-chatbox-button icon-close" title="{{info_close}}"></a>
{[ if (affiliation == 'owner') { ]}
<a class="chatbox-btn configure-chatroom-button icon-wrench" title="{{info_configure}} "></a>
{[ } ]}
<div class="chat-title">
{{ _.escape(name) }}
<p class="chatroom-topic"><p/>
</div>

View File

@ -0,0 +1,3 @@
<div class="dragresize dragresize-top"></div>
<div class="dragresize dragresize-topleft"></div>
<div class="dragresize dragresize-left"></div>