converse-muc: Cache the room configuration on the Backbone.Model object

This commit is contained in:
JC Brand 2016-12-03 17:39:59 +01:00
parent 6379676cff
commit fbf2e56be4
2 changed files with 226 additions and 71 deletions

View File

@ -246,7 +246,7 @@
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();
spyOn(view, 'findAndSaveOwnAffiliation').andCallThrough();
/* <presence to="dummy@localhost/converse.js-29092160"
* from="room@conference.example.org/some1">
@ -267,7 +267,7 @@
}).up()
.c('status', {code: '110'});
converse.connection._dataRecv(test_utils.createRequest(presence));
expect(view.showConfigureButtonIfRoomOwner).toHaveBeenCalled();
expect(view.findAndSaveOwnAffiliation).toHaveBeenCalled();
expect(view.$('.configure-chatroom-button').is(':visible')).toBeTruthy();
expect(view.$('.toggle-chatbox-button').is(':visible')).toBeTruthy();
expect(view.$('.toggle-bookmark').is(':visible')).toBeTruthy();
@ -741,6 +741,7 @@
* <actor nick='Fluellen'/>
* <reason>Avaunt, you cullion!</reason>
* </item>
* <status code='110'/>
* <status code='307'/>
* </x>
* </presence>
@ -760,6 +761,7 @@
.c('actor').attrs({nick: 'Fluellen'}).up()
.c('reason').t('Avaunt, you cullion!').up()
.up()
.c('status').attrs({code:'110'}).up()
.c('status').attrs({code:'307'}).nodeTree;
var view = converse.chatboxviews.get('lounge@localhost');

View File

@ -454,9 +454,15 @@
},
directInvite: function (recipient, reason) {
/* Send a direct invitation as per XEP-0249
*
* Parameters:
* (String) recipient - JID of the person being invited
* (String) reason - Optional reason for the invitation
*/
var attrs = {
xmlns: 'jabber:x:conference',
jid: this.model.get('jid')
'xmlns': 'jabber:x:conference',
'jid': this.model.get('jid')
};
if (reason !== null) { attrs.reason = reason; }
if (this.model.get('password')) { attrs.password = this.model.get('password'); }
@ -473,10 +479,6 @@
});
},
onCommandError: function (stanza) {
this.showStatusNotification(__("Error: could not execute the command"), true);
},
handleChatStateMessage: function (message) {
/* Override the method on the ChatBoxView base class to
* ignore <gone/> notifications in groupchats.
@ -583,6 +585,10 @@
return this;
},
onCommandError: function () {
this.showStatusNotification(__("Error: could not execute the command"), true);
},
onMessageSubmitted: function (text) {
/* Gets called when the user presses enter to send off a
* message in a chat room.
@ -788,6 +794,21 @@
},
renderConfigurationForm: function (stanza) {
/* Renders a form given an IQ stanza containing the current
* room configuration.
*
* Returns a promise which resolves once the user has
* either submitted the form, or canceled it.
*
* Parameters:
* (XMLElement) stanza: The IQ stanza containing the room config.
*/
var that = this,
deferred = new $.Deferred(),
$body = this.$('.chatroom-body');
$body.children().addClass('hidden');
$body.append(converse.templates.chatroom_form());
var $form = this.$el.find('form.chatroom-form'),
$fieldset = $form.children('fieldset:first'),
$stanza = $(stanza),
@ -806,47 +827,86 @@
$fieldset = $form.children('fieldset:last');
$fieldset.append('<input type="submit" class="pure-button button-primary" value="'+__('Save')+'"/>');
$fieldset.append('<input type="button" class="pure-button button-cancel" value="'+__('Cancel')+'"/>');
$fieldset.find('input[type=button]').on('click', this.cancelConfiguration.bind(this));
$form.on('submit', this.saveConfiguration.bind(this));
$fieldset.find('input[type=button]').on('click', function (ev) {
ev.preventDefault();
that.cancelConfiguration();
deferred.reject(stanza);
});
$form.on('submit', function (ev) {
ev.preventDefault();
that.saveConfiguration(ev.target)
.done(deferred.resolve)
.fail(_.partial(deferred, stanza));
});
return deferred.promise();
},
sendConfiguration: function(config, onSuccess, onError) {
// Send an IQ stanza with the room configuration.
/* Send an IQ stanza with the room configuration.
*
* Parameters:
* (Array) config: The room configuration
* (Function) onSuccess: Callback upon succesful IQ response
* The first parameter passed in is IQ containing the
* room configuration.
* The second is the response IQ from the server.
* (Function) onError: Callback upon error IQ response
* The first parameter passed in is IQ containing the
* room configuration.
* The second is the response IQ from the server.
*/
var iq = $iq({to: this.model.get('jid'), type: "set"})
.c("query", {xmlns: Strophe.NS.MUC_OWNER})
.c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
_.each(config, function (node) { iq.cnode(node).up(); });
_.each(config || [], function (node) { iq.cnode(node).up(); });
onSuccess = _.isUndefined(onSuccess) ? _.noop : _.partial(onSuccess, iq.nodeTree);
onError = _.isUndefined(onError) ? _.noop : _.partial(onError, iq.nodeTree);
return converse.connection.sendIQ(iq, onSuccess, onError);
},
saveConfiguration: function (ev) {
ev.preventDefault();
saveConfiguration: function (form) {
/* Submit the room configuration form by sending an IQ
* stanza to the server.
*
* Returns a promise which resolves once the XMPP server
* has return a response IQ.
*
* Parameters:
* (HTMLElement) form: The configuration form DOM element.
*/
var deferred = new $.Deferred();
var that = this;
var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
count = $inputs.length,
var $inputs = $(form).find(':input:not([type=button]):not([type=submit])'),
configArray = [];
$inputs.each(function () {
configArray.push(utils.webForm2xForm(this));
if (!--count) {
that.sendConfiguration(
configArray,
that.onConfigSaved.bind(that),
that.onErrorConfigSaved.bind(that)
);
}
});
this.sendConfiguration(
configArray,
deferred.resolve,
deferred.reject
);
this.$el.find('div.chatroom-form-container').hide(
function () {
$(this).remove();
that.$el.find('.chat-area').removeClass('hidden');
that.$el.find('.occupants').removeClass('hidden');
});
return deferred.promise();
},
autoConfigureChatRoom: function (stanza) {
/* Automatically configure room based on the
* 'roomconfigure' data on this view's model.
*
* Returns a promise which resolves once a response IQ has
* been received.
*
* Parameters:
* (XMLElement) stanza: IQ stanza from the server,
* containing the configuration.
*/
var deferred = new $.Deferred();
var that = this, configArray = [],
$fields = $(stanza).find('field'),
count = $fields.length,
@ -874,23 +934,18 @@
if (!--count) {
that.sendConfiguration(
configArray,
that.onConfigSaved.bind(that),
that.onErrorConfigSaved.bind(that)
deferred.resolve,
_.partial(deferred.reject, stanza)
);
}
});
return deferred.promise();
},
onConfigSaved: function (stanza) {
// TODO: provide feedback
},
onErrorConfigSaved: function (stanza) {
this.showStatusNotification(__("An error occurred while trying to save the form."));
},
cancelConfiguration: function (ev) {
ev.preventDefault();
cancelConfiguration: function () {
/* Remove the configuration form without submitting and
* return to the chat view.
*/
var that = this;
this.$el.find('div.chatroom-form-container').hide(
function () {
@ -900,29 +955,85 @@
});
},
configureChatRoom: function (ev) {
var handleIQ;
if (typeof ev !== 'undefined' && ev.preventDefault) {
ev.preventDefault();
}
if (this.model.get('auto_configure')) {
handleIQ = this.autoConfigureChatRoom.bind(this);
} else {
if (this.$el.find('div.chatroom-form-container').length) {
return;
}
var $body = this.$('.chatroom-body');
$body.children().addClass('hidden');
$body.append(converse.templates.chatroom_form());
handleIQ = this.renderConfigurationForm.bind(this);
}
fetchRoomConfiguration: function (handler) {
/* Send an IQ stanza to fetch the room configuration data.
* Returns a promise which resolves once the response IQ
* has been received.
*
* Parameters:
* (Function) handler: The handler for the response IQ
*/
var that = this;
var deferred = new $.Deferred();
converse.connection.sendIQ(
$iq({
'to': this.model.get('jid'),
'type': "get"
}).c("query", {xmlns: Strophe.NS.MUC_OWNER}),
handleIQ
function (iq) {
if (handler) {
handler.apply(that, arguments);
}
deferred.resolve(iq);
},
deferred.reject // errback
);
return deferred.promise();
},
cacheRoomConfiguration: function () {
/* Fetch the room configuration, parse it and then
* save it on the Backbone.Model of this chat rooms.
*/
var that = this;
this.fetchRoomConfiguration().then(function (iq) {
var roomconfig = {};
_.each(iq.querySelectorAll('field'), function (field) {
var type = field.getAttribute('type');
if (type === 'hidden' && type === 'fixed') { return; }
var fieldname = field.getAttribute('var').replace('muc#roomconfig_', '');
var value = _.propertyOf(field.querySelector('value') || {})('textContent');
/* Unfortunately we don't have enough information
* to determine which values are actually integers, only
* booleans.
*/
if (type === "boolean") {
value = parseInt(value, 10);
}
roomconfig[fieldname] = value;
});
that.model.save(roomconfig);
});
},
configureChatRoom: function (ev) {
/* Start the process of configuring a chat room, either by
* rendering a configuration form, or by auto-configuring
* based on the "roomconfig" data stored on the
* Backbone.Model.
*
* Stores the new configuration on the Backbone.Model once
* completed.
*
* Paremeters:
* (Event) ev: DOM event that might be passed in if this
* method is called due to a user action. In this
* case, auto-configure won't happen, regardless of
* the settings.
*/
var that = this;
if (_.isUndefined(ev) && this.model.get('auto_configure')) {
this.fetchRoomConfiguration().then(function (iq) {
that.autoConfigureChatRoom(iq).then(that.cacheRoomConfiguration.bind(that));
});
} else {
if (typeof ev !== 'undefined' && ev.preventDefault) {
ev.preventDefault();
}
this.fetchRoomConfiguration().then(function (iq) {
that.renderConfigurationForm(iq).then(that.cacheRoomConfiguration.bind(that));
});
}
},
submitNickname: function (ev) {
@ -1097,8 +1208,9 @@
return;
},
showConfigureButtonIfRoomOwner: function (pres) {
/* Show the configure button if the user is the room owner.
findAndSaveOwnAffiliation: function (pres) {
/* Parse the presence stanza for the current user's
* affiliation.
*
* Parameters:
* (XMLElement) pres: A <presence> stanza.
@ -1187,12 +1299,22 @@
}
},
showStatusMessages: function (stanza, is_self) {
showStatusMessages: function (stanza) {
/* 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
*
* Parameters:
* (XMLElement) stanza: The message or presence stanza
* containing the status codes.
*/
var elements = stanza.querySelectorAll('x[xmlns="'+Strophe.NS.MUC_USER+'"]');
var is_self = stanza.querySelectorAll("status[code='110']").length;
// Unfortunately this doesn't work (returns empty list)
// var elements = stanza.querySelectorAll('x[xmlns="'+Strophe.NS.MUC_USER+'"]');
var elements = _.chain(stanza.querySelectorAll('x')).filter(function (x) {
return x.getAttribute('xmlns') == Strophe.NS.MUC_USER;
}).value();
var notifications = _.map(
elements,
_.partial(this.parseXUserElement.bind(this), _, stanza, is_self)
@ -1255,28 +1377,59 @@
return this;
},
createInstantRoom: function () {
/* Sends an empty IQ config stanza to inform the server that the
* room should be created with its default configuration.
*
* See * http://xmpp.org/extensions/xep-0045.html#createroom-instant
*/
this.sendConfiguration().then(this.cacheRoomConfiguration.bind(this));
},
onChatRoomPresence: function (pres) {
/* Handles all MUC presence stanzas.
*
* Parameters:
* (XMLElement) pres: The stanza
*/
var $presence = $(pres), is_self, new_room;
var nick = this.model.get('nick');
var show_status_messages = true;
if ($presence.attr('type') === 'error') {
this.model.set('connection_status', Strophe.Status.DISCONNECTED);
this.showErrorMessage(pres);
} else {
is_self = ($presence.find("status[code='110']").length) ||
($presence.attr('from') === this.model.get('id')+'/'+Strophe.escapeNode(nick));
is_self = $presence.find("status[code='110']").length;
new_room = $presence.find("status[code='201']").length;
if (is_self) {
this.model.set('connection_status', Strophe.Status.CONNECTED);
if (!converse.muc_instant_rooms && new_room) {
this.configureChatRoom();
this.findAndSaveOwnAffiliation(pres);
}
if (is_self && new_room) {
// This is a new room. It will now be configured
// and the configuration cached on the
// Backbone.Model.
if (converse.muc_instant_rooms) {
this.createInstantRoom(); // Accept default configuration
} else {
this.hideSpinner().showStatusMessages(pres, is_self);
this.showConfigureButtonIfRoomOwner(pres);
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;
}
}
} else if (this.model.get('connection_status') !== Strophe.Status.CONNECTED) {
// This is not a new room, and this is the first
// presence received for this room (hence the
// "connection_status" check), so we now cache the
// room configuration.
this.cacheRoomConfiguration();
}
if (show_status_messages) {
this.hideSpinner().showStatusMessages(pres);
}
this.occupantsview.updateOccupantsOnPresence(pres);
this.model.set('connection_status', Strophe.Status.CONNECTED);
}
return true;
},