فهرست منبع

disco: Create new plugin `converse-disco`.

We can now support feature discovery for multiple entities (although we
currently still only query for the user's own XMPP server).
JC Brand 8 سال پیش
والد
کامیت
7850c38faa
12فایلهای تغییر یافته به همراه140 افزوده شده و 197 حذف شده
  1. 1 1
      .eslintrc.json
  2. 5 0
      CHANGES.md
  3. 8 4
      spec/converse.js
  4. 7 2
      spec/disco.js
  5. 30 25
      spec/mam.js
  6. 1 0
      src/config.js
  7. 3 92
      src/converse-core.js
  8. 10 3
      src/converse-disco.js
  9. 22 21
      src/converse-mam.js
  10. 43 33
      src/converse-muc.js
  11. 1 1
      src/converse-ping.js
  12. 9 15
      src/converse-vcard.js

+ 1 - 1
.eslintrc.json

@@ -59,7 +59,7 @@
         "dot-notation": [
             "error",
             {
-                "allowKeywords": false
+                "allowKeywords": true
             }
         ],
         "eol-last": "error",

+ 5 - 0
CHANGES.md

@@ -2,6 +2,11 @@
 
 ## 3.2.0 (Unreleased)
 
+### New Plugins
+- New plugin `converse-disco` which replaces the original support for
+  [XEP-0030](https://xmpp.org/extensions/xep-0030.html) and which has been
+  refactored to allow features for multiple entities to be stored. [jcbrand]
+
 ### New features and improvements
 - Add support for Emojis (either native, or via <a href="https://www.emojione.com/">Emojione</a>). [jcbrand]
 - Add JID validation in the contact add form. [jcbrand]

+ 8 - 4
spec/converse.js

@@ -53,7 +53,11 @@
 
         describe("A chat state indication", function () {
 
-            it("are sent out when the client becomes or stops being idle", mock.initConverse(function (_converse) {
+            it("are sent out when the client becomes or stops being idle",
+                mock.initConverseWithPromises(
+                    null, ['discoInitialized'], {},
+                    function (done, _converse) {
+
                 spyOn(_converse, 'sendCSI').and.callThrough();
                 var sent_stanza;
                 spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
@@ -61,7 +65,7 @@
                 });
                 var i = 0;
                 _converse.idle_seconds = 0; // Usually initialized by registerIntervalHandler
-                _converse.features['urn:xmpp:csi:0'] = true; // Mock that the server supports CSI
+                _converse.disco_entities.get(_converse.domain).features['urn:xmpp:csi:0'] = true; // Mock that the server supports CSI
 
                 _converse.csi_waiting_time = 3; // The relevant config option
                 while (i <= _converse.csi_waiting_time) {
@@ -78,10 +82,10 @@
                 expect(sent_stanza.toLocaleString()).toBe(
                     "<active xmlns='urn:xmpp:csi:0'/>"
                 );
-
                 // Reset values
                 _converse.csi_waiting_time = 0;
-                _converse.features['urn:xmpp:csi:0'] = false;
+                _converse.disco_entities.get(_converse.domain).features['urn:xmpp:csi:0'] = false;
+                done();
             }));
         });
 

+ 7 - 2
spec/disco.js

@@ -11,11 +11,16 @@
 
     describe("Service Discovery", function () {
         describe("Whenever converse.js discovers a new server feature", function () {
-           it("emits the serviceDiscovered event", mock.initConverse(function (_converse) {
+           it("emits the serviceDiscovered event",
+                mock.initConverseWithPromises(
+                    null, ['discoInitialized'], {},
+                    function (done, _converse) {
+
                 sinon.spy(_converse, 'emit');
-                _converse.features.create({'var': Strophe.NS.MAM});
+                _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 expect(_converse.emit.called).toBe(true);
                 expect(_converse.emit.args[0][1].get('var')).toBe(Strophe.NS.MAM);
+                done();
             }));
         });
     });

+ 30 - 25
spec/mam.js

@@ -15,20 +15,25 @@
 
         describe("The archive.query API", function () {
 
-           it("can be used to query for all archived messages", mock.initConverse(function (_converse) {
+           it("can be used to query for all archived messages",
+                mock.initConverseWithPromises(
+                    null, ['discoInitialized'], {},
+                    function (done, _converse) {
+
                 var sent_stanza, IQ_id;
                 var sendIQ = _converse.connection.sendIQ;
                 spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
                     sent_stanza = iq;
                     IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
-                if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
-                    _converse.features.create({'var': Strophe.NS.MAM});
+                if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 }
                 _converse.api.archive.query();
                 var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
                 expect(sent_stanza.toString()).toBe(
                     "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'><query xmlns='urn:xmpp:mam:2' queryid='"+queryid+"'/></iq>");
+                done();
             }));
 
            it("can be used to query for all messages to/from a particular JID", mock.initConverse(function (_converse) {
@@ -38,8 +43,8 @@
                     sent_stanza = iq;
                     IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
-                if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
-                    _converse.features.create({'var': Strophe.NS.MAM});
+                if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 }
                 _converse.api.archive.query({'with':'juliet@capulet.lit'});
                 var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
@@ -66,8 +71,8 @@
                     sent_stanza = iq;
                     IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
-                if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
-                    _converse.features.create({'var': Strophe.NS.MAM});
+                if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 }
                 var start = '2010-06-07T00:00:00Z';
                 var end = '2010-07-07T13:23:54Z';
@@ -97,8 +102,8 @@
            }));
 
            it("throws a TypeError if an invalid date is provided", mock.initConverse(function (_converse) {
-                if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
-                    _converse.features.create({'var': Strophe.NS.MAM});
+                if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 }
                 expect(_.partial(_converse.api.archive.query, {'start': 'not a real date'})).toThrow(
                     new TypeError('archive.query: invalid date provided for: start')
@@ -112,8 +117,8 @@
                     sent_stanza = iq;
                     IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
-                if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
-                    _converse.features.create({'var': Strophe.NS.MAM});
+                if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 }
                 var start = '2010-06-07T00:00:00Z';
                 _converse.api.archive.query({'start': start});
@@ -141,8 +146,8 @@
                     sent_stanza = iq;
                     IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
-                if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
-                    _converse.features.create({'var': Strophe.NS.MAM});
+                if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 }
                 var start = '2010-06-07T00:00:00Z';
                 _converse.api.archive.query({'start': start, 'max':10});
@@ -173,8 +178,8 @@
                     sent_stanza = iq;
                     IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
-                if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
-                    _converse.features.create({'var': Strophe.NS.MAM});
+                if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 }
                 var start = '2010-06-07T00:00:00Z';
                 _converse.api.archive.query({
@@ -210,8 +215,8 @@
                     sent_stanza = iq;
                     IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
-                if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
-                    _converse.features.create({'var': Strophe.NS.MAM});
+                if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 }
                 _converse.api.archive.query({'before': '', 'max':10});
                 var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
@@ -237,8 +242,8 @@
                 // and pass it in. However, in the callback method an RSM object is
                 // returned which can be reused for easy paging. This test is
                 // more for that usecase.
-                if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
-                    _converse.features.create({'var': Strophe.NS.MAM});
+                if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 }
                 var sent_stanza, IQ_id;
                 var sendIQ = _converse.connection.sendIQ;
@@ -247,7 +252,7 @@
                     IQ_id = sendIQ.bind(this)(iq, callback, errback);
                 });
                 var rsm =  new Strophe.RSM({'max': '10'});
-                rsm['with'] = 'romeo@montague.lit';
+                rsm['with'] = 'romeo@montague.lit'; // eslint-disable-line dot-notation
                 rsm.start = '2010-06-07T00:00:00Z';
                 _converse.api.archive.query(rsm);
 
@@ -275,8 +280,8 @@
            }));
 
            it("accepts a callback function, which it passes the messages and a Strophe.RSM object", mock.initConverse(function (_converse) {
-                if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
-                    _converse.features.create({'var': Strophe.NS.MAM});
+                if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
+                    _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
                 }
                 var sent_stanza, IQ_id;
                 var sendIQ = _converse.connection.sendIQ;
@@ -351,7 +356,7 @@
                 expect(args[0].length).toBe(2);
                 expect(args[0][0].outerHTML).toBe(msg1.nodeTree.outerHTML);
                 expect(args[0][1].outerHTML).toBe(msg2.nodeTree.outerHTML);
-                expect(args[1]['with']).toBe('romeo@capulet.lit');
+                expect(args[1]['with']).toBe('romeo@capulet.lit'); // eslint-disable-line dot-notation
                 expect(args[1].max).toBe('10');
                 expect(args[1].count).toBe('16');
                 expect(args[1].first).toBe('23452-4534-1');
@@ -376,7 +381,7 @@
                     'var': Strophe.NS.MAM
                 });
                 spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set
-                _converse.features.onFeatureAdded(feature);
+                _converse.disco_entities.get(_converse.domain).features.onFeatureAdded(feature);
 
                 expect(_converse.connection.sendIQ).toHaveBeenCalled();
                 expect(sent_stanza.toLocaleString()).toBe(
@@ -430,7 +435,7 @@
                     .c('never').up();
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
                 expect(feature.save).toHaveBeenCalled();
-                expect(feature.get('preferences')['default']).toBe('never');
+                expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation
 
                 // Restore
                 _converse.message_archiving = 'never';

+ 1 - 0
src/config.js

@@ -55,6 +55,7 @@ require.config({
         "converse-chatview":        "src/converse-chatview",
         "converse-controlbox":      "src/converse-controlbox",
         "converse-core":            "src/converse-core",
+        "converse-disco":           "src/converse-disco",
         "converse-dragresize":      "src/converse-dragresize",
         "converse-headline":        "src/converse-headline",
         "converse-inverse":         "src/converse-inverse",

+ 3 - 92
src/converse-core.js

@@ -16,7 +16,6 @@
             "strophe",
             "pluggable",
             "backbone.noconflict",
-            "strophe.disco",
             "backbone.browserStorage",
             "backbone.overview",
     ], factory);
@@ -59,6 +58,7 @@
         'converse-chatview',
         'converse-controlbox',
         'converse-core',
+        'converse-disco',
         'converse-dragresize',
         'converse-headline',
         'converse-mam',
@@ -794,16 +794,11 @@
             // If there's no xmppstatus obj, then we were never connected to
             // begin with, so we set reconnecting to false.
             reconnecting = _.isUndefined(_converse.xmppstatus) ? false : reconnecting;
-
             if (reconnecting) {
                 _converse.onStatusInitialized(true);
                 _converse.emit('reconnected');
             } else {
-                // There might be some open chat boxes. We don't
-                // know whether these boxes are of the same account or not, so we
-                // close them now.
                 _converse.chatboxviews.closeAllChatBoxes();
-                _converse.features = new _converse.Features();
                 _converse.initStatus()
                     .then(
                         _.partial(_converse.onStatusInitialized, false),
@@ -1866,88 +1861,6 @@
             }
         });
 
-        this.Features = Backbone.Collection.extend({
-            /* Service Discovery
-             * -----------------
-             * This collection stores Feature Models, representing features
-             * provided by available XMPP entities (e.g. servers)
-             * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
-             * All features are shown here: http://xmpp.org/registrar/disco-features.html
-             */
-            model: Backbone.Model,
-            initialize () {
-                this.addClientIdentities().addClientFeatures();
-                this.browserStorage = new Backbone.BrowserStorage[_converse.storage](
-                    b64_sha1(`converse.features${_converse.bare_jid}`)
-                );
-                this.on('add', this.onFeatureAdded, this);
-                this.fetchFeatures();
-            },
-
-            fetchFeatures () {
-                if (this.browserStorage.records.length === 0) {
-                    // browserStorage is empty, so we've likely never queried this
-                    // domain for features yet
-                    _converse.connection.disco.info(_converse.domain, null, this.onInfo.bind(this));
-                    _converse.connection.disco.items(_converse.domain, null, this.onItems.bind(this));
-                } else {
-                    this.fetch({add:true});
-                }
-            },
-
-            onFeatureAdded (feature) {
-                _converse.emit('serviceDiscovered', feature);
-            },
-
-            addClientIdentities () {
-                /* See http://xmpp.org/registrar/disco-categories.html
-                 */
-                 _converse.connection.disco.addIdentity('client', 'web', 'Converse.js');
-                 return this;
-            },
-
-            addClientFeatures () {
-                /* The strophe.disco.js plugin keeps a list of features which
-                 * it will advertise to any #info queries made to it.
-                 *
-                 * See: http://xmpp.org/extensions/xep-0030.html#info
-                 */
-                _converse.connection.disco.addFeature(Strophe.NS.BOSH);
-                _converse.connection.disco.addFeature(Strophe.NS.CHATSTATES);
-                _converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
-                _converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
-                if (_converse.message_carbons) {
-                    _converse.connection.disco.addFeature(Strophe.NS.CARBONS);
-                }
-                return this;
-            },
-
-            onItems (stanza) {
-                _.each(stanza.querySelectorAll('query item'), (item) => {
-                    _converse.connection.disco.info(
-                        item.getAttribute('jid'),
-                        null,
-                        this.onInfo.bind(this));
-                });
-            },
-
-            onInfo (stanza) {
-                if ((sizzle('identity[category=server][type=im]', stanza).length === 0) &&
-                    (sizzle('identity[category=conference][type=text]', stanza).length === 0)) {
-                    // This isn't an IM server component
-                    return;
-                }
-                _.forEach(stanza.querySelectorAll('feature'), (feature) => {
-                    const namespace = feature.getAttribute('var');
-                    this[namespace] = true;
-                    this.create({
-                        'var': namespace,
-                        'from': stanza.getAttribute('from')
-                    });
-                });
-            }
-        });
-
         this.setUpXMLLogging = function () {
             Strophe.log = function (level, msg) {
                 _converse.log(msg, level);
@@ -2165,16 +2078,13 @@
             /* Remove those views which are only allowed with a valid
              * connection.
              */
+            _converse.emit('beforeTearDown');
             this.unregisterPresenceHandler();
             if (this.roster) {
                 this.roster.off().reset(); // Removes roster contacts
             }
             this.chatboxes.remove(); // Don't call off(), events won't get re-registered upon reconnect.
             delete this.chatboxes.browserStorage;
-            if (this.features) {
-                this.features.reset();
-                this.features.browserStorage._clear();
-            }
             this.session.destroy();
             window.removeEventListener('click', _converse.onUserActivity);
             window.removeEventListener('focus', _converse.onUserActivity);
@@ -2182,6 +2092,7 @@
             window.removeEventListener('mousemove', _converse.onUserActivity);
             window.removeEventListener(unloadevent, _converse.onUserActivity);
             window.clearInterval(_converse.everySecondTrigger);
+            _converse.emit('afterTearDown');
             return this;
         };
 

+ 10 - 3
src/converse-disco.js

@@ -84,7 +84,12 @@
                 * All features are shown here: http://xmpp.org/registrar/disco-features.html
                 */
                 model: Backbone.Model,
-                initialize (jid) {
+
+                initialize (settings) {
+                    const jid = settings.jid;
+                    if (_.isNil(jid)) {
+                        throw new Error('DiscoEntity must be instantiated with a JID');
+                    }
                     this.addClientIdentities().addClientFeatures();
                     this.browserStorage = new Backbone.BrowserStorage[_converse.storage](
                         b64_sha1(`converse.features-${jid}`)
@@ -158,9 +163,11 @@
                 }
             });
 
-            _converse.api.waitUntil('connected').then(() => {
+            function initializeDisco () {
                 _converse.disco_entities = new _converse.DiscoEntities();
-            });
+            }
+            _converse.api.listen.on('reconnected', initializeDisco);
+            _converse.api.listen.on('connected', initializeDisco);
 
             _converse.api.listen.on('beforeTearDown', () => {
                 if (_converse.disco_entities) {

+ 22 - 21
src/converse-mam.js

@@ -11,6 +11,7 @@
 (function (root, factory) {
     define(["jquery.noconflict",
             "converse-core",
+            "converse-disco",
             "converse-chatview", // Could be made a soft dependency
             "converse-muc", // Could be made a soft dependency
             "strophe.rsm"
@@ -31,15 +32,6 @@
             // relevant objects or classes.
             //
             // New functions which don't exist yet can also be added.
-
-            Features: {
-                addClientFeatures () {
-                    const { _converse } = this.__super__;
-                    _converse.connection.disco.addFeature(Strophe.NS.MAM);
-                    return this.__super__.addClientFeatures.apply(this, arguments);
-                }
-            },
-
             ChatBox: {
                 getMessageAttributes ($message, $delay, original_stanza) {
                     const attrs = this.__super__.getMessageAttributes.apply(this, arguments);
@@ -60,7 +52,8 @@
                 afterMessagesFetched () {
                     const { _converse } = this.__super__;
                     if (this.disable_mam ||
-                            !_converse.features.findWhere({'var': Strophe.NS.MAM})) {
+                            !_converse.disco_entities.get(_converse.domain)
+                                .features.findWhere({'var': Strophe.NS.MAM})) {
                         return this.__super__.afterMessagesFetched.apply(this, arguments);
                     }
                     if (!this.model.get('mam_initialized') &&
@@ -83,7 +76,9 @@
                      * box, so that they are displayed inside it.
                      */
                     const { _converse } = this.__super__;
-                    if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
+                    if (!_converse.disco_entities.get(_converse.domain)
+                            .features.findWhere({'var': Strophe.NS.MAM})) {
+
                         _converse.log(
                             "Attempted to fetch archived messages but this "+
                             "user's server doesn't support XEP-0313",
@@ -167,7 +162,9 @@
                      * so that they are displayed inside it.
                      */
                     const { _converse } = this.__super__;
-                    if (!_converse.features.findWhere({'var': Strophe.NS.MAM})) {
+                    if (!_converse.disco_entities.get(_converse.domain)
+                            .features.findWhere({'var': Strophe.NS.MAM})) {
+
                         _converse.log(
                             "Attempted to fetch archived messages but this "+
                             "user's server doesn't support XEP-0313",
@@ -237,12 +234,12 @@
                 const queryid = _converse.connection.getUniqueId();
                 const attrs = {'type':'set'};
                 if (!_.isUndefined(options) && options.groupchat) {
-                    if (!options['with']) {
+                    if (!options['with']) { // eslint-disable-line dot-notation
                         throw new Error(
                             'You need to specify a "with" value containing '+
                             'the chat room JID, when querying groupchat messages.');
                     }
-                    attrs.to = options['with'];
+                    attrs.to = options['with']; // eslint-disable-line dot-notation
                 }
                 const stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid});
                 if (!_.isUndefined(options)) {
@@ -250,8 +247,9 @@
                             .c('field', {'var':'FORM_TYPE', 'type': 'hidden'})
                             .c('value').t(Strophe.NS.MAM).up().up();
 
-                    if (options['with'] && !options.groupchat) {
-                        stanza.c('field', {'var':'with'}).c('value').t(options['with']).up().up();
+                    if (options['with'] && !options.groupchat) {  // eslint-disable-line dot-notation
+                        stanza.c('field', {'var':'with'}).c('value')
+                            .t(options['with']).up().up(); // eslint-disable-line dot-notation
                     }
                     _.each(['start', 'end'], function (t) {
                         if (options[t]) {
@@ -352,11 +350,11 @@
                 }
             };
 
-
-            const onFeatureAdded = function (feature) {
+            /* Event handlers */
+            _converse.on('serviceDiscovered', (feature) => {
                 const prefs = feature.get('preferences') || {};
                 if (feature.get('var') === Strophe.NS.MAM &&
-                        prefs['default'] !== _converse.message_archiving &&
+                        prefs['default'] !== _converse.message_archiving && // eslint-disable-line dot-notation
                         !_.isUndefined(_converse.message_archiving) ) {
                     // Ask the server for archiving preferences
                     _converse.connection.sendIQ(
@@ -365,8 +363,11 @@
                         _.partial(_converse.onMAMError, feature)
                     );
                 }
-            };
-            _converse.on('serviceDiscovered', onFeatureAdded.bind(_converse.features));
+            });
+
+            _converse.on('addClientFeatures', () => {
+                _converse.connection.disco.addFeature(Strophe.NS.MAM);
+            });
         }
     });
 }));

+ 43 - 33
src/converse-muc.js

@@ -32,7 +32,8 @@
             "tpl!room_panel",
             "tpl!spinner",
             "awesomplete",
-            "converse-chatview"
+            "converse-chatview",
+            "converse-disco"
     ], factory);
 }(this, function (
             $,
@@ -130,19 +131,6 @@
                 this.__super__._tearDown.call(this, arguments);
             },
 
-            Features: {
-                addClientFeatures () {
-                    const { _converse } = this.__super__;
-                    this.__super__.addClientFeatures.apply(this, arguments);
-                    if (_converse.allow_muc_invitations) {
-                        _converse.connection.disco.addFeature('jabber:x:conference'); // Invites
-                    }
-                    if (_converse.allow_muc) {
-                        _converse.connection.disco.addFeature(Strophe.NS.MUC);
-                    }
-                }
-            },
-
             ChatBoxes: {
                 model (attrs, options) {
                     const { _converse } = this.__super__;
@@ -182,41 +170,54 @@
                     }
                 },
 
-                onConnected () {
+                featureAdded (feature) {
                     const { _converse } = this.__super__;
-                    this.__super__.onConnected.apply(this, arguments);
-                    if (!this.model.get('connected')) {
-                        return;
+                    if ((feature.get('var') === Strophe.NS.MUC) && (_converse.allow_muc)) {
+                        this.setMUCDomain(feature.get('from'));
                     }
-                    if (_.isUndefined(_converse.muc_domain)) {
-                        _converse.features.off('add', this.featureAdded, this);
-                        _converse.features.on('add', this.featureAdded, this);
+                },
+
+                getMUCDomainFromDisco () {
+                    /* Check whether service discovery for the user's domain
+                     * returned MUC information and use that to automatically
+                     * set the MUC domain for the "Rooms" panel of the
+                     * controlbox.
+                     */
+                    const { _converse } = this.__super__;
+                    _converse.api.waitUntil('discoInitialized').then(() => {
+                        _converse.api.listen.on('serviceDiscovered', this.featureAdded, this);
                         // Features could have been added before the controlbox was
                         // initialized. We're only interested in MUC
-                        const feature = _converse.features.findWhere({
+                        const feature = _converse.disco_entities[_converse.domain].features.findWhere({
                             'var': Strophe.NS.MUC
                         });
                         if (feature) {
                             this.featureAdded(feature);
                         }
+                    });
+                },
+
+                onConnected () {
+                    const { _converse } = this.__super__;
+                    this.__super__.onConnected.apply(this, arguments);
+                    if (!this.model.get('connected')) {
+                        return;
+                    }
+                    if (_.isUndefined(_converse.muc_domain)) {
+                        this.getMUCDomainFromDisco();
                     } else {
                         this.setMUCDomain(_converse.muc_domain);
                     }
                 },
 
                 setMUCDomain (domain) {
+                    const { _converse } = this.__super__;
+                    _converse.muc_domain = domain;
                     this.roomspanel.model.save({'muc_domain': domain});
                     const $server= this.$el.find('input.new-chatroom-server');
                     if (!$server.is(':focus')) {
                         $server.val(this.roomspanel.model.get('muc_domain'));
                     }
-                },
-
-                featureAdded (feature) {
-                    const { _converse } = this.__super__;
-                    if ((feature.get('var') === Strophe.NS.MUC) && (_converse.allow_muc)) {
-                        this.setMUCDomain(feature.get('from'));
-                    }
                 }
             },
 
@@ -1974,7 +1975,7 @@
                         return false;
                     } else {
                         return this.model.messages.where({
-                            'sender': this.model.get('nick'),
+                            'sender': 'me',
                             'message': this.model.getMessageBody(message)
                         }).filter(
                             (msg) => Math.abs(moment(msg.get('time')).diff(moment.unix(ts))) < 2000
@@ -2778,7 +2779,17 @@
                 }
             });
 
-            function reconnectToChatRooms () {
+            /* Event handlers */
+            _converse.on('addClientFeatures', () => {
+                if (_converse.allow_muc) {
+                    _converse.connection.disco.addFeature(Strophe.NS.MUC);
+                }
+                if (_converse.allow_muc_invitations) {
+                    _converse.connection.disco.addFeature('jabber:x:conference'); // Invites
+                }
+            });
+
+            _converse.on('reconnected', function reconnectToChatRooms () {
                 /* Upon a reconnection event from converse, join again
                  * all the open chat rooms.
                  */
@@ -2790,8 +2801,7 @@
                         view.fetchMessages();
                     }
                 });
-            }
-            _converse.on('reconnected', reconnectToChatRooms);
+            });
 
             function disconnectChatRooms () {
                 /* When disconnecting, or reconnecting, mark all chat rooms as

+ 1 - 1
src/converse-ping.js

@@ -34,7 +34,7 @@
                 // However, some servers don't advertise while still keeping the
                 // connection option due to pings.
                 //
-                // var feature = _converse.features.findWhere({'var': Strophe.NS.PING});
+                // var feature = _converse.disco_entities[_converse.domain].features.findWhere({'var': Strophe.NS.PING});
                 _converse.lastStanzaDate = new Date();
                 if (_.isNil(jid)) {
                     jid = Strophe.getDomainFromJid(_converse.bare_jid);

+ 9 - 15
src/converse-vcard.js

@@ -21,16 +21,6 @@
             //
             // New functions which don't exist yet can also be added.
 
-            Features: {
-                addClientFeatures () {
-                    const { _converse } = this.__super__;
-                    this.__super__.addClientFeatures.apply(this, arguments);
-                    if (_converse.use_vcards) {
-                        _converse.connection.disco.addFeature(Strophe.NS.VCARD);
-                    }
-                }
-            },
-
             RosterContacts: {
                 createRequestingContact (presence) {
                     const { _converse } = this.__super__;
@@ -50,7 +40,6 @@
             }
         },
 
-
         initialize () {
             /* The initialize function gets called as soon as the plugin is
              * loaded by converse.js's plugin machinery.
@@ -135,6 +124,13 @@
                 }
             };
 
+            /* Event handlers */
+            _converse.on('addClientFeatures', () => {
+                if (_converse.use_vcards) {
+                    _converse.connection.disco.addFeature(Strophe.NS.VCARD);
+                }
+            });
+
             const updateVCardForChatBox = function (chatbox) {
                 if (!_converse.use_vcards) { return; }
                 const jid = chatbox.model.get('jid'),
@@ -161,7 +157,6 @@
             };
             _converse.on('chatBoxInitialized', updateVCardForChatBox);
 
-
             const onContactAdd = function (contact) {
                 if (!contact.get('vcard_updated')) {
                     // This will update the vcard, which triggers a change
@@ -173,7 +168,7 @@
                 _converse.roster.on("add", onContactAdd);
             });
 
-            const fetchOwnVCard = function () {
+            _converse.on('statusInitialized', function fetchOwnVCard () {
                 if (_converse.xmppstatus.get('fullname') === undefined) {
                     _converse.getVCard(
                         null, // No 'to' attr when getting one's own vCard
@@ -182,8 +177,7 @@
                         }
                     );
                 }
-            };
-            _converse.on('statusInitialized', fetchOwnVCard);
+            });
         }
     });
 }));