Browse Source

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

JC Brand 8 years ago
parent
commit
fbf2e56be4
2 changed files with 224 additions and 69 deletions
  1. 6 4
      spec/chatroom.js
  2. 218 65
      src/converse-muc.js

+ 6 - 4
spec/chatroom.js

@@ -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();
@@ -738,9 +738,10 @@
                  *      type='unavailable'>
                  *  <x xmlns='http://jabber.org/protocol/muc#user'>
                  *      <item affiliation='none' role='none'>
-                 *      <actor nick='Fluellen'/>
-                 *      <reason>Avaunt, you cullion!</reason>
+                 *          <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');

+ 218 - 65
src/converse-muc.js

@@ -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);
                     }
-                    this.occupantsview.updateOccupantsOnPresence(pres);
                     return true;
                 },