Bladeren bron

New dist files

JC Brand 7 jaren geleden
bovenliggende
commit
ebcf7098bb
2 gewijzigde bestanden met toevoegingen van 426 en 199 verwijderingen
  1. 140 62
      dist/converse-no-dependencies.js
  2. 286 137
      dist/converse.js

+ 140 - 62
dist/converse-no-dependencies.js

@@ -10760,7 +10760,7 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
 // Converse.js
 // https://conversejs.org
 //
-// Copyright (c) 2012-2018, the Converse.js developers
+// Copyright (c) 2013-2018, the Converse.js developers
 // Licensed under the Mozilla Public License (MPLv2)
 (function (root, factory) {
   define('converse-core',["sizzle", "es6-promise", "lodash.noconflict", "lodash.fp", "polyfill", "i18n", "utils", "moment", "strophe", "pluggable", "backbone.noconflict", "backbone.nativeview", "backbone.browserStorage"], factory);
@@ -11427,6 +11427,8 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
       _converse.session.browserStorage = new Backbone.BrowserStorage[_converse.storage](id);
 
       _converse.session.fetch();
+
+      _converse.emit('sessionInitialized');
     };
 
     this.clearSession = function () {
@@ -13086,15 +13088,15 @@ define("emojione", (function (global) {
         initialize: function initialize() {
           this.waitUntilFeaturesDiscovered = utils.getResolveablePromise();
           this.dataforms = new Backbone.Collection();
-          this.dataforms.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.dataforms-{this.get('jid')}"));
+          this.dataforms.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.dataforms-{this.get('jid')}"));
           this.features = new Backbone.Collection();
-          this.features.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.features-".concat(this.get('jid'))));
+          this.features.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.features-".concat(this.get('jid'))));
           this.features.on('add', this.onFeatureAdded, this);
           this.identities = new Backbone.Collection();
-          this.identities.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.identities-".concat(this.get('jid'))));
+          this.identities.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.identities-".concat(this.get('jid'))));
           this.fetchFeatures();
           this.items = new _converse.DiscoEntities();
-          this.items.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.disco-items-".concat(this.get('jid'))));
+          this.items.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.disco-items-".concat(this.get('jid'))));
           this.items.fetch();
         },
         getIdentity: function getIdentity(category, type) {
@@ -13275,13 +13277,33 @@ define("emojione", (function (global) {
         return this;
       }
 
+      function initStreamFeatures() {
+        _converse.stream_features = new Backbone.Collection();
+        _converse.stream_features.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.stream-features-".concat(_converse.bare_jid)));
+
+        _converse.stream_features.fetch({
+          success: function success(collection) {
+            if (collection.length === 0 && _converse.connection.features) {
+              _.forEach(_converse.connection.features.childNodes, function (feature) {
+                _converse.stream_features.create({
+                  'name': feature.nodeName,
+                  'xmlns': feature.getAttribute('xmlns')
+                });
+              });
+            }
+          }
+        });
+
+        _converse.emit('streamFeaturesAdded');
+      }
+
       function initializeDisco() {
         addClientFeatures();
 
         _converse.connection.addHandler(onDiscoInfoRequest, Strophe.NS.DISCO_INFO, 'iq', 'get', null, null);
 
         _converse.disco_entities = new _converse.DiscoEntities();
-        _converse.disco_entities.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.disco-entities-".concat(_converse.bare_jid)));
+        _converse.disco_entities.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.disco-entities-".concat(_converse.bare_jid)));
 
         _converse.disco_entities.fetchEntities().then(function (collection) {
           if (collection.length === 0 || !collection.get(_converse.domain)) {
@@ -13296,6 +13318,8 @@ define("emojione", (function (global) {
         }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
       }
 
+      _converse.api.listen.on('sessionInitialized', initStreamFeatures);
+
       _converse.api.listen.on('reconnected', initializeDisco);
 
       _converse.api.listen.on('connected', initializeDisco);
@@ -13376,6 +13400,19 @@ define("emojione", (function (global) {
          * @namespace
          */
         'disco': {
+          'stream': {
+            'getFeature': function getFeature(name, xmlns) {
+              if (_.isNil(name) || _.isNil(xmlns)) {
+                throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature");
+              }
+
+              return _converse.stream_features.findWhere({
+                'name': name,
+                'xmlns': xmlns
+              });
+            }
+          },
+
           /**
            * The "own" grouping
            * @namespace
@@ -15890,19 +15927,24 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
           this.on('change:image_hash', this.onAvatarChanged, this);
         },
         onAvatarChanged: function onAvatarChanged() {
-          var vcard = _converse.vcards.findWhere({
-            'jid': this.get('from')
-          });
+          var hash = this.get('image_hash');
+          var vcards = [];
 
-          if (!vcard) {
-            return;
+          if (this.get('jid')) {
+            vcards.push(this.updateVCard(_converse.vcards.findWhere({
+              'jid': this.get('jid')
+            })));
           }
 
-          var hash = this.get('image_hash');
+          vcards.push(this.updateVCard(_converse.vcards.findWhere({
+            'jid': this.get('from')
+          })));
 
-          if (hash && vcard.get('image_hash') !== hash) {
-            _converse.api.vcard.update(vcard);
-          }
+          _.forEach(_.filter(vcards, undefined), function (vcard) {
+            if (hash && vcard.get('image_hash') !== hash) {
+              _converse.api.vcard.update(vcard);
+            }
+          });
         },
         getDisplayName: function getDisplayName() {
           return this.get('nick') || this.get('jid');
@@ -15950,6 +15992,10 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
                 'jid': removed_jid
               });
 
+              if (!occupant) {
+                return;
+              }
+
               if (occupant.get('show') === 'offline') {
                 occupant.destroy();
               }
@@ -25093,7 +25139,7 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
 // Converse.js
 // http://conversejs.org
 //
-// Copyright (c) 2012-2018, the Converse.js developers
+// Copyright (c) 2013-2018, the Converse.js developers
 // Licensed under the Mozilla Public License (MPLv2)
 (function (root, factory) {
   define('converse-vcard',["converse-core", "crypto", "tpl!vcard"], factory);
@@ -25166,7 +25212,9 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
             'image_type': _.get(vcard.querySelector('PHOTO TYPE'), 'textContent'),
             'url': _.get(vcard.querySelector('URL'), 'textContent'),
             'role': _.get(vcard.querySelector('ROLE'), 'textContent'),
-            'email': _.get(vcard.querySelector('EMAIL USERID'), 'textContent')
+            'email': _.get(vcard.querySelector('EMAIL USERID'), 'textContent'),
+            'vcard_updated': moment().format(),
+            'vcard_error': undefined
           };
         }
 
@@ -25184,7 +25232,8 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
         if (errback) {
           errback({
             'stanza': iq,
-            'jid': jid
+            'jid': jid,
+            'vcard_error': moment().format()
           });
         }
       }
@@ -25250,8 +25299,8 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
           'get': function get(model, force) {
             if (_.isString(model)) {
               return getVCard(_converse, model);
-            } else if (!model.get('vcard_updated') || force) {
-              var jid = model.get('jid') || model.get('muc_jid');
+            } else if (force || !model.get('vcard_updated') || !moment(model.get('vcard_error')).isSame(new Date(), "day")) {
+              var jid = model.get('jid');
 
               if (!jid) {
                 throw new Error("No JID to get vcard for!");
@@ -25267,9 +25316,8 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
 
             return new Promise(function (resolve, reject) {
               _this.get(model, force).then(function (vcard) {
-                model.save(_.extend(_.pick(vcard, ['fullname', 'nickname', 'email', 'url', 'role', 'image_type', 'image', 'image_hash']), {
-                  'vcard_updated': moment().format()
-                }));
+                delete vcard['stanza'];
+                model.save(vcard);
                 resolve();
               });
             });
@@ -31439,6 +31487,13 @@ return __p
                 _converse.roster = new _converse.RosterContacts();
                 _converse.roster.browserStorage = new Backbone.BrowserStorage[_converse.storage](
                     b64_sha1(`converse.contacts-${_converse.bare_jid}`));
+
+                _converse.roster.data = new Backbone.Model();
+                const id = b64_sha1(`converse-roster-model-${_converse.bare_jid}`);
+                _converse.roster.data.id = id;
+                _converse.roster.data.browserStorage = new Backbone.BrowserStorage[_converse.storage](id);
+                _converse.roster.data.fetch();
+
                 _converse.rostergroups = new _converse.RosterGroups();
                 _converse.rostergroups.browserStorage = new Backbone.BrowserStorage[_converse.storage](
                     b64_sha1(`converse.roster.groups${_converse.bare_jid}`));
@@ -31721,28 +31776,28 @@ return __p
 
                 onConnected () {
                     /* Called as soon as the connection has been established
-                    * (either after initial login, or after reconnection).
-                    *
-                    * Use the opportunity to register stanza handlers.
-                    */
+                     * (either after initial login, or after reconnection).
+                     *
+                     * Use the opportunity to register stanza handlers.
+                     */
                     this.registerRosterHandler();
                     this.registerRosterXHandler();
                 },
 
                 registerRosterHandler () {
                     /* Register a handler for roster IQ "set" stanzas, which update
-                    * roster contacts.
-                    */
-                    _converse.connection.addHandler(
-                        _converse.roster.onRosterPush.bind(_converse.roster),
-                        Strophe.NS.ROSTER, 'iq', "set"
-                    );
+                     * roster contacts.
+                     */
+                    _converse.connection.addHandler((iq) => {
+                        _converse.roster.onRosterPush(iq);
+                        return true;
+                    }, Strophe.NS.ROSTER, 'iq', "set");
                 },
 
                 registerRosterXHandler () {
                     /* Register a handler for RosterX message stanzas, which are
-                    * used to suggest roster contacts to a user.
-                    */
+                     * used to suggest roster contacts to a user.
+                     */
                     let t = 0;
                     _converse.connection.addHandler(
                         function (msg) {
@@ -31766,12 +31821,14 @@ return __p
                      * Returns a promise which resolves once the contacts have been
                      * fetched.
                      */
+                    const that = this;
                     return new Promise((resolve, reject) => {
                         this.fetch({
                             'add': true,
                             'silent': true,
                             success (collection) {
-                                if (collection.length === 0) {
+                                if (collection.length === 0 || 
+                                        (that.rosterVersioningSupported() && !_converse.session.get('roster_fetched'))) {
                                     _converse.send_initial_presence = true;
                                     _converse.roster.fetchFromServer().then(resolve).catch(reject);
                                 } else {
@@ -31897,30 +31954,44 @@ return __p
 
                 onRosterPush (iq) {
                     /* Handle roster updates from the XMPP server.
-                    * See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
-                    *
-                    * Parameters:
-                    *    (XMLElement) IQ - The IQ stanza received from the XMPP server.
-                    */
+                     * See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
+                     *
+                     * Parameters:
+                     *    (XMLElement) IQ - The IQ stanza received from the XMPP server.
+                     */
                     const id = iq.getAttribute('id');
                     const from = iq.getAttribute('from');
-                    if (from && from !== "" && Strophe.getBareJidFromJid(from) !== _converse.bare_jid) {
-                        // Receiving client MUST ignore stanza unless it has no from or from = user's bare JID.
-                        // XXX: Some naughty servers apparently send from a full
-                        // JID so we need to explicitly compare bare jids here.
-                        // https://github.com/jcbrand/converse.js/issues/493
-                        _converse.connection.send(
-                            $iq({type: 'error', id, from: _converse.connection.jid})
-                                .c('error', {'type': 'cancel'})
-                                .c('service-unavailable', {'xmlns': Strophe.NS.ROSTER })
-                        );
-                        return true;
+                    if (from && from !== _converse.connection.jid) {
+                        // https://tools.ietf.org/html/rfc6121#page-15
+                        // 
+                        // A receiving client MUST ignore the stanza unless it has no 'from'
+                        // attribute (i.e., implicitly from the bare JID of the user's
+                        // account) or it has a 'from' attribute whose value matches the
+                        // user's bare JID <user@domainpart>.
+                        return;
                     }
                     _converse.connection.send($iq({type: 'result', id, from: _converse.connection.jid}));
-                    const items = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"] item`, iq);
-                    _.each(items, this.updateContact.bind(this));
+
+                    const query = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, iq).pop();
+                    this.data.save('version', query.getAttribute('ver'));
+
+                    const items = sizzle(`item`, query);
+                    if (items.length > 1) {
+                        _converse.log(iq, Strophe.LogLevel.ERROR);
+                        throw new Error('Roster push query may not contain more than one "item" element.');
+                    }
+                    if (items.length === 0) {
+                        _converse.log(iq, Strophe.LogLevel.WARN);
+                        _converse.log('Received a roster push stanza without an "item" element.', Strophe.LogLevel.WARN);
+                        return;
+                    }
+                    this.updateContact(items.pop());
                     _converse.emit('rosterPush', iq);
-                    return true;
+                    return;
+                },
+
+                rosterVersioningSupported () {
+                    return _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') && this.data.get('version');
                 },
 
                 fetchFromServer () {
@@ -31930,7 +32001,9 @@ return __p
                             'type': 'get',
                             'id': _converse.connection.getUniqueId('roster')
                         }).c('query', {xmlns: Strophe.NS.ROSTER});
-
+                        if (this.rosterVersioningSupported()) {
+                            iq.attrs({'ver': this.data.get('version')});
+                        }
                         const callback = _.flow(this.onReceivedFromServer.bind(this), resolve);
                         const errback = function (iq) {
                             const errmsg = "Error while trying to fetch roster from the server";
@@ -31943,17 +32016,22 @@ return __p
 
                 onReceivedFromServer (iq) {
                     /* An IQ stanza containing the roster has been received from
-                    * the XMPP server.
-                    */
-                    const items = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"] item`, iq);
-                    _.each(items, this.updateContact.bind(this));
+                     * the XMPP server.
+                     */
+                    const query = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, iq).pop();
+                    if (query) {
+                        const items = sizzle(`item`, query);
+                        _.each(items, (item) => this.updateContact(item));
+                        this.data.save('version', query.getAttribute('ver'));
+                        _converse.session.save('roster_fetched', true);
+                    }
                     _converse.emit('roster', iq);
                 },
 
                 updateContact (item) {
                     /* Update or create RosterContact models based on items
-                    * received in the IQ from the server.
-                    */
+                     * received in the IQ from the server.
+                     */
                     const jid = item.getAttribute('jid');
                     if (this.isSelf(jid)) { return; }
 

+ 286 - 137
dist/converse.js

@@ -32610,6 +32610,8 @@ if (!root.atob) {
         define('strophe-sha1', [],function () {
             return factory();
         });
+    } else if (typeof exports === 'object') {
+        module.exports = factory();
     } else {
         // Browser globals
         root.SHA1 = factory();
@@ -32804,6 +32806,8 @@ return {
         define('strophe-md5',[], function () {
             return factory();
         });
+    } else if (typeof exports === 'object') {
+        module.exports = factory();
     } else {
         // Browser globals
         root.MD5 = factory();
@@ -33006,6 +33010,8 @@ return {
         define('strophe-utils',[], function () {
             return factory();
         });
+    } else if (typeof exports === 'object') {
+        module.exports = factory();
     } else {
         // Browser globals
         root.stropheUtils = factory();
@@ -33094,6 +33100,12 @@ return {
         ], function () {
             return factory.apply(this, arguments);
         });
+    }  else if (typeof exports === 'object') {
+        module.exports = factory(
+            require('./sha1'),
+            require('./md5'),
+            require('./utils')
+        );
     } else {
         // Browser globals
         var o = factory(root.SHA1, root.MD5, root.stropheUtils);
@@ -33168,7 +33180,7 @@ function $pres(attrs) { return new Strophe.Builder("presence", attrs); }
  */
 Strophe = {
     /** Constant: VERSION */
-    VERSION: "1.2.14",
+    VERSION: "1.2.15",
 
     /** Constants: XMPP Namespace Constants
      *  Common namespace constants from the XMPP RFCs and XEPs.
@@ -33306,6 +33318,14 @@ Strophe = {
         CONNTIMEOUT: 10
     },
 
+    ErrorCondition: {
+        BAD_FORMAT: "bad-format",
+        CONFLICT: "conflict",
+        MISSING_JID_NODE: "x-strophe-bad-non-anon-jid",
+        NO_AUTH_MECH: "no-auth-mech",
+        UNKNOWN_REASON: "unknown",
+    },
+
     /** Constants: Log Level Constants
      *  Logging level indicators.
      *
@@ -33882,7 +33902,7 @@ Strophe = {
      *
      *  This function is called whenever the Strophe library calls any
      *  of the logging functions.  The default implementation of this
-     *  function does nothing.  If client code wishes to handle the logging
+     *  function logs only fatal errors.  If client code wishes to handle the logging
      *  messages, it should override this with
      *  > Strophe.log = function (level, msg) {
      *  >   (user code here)
@@ -33906,11 +33926,13 @@ Strophe = {
      *      be one of the values in Strophe.LogLevel.
      *    (String) msg - The log message.
      */
-    /* jshint ignore:start */
     log: function (level, msg) {
-        return;
+        if (level === this.LogLevel.FATAL &&
+            typeof window.console === 'object' &&
+            typeof window.console.error === 'function') {
+            window.console.error(msg);
+        }
     },
-    /* jshint ignore:end */
 
     /** Function: debug
      *  Log a message at the Strophe.LogLevel.DEBUG level.
@@ -34576,10 +34598,11 @@ Strophe.TimedHandler.prototype = {
  *  If nothing is specified, then the following mechanisms (and their
  *  priorities) are registered:
  *
- *      OAUTHBEARER - 60
- *      SCRAM-SHA1 - 50
- *      DIGEST-MD5 - 40
- *      PLAIN - 30
+ *      SCRAM-SHA1 - 70
+ *      DIGEST-MD5 - 60
+ *      PLAIN - 50
+ *      OAUTH-BEARER - 40
+ *      OAUTH-2 - 30
  *      ANONYMOUS - 20
  *      EXTERNAL - 10
  *
@@ -35460,6 +35483,7 @@ Strophe.Connection.prototype = {
             Strophe.SASLExternal,
             Strophe.SASLMD5,
             Strophe.SASLOAuthBearer,
+            Strophe.SASLXOAuth2,
             Strophe.SASLPlain,
             Strophe.SASLSHA1
         ];
@@ -35526,8 +35550,9 @@ Strophe.Connection.prototype = {
      *    (Integer) status - the new connection status, one of the values
      *      in Strophe.Status
      *    (String) condition - the error condition or null
+     *    (XMLElement) elem - The triggering stanza.
      */
-    _changeConnectStatus: function (status, condition) {
+    _changeConnectStatus: function (status, condition, elem) {
         // notify all plugins listening for status changes
         for (var k in Strophe._connectionPlugins) {
             if (Strophe._connectionPlugins.hasOwnProperty(k)) {
@@ -35546,7 +35571,7 @@ Strophe.Connection.prototype = {
         // notify the user's callback
         if (this.connect_callback) {
             try {
-                this.connect_callback(status, condition);
+                this.connect_callback(status, condition, elem);
             } catch (e) {
                 Strophe._handleError(e);
                 Strophe.error(
@@ -35662,7 +35687,10 @@ Strophe.Connection.prototype = {
                 }
                 this._changeConnectStatus(Strophe.Status.CONNFAIL, cond);
             } else {
-                this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown");
+                this._changeConnectStatus(
+                    Strophe.Status.CONNFAIL,
+                    Strophe.ErrorCondition.UNKOWN_REASON
+                );
             }
             this._doDisconnect(cond);
             return;
@@ -35702,6 +35730,26 @@ Strophe.Connection.prototype = {
      */
     mechanisms: {},
 
+    /** PrivateFunction: _no_auth_received
+     *
+     * Called on stream start/restart when no stream:features
+     * has been received or when no viable authentication mechanism is offered.
+     *
+     * Sends a blank poll request.
+     */
+    _no_auth_received: function (_callback) {
+        var error_msg =  "Server did not offer a supported authentication mechanism";
+        Strophe.error(error_msg);
+        this._changeConnectStatus(
+            Strophe.Status.CONNFAIL,
+            Strophe.ErrorCondition.NO_AUTH_MECH
+        );
+        if (_callback) {
+            _callback.call(this);
+        }
+        this._doDisconnect();
+    },
+
     /** PrivateFunction: _connect_cb
      *  _Private_ handler for initial connection request.
      *
@@ -35715,7 +35763,7 @@ Strophe.Connection.prototype = {
      *  Parameters:
      *    (Strophe.Request) req - The current request.
      *    (Function) _callback - low level (xmpp) connect callback function.
-     *      Useful for plugins with their own xmpp connect callback (when their)
+     *      Useful for plugins with their own xmpp connect callback (when they
      *      want to do something special).
      */
     _connect_cb: function (req, _callback, raw) {
@@ -35727,8 +35775,11 @@ Strophe.Connection.prototype = {
             bodyWrap = this._proto._reqToData(req);
         } catch (e) {
             if (e !== "badformat") { throw e; }
-            this._changeConnectStatus(Strophe.Status.CONNFAIL, 'bad-format');
-            this._doDisconnect('bad-format');
+            this._changeConnectStatus(
+                Strophe.Status.CONNFAIL,
+                Strophe.ErrorCondition.BAD_FORMAT
+            );
+            this._doDisconnect(Strophe.ErrorCondition.BAD_FORMAT);
         }
         if (!bodyWrap) { return; }
 
@@ -35761,7 +35812,7 @@ Strophe.Connection.prototype = {
                             bodyWrap.getElementsByTagName("features").length > 0;
         }
         if (!hasFeatures) {
-            this._proto._no_auth_received(_callback);
+            this._no_auth_received(_callback);
             return;
         }
 
@@ -35777,7 +35828,7 @@ Strophe.Connection.prototype = {
             if (bodyWrap.getElementsByTagName("auth").length === 0) {
                 // There are no matching SASL mechanisms and also no legacy
                 // auth available.
-                this._proto._no_auth_received(_callback);
+                this._no_auth_received(_callback);
                 return;
             }
         }
@@ -35873,9 +35924,9 @@ Strophe.Connection.prototype = {
             // client connections
             this._changeConnectStatus(
                 Strophe.Status.CONNFAIL,
-                'x-strophe-bad-non-anon-jid'
+                Strophe.ErrorCondition.MISSING_JID_NODE
             );
-            this.disconnect('x-strophe-bad-non-anon-jid');
+            this.disconnect(Strophe.ErrorCondition.MISSING_JID_NODE);
         } else {
             // Fall back to legacy authentication
             this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
@@ -36093,9 +36144,9 @@ Strophe.Connection.prototype = {
             Strophe.info("SASL binding failed.");
             var conflict = elem.getElementsByTagName("conflict"), condition;
             if (conflict.length > 0) {
-                condition = 'conflict';
+                condition = Strophe.ErrorCondition.CONFLICT;
             }
-            this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition);
+            this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition, elem);
             return false;
         }
 
@@ -36122,7 +36173,7 @@ Strophe.Connection.prototype = {
             }
         } else {
             Strophe.info("SASL binding failed.");
-            this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+            this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem);
             return false;
         }
     },
@@ -36145,7 +36196,7 @@ Strophe.Connection.prototype = {
             this._changeConnectStatus(Strophe.Status.CONNECTED, null);
         } else if (elem.getAttribute("type") === "error") {
             Strophe.info("Session creation failed.");
-            this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+            this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem);
             return false;
         }
         return false;
@@ -36174,7 +36225,7 @@ Strophe.Connection.prototype = {
 
         if(this._sasl_mechanism)
           this._sasl_mechanism.onFailure();
-        this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+        this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem);
         return false;
     },
     /* jshint unused:true */
@@ -36196,7 +36247,7 @@ Strophe.Connection.prototype = {
             this.authenticated = true;
             this._changeConnectStatus(Strophe.Status.CONNECTED, null);
         } else if (elem.getAttribute("type") === "error") {
-            this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+            this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem);
             this.disconnect('authentication failed');
         }
         return false;
@@ -36453,6 +36504,7 @@ Strophe.SASLMechanism.prototype = {
    *  Strophe.SASLSHA1 - SASL SCRAM-SHA1 authentication
    *  Strophe.SASLOAuthBearer - SASL OAuth Bearer authentication
    *  Strophe.SASLExternal - SASL EXTERNAL authentication
+   *  Strophe.SASLXOAuth2 - SASL X-OAuth2 authentication
    */
 
 // Building SASL callbacks
@@ -36472,7 +36524,7 @@ Strophe.SASLAnonymous.prototype.test = function(connection) {
  *  SASL PLAIN authentication.
  */
 Strophe.SASLPlain = function() {};
-Strophe.SASLPlain.prototype = new Strophe.SASLMechanism("PLAIN", true, 30);
+Strophe.SASLPlain.prototype = new Strophe.SASLMechanism("PLAIN", true, 50);
 
 Strophe.SASLPlain.prototype.test = function(connection) {
     return connection.authcid !== null;
@@ -36492,7 +36544,7 @@ Strophe.SASLPlain.prototype.onChallenge = function(connection) {
  *  SASL SCRAM SHA 1 authentication.
  */
 Strophe.SASLSHA1 = function() {};
-Strophe.SASLSHA1.prototype = new Strophe.SASLMechanism("SCRAM-SHA-1", true, 50);
+Strophe.SASLSHA1.prototype = new Strophe.SASLMechanism("SCRAM-SHA-1", true, 70);
 
 Strophe.SASLSHA1.prototype.test = function(connection) {
     return connection.authcid !== null;
@@ -36576,7 +36628,7 @@ Strophe.SASLSHA1.prototype.onChallenge = function(connection, challenge, test_cn
  *  SASL DIGEST MD5 authentication.
  */
 Strophe.SASLMD5 = function() {};
-Strophe.SASLMD5.prototype = new Strophe.SASLMechanism("DIGEST-MD5", false, 40);
+Strophe.SASLMD5.prototype = new Strophe.SASLMechanism("DIGEST-MD5", false, 60);
 
 Strophe.SASLMD5.prototype.test = function(connection) {
     return connection.authcid !== null;
@@ -36659,7 +36711,7 @@ Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cno
  *  SASL OAuth Bearer authentication.
  */
 Strophe.SASLOAuthBearer = function() {};
-Strophe.SASLOAuthBearer.prototype = new Strophe.SASLMechanism("OAUTHBEARER", true, 60);
+Strophe.SASLOAuthBearer.prototype = new Strophe.SASLMechanism("OAUTHBEARER", true, 40);
 
 Strophe.SASLOAuthBearer.prototype.test = function(connection) {
     return connection.pass !== null;
@@ -36703,6 +36755,29 @@ Strophe.SASLExternal.prototype.onChallenge = function(connection) {
     return connection.authcid === connection.authzid ? '' : connection.authzid;
 };
 
+
+/** PrivateConstructor: SASLXOAuth2
+ *  SASL X-OAuth2 authentication.
+ */
+Strophe.SASLXOAuth2 = function () { };
+Strophe.SASLXOAuth2.prototype = new Strophe.SASLMechanism("X-OAUTH2", true, 30);
+
+Strophe.SASLXOAuth2.prototype.test = function (connection) {
+    return connection.pass !== null;
+};
+
+Strophe.SASLXOAuth2.prototype.onChallenge = function (connection) {
+    var auth_str = '\u0000';
+    if (connection.authcid !== null) {
+        auth_str = auth_str + connection.authzid;
+    }
+    auth_str = auth_str + "\u0000";
+    auth_str = auth_str + connection.pass;
+
+    return utils.utf16to8(auth_str);
+};
+
+
 return {
     'Strophe':         Strophe,
     '$build':          $build,
@@ -36736,6 +36811,10 @@ return {
                 core.$build
             );
         });
+    } else if (typeof exports === 'object') {
+        var core = require('./core');
+
+        module.exports = factory(core.Strophe, core.$build);
     } else {
         // Browser globals
         return factory(Strophe, $build);
@@ -36813,11 +36892,17 @@ Strophe.Request.prototype = {
                 throw "parsererror";
             }
         } else if (this.xhr.responseText) {
-            Strophe.error("invalid response received");
-            Strophe.error("responseText: " + this.xhr.responseText);
-            throw "badformat";
+            // In React Native, we may get responseText but no responseXML.  We can try to parse it manually.
+            Strophe.debug("Got responseText but no responseXML; attempting to parse it with DOMParser...");
+            node = new DOMParser().parseFromString(this.xhr.responseText, 'application/xml').documentElement;
+            if (!node) {
+                throw new Error('Parsing produced null node');
+            } else if (node.querySelector('parsererror')) {
+                Strophe.error("invalid response received: " + node.querySelector('parsererror').textContent);
+                Strophe.error("responseText: " + this.xhr.responseText);
+                throw "badformat";
+            }
         }
-
         return node;
     },
 
@@ -36883,6 +36968,8 @@ Strophe.Bosh = function(connection) {
     this.errors = 0;
     this.inactivity = null;
 
+    this.lastResponseHeaders = null;
+
     this._requests = [];
 };
 
@@ -37190,26 +37277,6 @@ Strophe.Bosh.prototype = {
         }
     },
 
-    /** PrivateFunction: _no_auth_received
-     *
-     * Called on stream start/restart when no stream:features
-     * has been received and sends a blank poll request.
-     */
-    _no_auth_received: function (_callback) {
-        if (_callback) {
-            _callback = _callback.bind(this._conn);
-        } else {
-            _callback = this._conn._connect_cb.bind(this._conn);
-        }
-        var body = this._buildBody();
-        this._requests.push(
-                new Strophe.Request(body.tree(),
-                    this._onRequestStateChange.bind(
-                        this, _callback.bind(this._conn)),
-                    body.tree().getAttribute("rid")));
-        this._throttledRequestHandler();
-    },
-
     /** PrivateFunction: _onDisconnectTimeout
      *  _Private_ timeout handler for handling non-graceful disconnection.
      *
@@ -37350,6 +37417,7 @@ Strophe.Bosh.prototype = {
             return;
         }
         var reqStatus = this._getRequestStatus(req);
+        this.lastResponseHeaders = req.xhr.getAllResponseHeaders();
         if (this.disconnecting && reqStatus >= 400) {
             this._hitError(reqStatus);
             this._callProtocolErrorHandlers(req);
@@ -37674,6 +37742,10 @@ return Strophe;
                 core.$build
             );
         });
+    } else if (typeof exports === 'object') {
+        var core = require('./core');
+
+        module.exports = factory(core.Strophe, core.$build);
     } else {
         // Browser globals
         return factory(Strophe, $build);
@@ -37948,7 +38020,7 @@ Strophe.Websocket.prototype = {
                 this._conn.send(pres);
             }
             var close = $build("close", { "xmlns": Strophe.NS.FRAMING });
-            this._conn.xmlOutput(close);
+            this._conn.xmlOutput(close.tree());
             var closeString = Strophe.serialize(close);
             this._conn.rawOutput(closeString);
             try {
@@ -37986,6 +38058,7 @@ Strophe.Websocket.prototype = {
      */
     _closeSocket: function () {
         if (this.socket) { try {
+            this.socket.onerror = null;
             this.socket.close();
         } catch (e) {} }
         this.socket = null;
@@ -38026,24 +38099,6 @@ Strophe.Websocket.prototype = {
         }
     },
 
-    /** PrivateFunction: _no_auth_received
-     *
-     * Called on stream start/restart when no stream:features
-     * has been received.
-     */
-    _no_auth_received: function (_callback) {
-        Strophe.error("Server did not send any auth methods");
-        this._conn._changeConnectStatus(
-            Strophe.Status.CONNFAIL,
-            "Server did not send any auth methods"
-        );
-        if (_callback) {
-            _callback = _callback.bind(this._conn);
-            _callback();
-        }
-        this._conn._doDisconnect();
-    },
-
     /** PrivateFunction: _onDisconnectTimeout
      *  _Private_ timeout handler for handling non-graceful disconnection.
      *
@@ -38219,6 +38274,11 @@ return Strophe;
         ], function (wrapper) {
             return wrapper;
         });
+    } else if (typeof exports === 'object') {
+        var core = require('./core');
+        require('./bosh');
+        require('./websocket');
+        module.exports = core;
     }
 })(this);
 
@@ -43335,7 +43395,7 @@ return Backbone.BrowserStorage;
 // Converse.js
 // https://conversejs.org
 //
-// Copyright (c) 2012-2018, the Converse.js developers
+// Copyright (c) 2013-2018, the Converse.js developers
 // Licensed under the Mozilla Public License (MPLv2)
 (function (root, factory) {
   define('converse-core',["sizzle", "es6-promise", "lodash.noconflict", "lodash.fp", "polyfill", "i18n", "utils", "moment", "strophe", "pluggable", "backbone.noconflict", "backbone.nativeview", "backbone.browserStorage"], factory);
@@ -44002,6 +44062,8 @@ return Backbone.BrowserStorage;
       _converse.session.browserStorage = new Backbone.BrowserStorage[_converse.storage](id);
 
       _converse.session.fetch();
+
+      _converse.emit('sessionInitialized');
     };
 
     this.clearSession = function () {
@@ -45661,15 +45723,15 @@ define("emojione", (function (global) {
         initialize: function initialize() {
           this.waitUntilFeaturesDiscovered = utils.getResolveablePromise();
           this.dataforms = new Backbone.Collection();
-          this.dataforms.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.dataforms-{this.get('jid')}"));
+          this.dataforms.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.dataforms-{this.get('jid')}"));
           this.features = new Backbone.Collection();
-          this.features.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.features-".concat(this.get('jid'))));
+          this.features.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.features-".concat(this.get('jid'))));
           this.features.on('add', this.onFeatureAdded, this);
           this.identities = new Backbone.Collection();
-          this.identities.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.identities-".concat(this.get('jid'))));
+          this.identities.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.identities-".concat(this.get('jid'))));
           this.fetchFeatures();
           this.items = new _converse.DiscoEntities();
-          this.items.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.disco-items-".concat(this.get('jid'))));
+          this.items.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.disco-items-".concat(this.get('jid'))));
           this.items.fetch();
         },
         getIdentity: function getIdentity(category, type) {
@@ -45850,13 +45912,33 @@ define("emojione", (function (global) {
         return this;
       }
 
+      function initStreamFeatures() {
+        _converse.stream_features = new Backbone.Collection();
+        _converse.stream_features.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.stream-features-".concat(_converse.bare_jid)));
+
+        _converse.stream_features.fetch({
+          success: function success(collection) {
+            if (collection.length === 0 && _converse.connection.features) {
+              _.forEach(_converse.connection.features.childNodes, function (feature) {
+                _converse.stream_features.create({
+                  'name': feature.nodeName,
+                  'xmlns': feature.getAttribute('xmlns')
+                });
+              });
+            }
+          }
+        });
+
+        _converse.emit('streamFeaturesAdded');
+      }
+
       function initializeDisco() {
         addClientFeatures();
 
         _converse.connection.addHandler(onDiscoInfoRequest, Strophe.NS.DISCO_INFO, 'iq', 'get', null, null);
 
         _converse.disco_entities = new _converse.DiscoEntities();
-        _converse.disco_entities.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.disco-entities-".concat(_converse.bare_jid)));
+        _converse.disco_entities.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.disco-entities-".concat(_converse.bare_jid)));
 
         _converse.disco_entities.fetchEntities().then(function (collection) {
           if (collection.length === 0 || !collection.get(_converse.domain)) {
@@ -45871,6 +45953,8 @@ define("emojione", (function (global) {
         }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
       }
 
+      _converse.api.listen.on('sessionInitialized', initStreamFeatures);
+
       _converse.api.listen.on('reconnected', initializeDisco);
 
       _converse.api.listen.on('connected', initializeDisco);
@@ -45951,6 +46035,19 @@ define("emojione", (function (global) {
          * @namespace
          */
         'disco': {
+          'stream': {
+            'getFeature': function getFeature(name, xmlns) {
+              if (_.isNil(name) || _.isNil(xmlns)) {
+                throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature");
+              }
+
+              return _converse.stream_features.findWhere({
+                'name': name,
+                'xmlns': xmlns
+              });
+            }
+          },
+
           /**
            * The "own" grouping
            * @namespace
@@ -48627,19 +48724,24 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
           this.on('change:image_hash', this.onAvatarChanged, this);
         },
         onAvatarChanged: function onAvatarChanged() {
-          var vcard = _converse.vcards.findWhere({
-            'jid': this.get('from')
-          });
+          var hash = this.get('image_hash');
+          var vcards = [];
 
-          if (!vcard) {
-            return;
+          if (this.get('jid')) {
+            vcards.push(this.updateVCard(_converse.vcards.findWhere({
+              'jid': this.get('jid')
+            })));
           }
 
-          var hash = this.get('image_hash');
+          vcards.push(this.updateVCard(_converse.vcards.findWhere({
+            'jid': this.get('from')
+          })));
 
-          if (hash && vcard.get('image_hash') !== hash) {
-            _converse.api.vcard.update(vcard);
-          }
+          _.forEach(_.filter(vcards, undefined), function (vcard) {
+            if (hash && vcard.get('image_hash') !== hash) {
+              _converse.api.vcard.update(vcard);
+            }
+          });
         },
         getDisplayName: function getDisplayName() {
           return this.get('nick') || this.get('jid');
@@ -48687,6 +48789,10 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
                 'jid': removed_jid
               });
 
+              if (!occupant) {
+                return;
+              }
+
               if (occupant.get('show') === 'offline') {
                 occupant.destroy();
               }
@@ -60751,7 +60857,7 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
 // Converse.js
 // http://conversejs.org
 //
-// Copyright (c) 2012-2018, the Converse.js developers
+// Copyright (c) 2013-2018, the Converse.js developers
 // Licensed under the Mozilla Public License (MPLv2)
 (function (root, factory) {
   define('converse-vcard',["converse-core", "crypto", "tpl!vcard"], factory);
@@ -60824,7 +60930,9 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
             'image_type': _.get(vcard.querySelector('PHOTO TYPE'), 'textContent'),
             'url': _.get(vcard.querySelector('URL'), 'textContent'),
             'role': _.get(vcard.querySelector('ROLE'), 'textContent'),
-            'email': _.get(vcard.querySelector('EMAIL USERID'), 'textContent')
+            'email': _.get(vcard.querySelector('EMAIL USERID'), 'textContent'),
+            'vcard_updated': moment().format(),
+            'vcard_error': undefined
           };
         }
 
@@ -60842,7 +60950,8 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
         if (errback) {
           errback({
             'stanza': iq,
-            'jid': jid
+            'jid': jid,
+            'vcard_error': moment().format()
           });
         }
       }
@@ -60908,8 +61017,8 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
           'get': function get(model, force) {
             if (_.isString(model)) {
               return getVCard(_converse, model);
-            } else if (!model.get('vcard_updated') || force) {
-              var jid = model.get('jid') || model.get('muc_jid');
+            } else if (force || !model.get('vcard_updated') || !moment(model.get('vcard_error')).isSame(new Date(), "day")) {
+              var jid = model.get('jid');
 
               if (!jid) {
                 throw new Error("No JID to get vcard for!");
@@ -60925,9 +61034,8 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat
 
             return new Promise(function (resolve, reject) {
               _this.get(model, force).then(function (vcard) {
-                model.save(_.extend(_.pick(vcard, ['fullname', 'nickname', 'email', 'url', 'role', 'image_type', 'image', 'image_hash']), {
-                  'vcard_updated': moment().format()
-                }));
+                delete vcard['stanza'];
+                model.save(vcard);
                 resolve();
               });
             });
@@ -72046,6 +72154,13 @@ return __p
         */
         _converse.roster = new _converse.RosterContacts();
         _converse.roster.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.contacts-".concat(_converse.bare_jid)));
+        _converse.roster.data = new Backbone.Model();
+        var id = b64_sha1("converse-roster-model-".concat(_converse.bare_jid));
+        _converse.roster.data.id = id;
+        _converse.roster.data.browserStorage = new Backbone.BrowserStorage[_converse.storage](id);
+
+        _converse.roster.data.fetch();
+
         _converse.rostergroups = new _converse.RosterGroups();
         _converse.rostergroups.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.roster.groups".concat(_converse.bare_jid)));
 
@@ -72350,23 +72465,27 @@ return __p
         },
         onConnected: function onConnected() {
           /* Called as soon as the connection has been established
-          * (either after initial login, or after reconnection).
-          *
-          * Use the opportunity to register stanza handlers.
-          */
+           * (either after initial login, or after reconnection).
+           *
+           * Use the opportunity to register stanza handlers.
+           */
           this.registerRosterHandler();
           this.registerRosterXHandler();
         },
         registerRosterHandler: function registerRosterHandler() {
           /* Register a handler for roster IQ "set" stanzas, which update
-          * roster contacts.
-          */
-          _converse.connection.addHandler(_converse.roster.onRosterPush.bind(_converse.roster), Strophe.NS.ROSTER, 'iq', "set");
+           * roster contacts.
+           */
+          _converse.connection.addHandler(function (iq) {
+            _converse.roster.onRosterPush(iq);
+
+            return true;
+          }, Strophe.NS.ROSTER, 'iq', "set");
         },
         registerRosterXHandler: function registerRosterXHandler() {
           /* Register a handler for RosterX message stanzas, which are
-          * used to suggest roster contacts to a user.
-          */
+           * used to suggest roster contacts to a user.
+           */
           var t = 0;
 
           _converse.connection.addHandler(function (msg) {
@@ -72389,12 +72508,13 @@ return __p
            * Returns a promise which resolves once the contacts have been
            * fetched.
            */
+          var that = this;
           return new Promise(function (resolve, reject) {
             _this2.fetch({
               'add': true,
               'silent': true,
               success: function success(collection) {
-                if (collection.length === 0) {
+                if (collection.length === 0 || that.rosterVersioningSupported() && !_converse.session.get('roster_fetched')) {
                   _converse.send_initial_presence = true;
 
                   _converse.roster.fetchFromServer().then(resolve).catch(reject);
@@ -72536,30 +72656,22 @@ return __p
         },
         onRosterPush: function onRosterPush(iq) {
           /* Handle roster updates from the XMPP server.
-          * See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
-          *
-          * Parameters:
-          *    (XMLElement) IQ - The IQ stanza received from the XMPP server.
-          */
+           * See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
+           *
+           * Parameters:
+           *    (XMLElement) IQ - The IQ stanza received from the XMPP server.
+           */
           var id = iq.getAttribute('id');
           var from = iq.getAttribute('from');
 
-          if (from && from !== "" && Strophe.getBareJidFromJid(from) !== _converse.bare_jid) {
-            // Receiving client MUST ignore stanza unless it has no from or from = user's bare JID.
-            // XXX: Some naughty servers apparently send from a full
-            // JID so we need to explicitly compare bare jids here.
-            // https://github.com/jcbrand/converse.js/issues/493
-            _converse.connection.send($iq({
-              type: 'error',
-              id: id,
-              from: _converse.connection.jid
-            }).c('error', {
-              'type': 'cancel'
-            }).c('service-unavailable', {
-              'xmlns': Strophe.NS.ROSTER
-            }));
-
-            return true;
+          if (from && from !== _converse.connection.jid) {
+            // https://tools.ietf.org/html/rfc6121#page-15
+            // 
+            // A receiving client MUST ignore the stanza unless it has no 'from'
+            // attribute (i.e., implicitly from the bare JID of the user's
+            // account) or it has a 'from' attribute whose value matches the
+            // user's bare JID <user@domainpart>.
+            return;
           }
 
           _converse.connection.send($iq({
@@ -72568,13 +72680,32 @@ return __p
             from: _converse.connection.jid
           }));
 
-          var items = sizzle("query[xmlns=\"".concat(Strophe.NS.ROSTER, "\"] item"), iq);
+          var query = sizzle("query[xmlns=\"".concat(Strophe.NS.ROSTER, "\"]"), iq).pop();
+          this.data.save('version', query.getAttribute('ver'));
+          var items = sizzle("item", query);
+
+          if (items.length > 1) {
+            _converse.log(iq, Strophe.LogLevel.ERROR);
+
+            throw new Error('Roster push query may not contain more than one "item" element.');
+          }
+
+          if (items.length === 0) {
+            _converse.log(iq, Strophe.LogLevel.WARN);
+
+            _converse.log('Received a roster push stanza without an "item" element.', Strophe.LogLevel.WARN);
+
+            return;
+          }
 
-          _.each(items, this.updateContact.bind(this));
+          this.updateContact(items.pop());
 
           _converse.emit('rosterPush', iq);
 
-          return true;
+          return;
+        },
+        rosterVersioningSupported: function rosterVersioningSupported() {
+          return _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') && this.data.get('version');
         },
         fetchFromServer: function fetchFromServer() {
           var _this4 = this;
@@ -72588,6 +72719,12 @@ return __p
               xmlns: Strophe.NS.ROSTER
             });
 
+            if (_this4.rosterVersioningSupported()) {
+              iq.attrs({
+                'ver': _this4.data.get('version')
+              });
+            }
+
             var callback = _.flow(_this4.onReceivedFromServer.bind(_this4), resolve);
 
             var errback = function errback(iq) {
@@ -72602,19 +72739,31 @@ return __p
           });
         },
         onReceivedFromServer: function onReceivedFromServer(iq) {
+          var _this5 = this;
+
           /* An IQ stanza containing the roster has been received from
-          * the XMPP server.
-          */
-          var items = sizzle("query[xmlns=\"".concat(Strophe.NS.ROSTER, "\"] item"), iq);
+           * the XMPP server.
+           */
+          var query = sizzle("query[xmlns=\"".concat(Strophe.NS.ROSTER, "\"]"), iq).pop();
+
+          if (query) {
+            var items = sizzle("item", query);
+
+            _.each(items, function (item) {
+              return _this5.updateContact(item);
+            });
+
+            this.data.save('version', query.getAttribute('ver'));
 
-          _.each(items, this.updateContact.bind(this));
+            _converse.session.save('roster_fetched', true);
+          }
 
           _converse.emit('roster', iq);
         },
         updateContact: function updateContact(item) {
           /* Update or create RosterContact models based on items
-          * received in the IQ from the server.
-          */
+           * received in the IQ from the server.
+           */
           var jid = item.getAttribute('jid');
 
           if (this.isSelf(jid)) {
@@ -72795,7 +72944,7 @@ return __p
       _converse.RosterGroups = Backbone.Collection.extend({
         model: _converse.RosterGroup,
         fetchRosterGroups: function fetchRosterGroups() {
-          var _this5 = this;
+          var _this6 = this;
 
           /* Fetches all the roster groups from sessionStorage.
           *
@@ -72803,7 +72952,7 @@ return __p
           * returned.
           */
           return new Promise(function (resolve, reject) {
-            _this5.fetch({
+            _this6.fetch({
               silent: true,
               // We need to first have all groups before
               // we can start positioning them, so we set