diff --git a/dist/converse-no-dependencies.js b/dist/converse-no-dependencies.js index 4199f8237..4e4b72909 100644 --- a/dist/converse-no-dependencies.js +++ b/dist/converse-no-dependencies.js @@ -9631,7 +9631,7 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat /*global define, escape, window */ (function (root, factory) { if (typeof define === 'function' && define.amd) { - define('utils',["sizzle", "es6-promise", "lodash.noconflict", "strophe", "uri", "tpl!audio", "tpl!file", "tpl!image", "tpl!video"], factory); + define('utils',["sizzle", "es6-promise", "lodash.noconflict", "backbone", "strophe", "uri", "tpl!audio", "tpl!file", "tpl!image", "tpl!video"], factory); } else { // Used by the mockups var Strophe = { @@ -9649,7 +9649,7 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat }; root.converse_utils = factory(root.sizzle, root.Promise, root._, Strophe); } -})(this, function (sizzle, Promise, _, Strophe, URI, tpl_audio, tpl_file, tpl_image, tpl_video) { +})(this, function (sizzle, Promise, _, Backbone, Strophe, URI, tpl_audio, tpl_file, tpl_image, tpl_video) { "use strict"; var b64_sha1 = Strophe.SHA1.b64_sha1; @@ -10105,6 +10105,14 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat } }; + u.isOnlyChatStateNotification = function (attrs) { + if (attrs instanceof Backbone.Model) { + attrs = attrs.attributes; + } + + return attrs['chat_state'] && !attrs['oob_url'] && !attrs['file'] && !attrs['message']; + }; + u.isOTRMessage = function (message) { var body = message.querySelector('body'), text = !_.isNull(body) ? body.textContent : undefined; @@ -10762,7 +10770,8 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm'); Strophe.addNamespace('SID', 'urn:xmpp:sid:0'); Strophe.addNamespace('SPOILER', 'urn:xmpp:spoiler:0'); - Strophe.addNamespace('XFORM', 'jabber:x:data'); // Use Mustache style syntax for variable interpolation + Strophe.addNamespace('XFORM', 'jabber:x:data'); + Strophe.addNamespace('VCARDUPDATE', 'vcard-temp:x:update'); // Use Mustache style syntax for variable interpolation /* Configuration of Lodash templates (this config is distinct to the * config of requirejs-tpl in main.js). This one is for normal inline templates. @@ -10784,7 +10793,7 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat _.extend(_converse, Backbone.Events); // Core plugins are whitelisted automatically - _converse.core_plugins = ['converse-bookmarks', 'converse-chatboxes', 'converse-chatview', 'converse-controlbox', 'converse-core', 'converse-disco', 'converse-dragresize', 'converse-dropdown', 'converse-fullscreen', 'converse-headline', 'converse-http-file-upload', 'converse-mam', 'converse-message-view', 'converse-minimize', 'converse-modal', 'converse-muc', 'converse-muc-embedded', 'converse-muc-views', 'converse-notification', 'converse-otr', 'converse-ping', 'converse-profile', 'converse-register', 'converse-roomslist', 'converse-rosterview', 'converse-singleton', 'converse-spoilers', 'converse-vcard']; // Make converse pluggable + _converse.core_plugins = ['converse-bookmarks', 'converse-chatboxes', 'converse-chatview', 'converse-controlbox', 'converse-core', 'converse-disco', 'converse-dragresize', 'converse-embedded', 'converse-fullscreen', 'converse-headline', 'converse-mam', 'converse-message-view', 'converse-minimize', 'converse-modal', 'converse-muc', 'converse-muc-views', 'converse-notification', 'converse-otr', 'converse-ping', 'converse-profile', 'converse-register', 'converse-roomslist', 'converse-roster', 'converse-rosterview', 'converse-singleton', 'converse-spoilers', 'converse-vcard']; // Make converse pluggable pluggable.enable(_converse, '_converse', 'pluggable'); // Module-level constants @@ -10904,7 +10913,7 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat }; var __ = _converse.__; - var PROMISES = ['initialized', 'cachedRoster', 'connectionInitialized', 'pluginsInitialized', 'roster', 'rosterContactsFetched', 'rosterGroupsFetched', 'rosterInitialized', 'statusInitialized']; + var PROMISES = ['initialized', 'connectionInitialized', 'pluginsInitialized', 'statusInitialized']; function addPromise(promise) { /* Private function, used to add a new promise to the ones already @@ -10925,6 +10934,32 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat }; _converse.router = new Backbone.Router(); + _converse.ModelWithDefaultAvatar = Backbone.Model.extend({ + defaults: { + 'image': _converse.DEFAULT_IMAGE, + 'image_type': _converse.DEFAULT_IMAGE_TYPE + }, + set: function set(key, val, options) { + // Override Backbone.Model.prototype.set to make sure that the + // default `image` and `image_type` values are maintained. + var attrs; + + if (_typeof(key) === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + if (_.has(attrs, 'image') && _.isUndefined(attrs['image'])) { + attrs['image'] = _converse.DEFAULT_IMAGE; + attrs['image_type'] = _converse.DEFAULT_IMAGE_TYPE; + return Backbone.Model.prototype.set.call(this, attrs, options); + } else { + return Backbone.Model.prototype.set.apply(this, arguments); + } + } + }); _converse.initialize = function (settings, callback) { "use strict"; @@ -11028,7 +11063,6 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat registration_domain: '', rid: undefined, root: window.document, - roster_groups: true, show_only_online_users: false, show_send_button: false, sid: undefined, @@ -11394,10 +11428,6 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat }; this.clearSession = function () { - if (!_.isUndefined(this.roster)) { - this.roster.browserStorage._clear(); - } - if (!_.isUndefined(this.session) && this.session.browserStorage) { this.session.browserStorage._clear(); } @@ -11512,58 +11542,6 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat this.connection.send(carbons_iq); }; - this.initRoster = function () { - /* Initialize the Bakcbone collections that represent the contats - * roster and the roster groups. - */ - _converse.roster = new _converse.RosterContacts(); - _converse.roster.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.contacts-".concat(_converse.bare_jid))); - _converse.rostergroups = new _converse.RosterGroups(); - _converse.rostergroups.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.roster.groups".concat(_converse.bare_jid))); - - _converse.emit('rosterInitialized'); - }; - - this.populateRoster = function () { - var ignore_cache = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; - - /* Fetch all the roster groups, and then the roster contacts. - * Emit an event after fetching is done in each case. - * - * Parameters: - * (Bool) ignore_cache - If set to to true, the local cache - * will be ignored it's guaranteed that the XMPP server - * will be queried for the roster. - */ - if (ignore_cache) { - _converse.send_initial_presence = true; - - _converse.roster.fetchFromServer().then(function () { - _converse.emit('rosterContactsFetched'); - - _converse.sendInitialPresence(); - }).catch(function (reason) { - _converse.log(reason, Strophe.LogLevel.ERROR); - - _converse.sendInitialPresence(); - }); - } else { - _converse.rostergroups.fetchRosterGroups().then(function () { - _converse.emit('rosterGroupsFetched'); - - return _converse.roster.fetchRosterContacts(); - }).then(function () { - _converse.emit('rosterContactsFetched'); - - _converse.sendInitialPresence(); - }).catch(function (reason) { - _converse.log(reason, Strophe.LogLevel.ERROR); - - _converse.sendInitialPresence(); - }); - } - }; - this.unregisterPresenceHandler = function () { if (!_.isUndefined(_converse.presence_ref)) { _converse.connection.deleteHandler(_converse.presence_ref); @@ -11572,16 +11550,6 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat } }; - this.registerPresenceHandler = function () { - _converse.unregisterPresenceHandler(); - - _converse.presence_ref = _converse.connection.addHandler(function (presence) { - _converse.roster.presenceHandler(presence); - - return true; - }, null, 'presence', null); - }; - this.sendInitialPresence = function () { if (_converse.send_initial_presence) { _converse.xmppstatus.sendPresence(); @@ -11589,29 +11557,7 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat }; this.onStatusInitialized = function (reconnecting) { - /* Continue with session establishment (e.g. fetching chat boxes, - * populating the roster etc.) necessary once the connection has - * been established. - */ - _converse.emit('statusInitialized'); - - if (reconnecting) { - // No need to recreate the roster, otherwise we lose our - // cached data. However we still emit an event, to give - // event handlers a chance to register views for the - // roster and its groups, before we start populating. - _converse.emit('rosterReadyAfterReconnection'); - } else { - _converse.registerIntervalHandler(); - - _converse.initRoster(); - } - - _converse.roster.onConnected(); - - _converse.populateRoster(reconnecting); - - _converse.registerPresenceHandler(); + _converse.emit('statusInitialized', reconnecting); if (reconnecting) { _converse.emit('reconnected'); @@ -11647,682 +11593,6 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat _converse.initStatus(reconnecting); }; - this.RosterContact = Backbone.Model.extend({ - defaults: { - 'chat_state': undefined, - 'chat_status': 'offline', - 'image': _converse.DEFAULT_IMAGE, - 'image_type': _converse.DEFAULT_IMAGE_TYPE, - 'num_unread': 0, - 'status': '' - }, - initialize: function initialize(attributes) { - var jid = attributes.jid, - bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(), - resource = Strophe.getResourceFromJid(jid); - attributes.jid = bare_jid; - this.set(_.assignIn({ - 'fullname': bare_jid, - 'groups': [], - 'id': bare_jid, - 'jid': bare_jid, - 'resources': {}, - 'user_id': Strophe.getNodeFromJid(jid) - }, attributes)); - this.on('change:chat_status', function (item) { - _converse.emit('contactStatusChanged', item.attributes); - }); - }, - subscribe: function subscribe(message) { - /* Send a presence subscription request to this roster contact - * - * Parameters: - * (String) message - An optional message to explain the - * reason for the subscription request. - */ - var pres = $pres({ - to: this.get('jid'), - type: "subscribe" - }); - - if (message && message !== "") { - pres.c("status").t(message).up(); - } - - var nick = _converse.xmppstatus.get('nickname') || _converse.xmppstatus.get('fullname'); - - if (nick) { - pres.c('nick', { - 'xmlns': Strophe.NS.NICK - }).t(nick).up(); - } - - _converse.connection.send(pres); - - this.save('ask', "subscribe"); // ask === 'subscribe' Means we have asked to subscribe to them. - - return this; - }, - ackSubscribe: function ackSubscribe() { - /* Upon receiving the presence stanza of type "subscribed", - * the user SHOULD acknowledge receipt of that subscription - * state notification by sending a presence stanza of type - * "subscribe" to the contact - */ - _converse.connection.send($pres({ - 'type': 'subscribe', - 'to': this.get('jid') - })); - }, - ackUnsubscribe: function ackUnsubscribe() { - /* Upon receiving the presence stanza of type "unsubscribed", - * the user SHOULD acknowledge receipt of that subscription state - * notification by sending a presence stanza of type "unsubscribe" - * this step lets the user's server know that it MUST no longer - * send notification of the subscription state change to the user. - * Parameters: - * (String) jid - The Jabber ID of the user who is unsubscribing - */ - _converse.connection.send($pres({ - 'type': 'unsubscribe', - 'to': this.get('jid') - })); - - this.removeFromRoster(); - this.destroy(); - }, - unauthorize: function unauthorize(message) { - /* Unauthorize this contact's presence subscription - * Parameters: - * (String) message - Optional message to send to the person being unauthorized - */ - _converse.rejectPresenceSubscription(this.get('jid'), message); - - return this; - }, - authorize: function authorize(message) { - /* Authorize presence subscription - * Parameters: - * (String) message - Optional message to send to the person being authorized - */ - var pres = $pres({ - 'to': this.get('jid'), - 'type': "subscribed" - }); - - if (message && message !== "") { - pres.c("status").t(message); - } - - _converse.connection.send(pres); - - return this; - }, - addResource: function addResource(presence) { - /* Adds a new resource and it's associated attributes as taken - * from the passed in presence stanza. - * - * Also updates the contact's chat_status if the presence has - * higher priority (and is newer). - */ - var jid = presence.getAttribute('from'), - chat_status = _.propertyOf(presence.querySelector('show'))('textContent') || 'online', - resource = Strophe.getResourceFromJid(jid), - delay = presence.querySelector("delay[xmlns=\"".concat(Strophe.NS.DELAY, "\"]")), - timestamp = _.isNull(delay) ? moment().format() : moment(delay.getAttribute('stamp')).format(); - var priority = _.propertyOf(presence.querySelector('priority'))('textContent') || 0; - priority = _.isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10); - var resources = _.isObject(this.get('resources')) ? this.get('resources') : {}; - resources[resource] = { - 'name': resource, - 'priority': priority, - 'status': chat_status, - 'timestamp': timestamp - }; - var changed = { - 'resources': resources - }; - var hpr = this.getHighestPriorityResource(); - - if (priority == hpr.priority && timestamp == hpr.timestamp) { - // Only set the chat status if this is the newest resource - // with the highest priority - changed.chat_status = chat_status; - } - - this.save(changed); - return resources; - }, - removeResource: function removeResource(resource) { - /* Remove the passed in resource from the contact's resources map. - * - * Also recomputes the chat_status given that there's one less - * resource. - */ - var resources = this.get('resources'); - - if (!_.isObject(resources)) { - resources = {}; - } else { - delete resources[resource]; - } - - this.save({ - 'resources': resources, - 'chat_status': _.propertyOf(this.getHighestPriorityResource())('status') || 'offline' - }); - }, - getHighestPriorityResource: function getHighestPriorityResource() { - /* Return the resource with the highest priority. - * - * If multiple resources have the same priority, take the - * newest one. - */ - var resources = this.get('resources'); - - if (_.isObject(resources) && _.size(resources)) { - var val = _.flow(_.values, _.partial(_.sortBy, _, ['priority', 'timestamp']), _.reverse)(resources)[0]; - - if (!_.isUndefined(val)) { - return val; - } - } - }, - removeFromRoster: function removeFromRoster(callback, errback) { - /* Instruct the XMPP server to remove this contact from our roster - * Parameters: - * (Function) callback - */ - var iq = $iq({ - type: 'set' - }).c('query', { - xmlns: Strophe.NS.ROSTER - }).c('item', { - jid: this.get('jid'), - subscription: "remove" - }); - - _converse.connection.sendIQ(iq, callback, errback); - - return this; - } - }); - this.RosterContacts = Backbone.Collection.extend({ - model: _converse.RosterContact, - comparator: function comparator(contact1, contact2) { - var status1 = contact1.get('chat_status') || 'offline'; - var status2 = contact2.get('chat_status') || 'offline'; - - if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) { - var name1 = contact1.get('fullname').toLowerCase(); - var name2 = contact2.get('fullname').toLowerCase(); - return name1 < name2 ? -1 : name1 > name2 ? 1 : 0; - } else { - return _converse.STATUS_WEIGHTS[status1] < _converse.STATUS_WEIGHTS[status2] ? -1 : 1; - } - }, - 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. - */ - 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"); - }, - registerRosterXHandler: function registerRosterXHandler() { - /* Register a handler for RosterX message stanzas, which are - * used to suggest roster contacts to a user. - */ - var t = 0; - - _converse.connection.addHandler(function (msg) { - window.setTimeout(function () { - _converse.connection.flush(); - - _converse.roster.subscribeToSuggestedItems.bind(_converse.roster)(msg); - }, t); - t += msg.querySelectorAll('item').length * 250; - return true; - }, Strophe.NS.ROSTERX, 'message', null); - }, - fetchRosterContacts: function fetchRosterContacts() { - var _this3 = this; - - /* Fetches the roster contacts, first by trying the - * sessionStorage cache, and if that's empty, then by querying - * the XMPP server. - * - * Returns a promise which resolves once the contacts have been - * fetched. - */ - return new Promise(function (resolve, reject) { - _this3.fetch({ - 'add': true, - 'silent': true, - success: function success(collection) { - if (collection.length === 0) { - _converse.send_initial_presence = true; - - _converse.roster.fetchFromServer().then(resolve).catch(reject); - } else { - _converse.emit('cachedRoster', collection); - - resolve(); - } - } - }); - }); - }, - subscribeToSuggestedItems: function subscribeToSuggestedItems(msg) { - _.each(msg.querySelectorAll('item'), function (item) { - if (item.getAttribute('action') === 'add') { - _converse.roster.addAndSubscribe(item.getAttribute('jid'), _converse.xmppstatus.get('nickname') || _converse.xmppstatus.get('fullname')); - } - }); - - return true; - }, - isSelf: function isSelf(jid) { - return u.isSameBareJID(jid, _converse.connection.jid); - }, - addAndSubscribe: function addAndSubscribe(jid, name, groups, message, attributes) { - /* Add a roster contact and then once we have confirmation from - * the XMPP server we subscribe to that contact's presence updates. - * Parameters: - * (String) jid - The Jabber ID of the user being added and subscribed to. - * (String) name - The name of that user - * (Array of Strings) groups - Any roster groups the user might belong to - * (String) message - An optional message to explain the - * reason for the subscription request. - * (Object) attributes - Any additional attributes to be stored on the user's model. - */ - var handler = function handler(contact) { - if (contact instanceof _converse.RosterContact) { - contact.subscribe(message); - } - }; - - this.addContactToRoster(jid, name, groups, attributes).then(handler, handler); - }, - sendContactAddIQ: function sendContactAddIQ(jid, name, groups, callback, errback) { - /* Send an IQ stanza to the XMPP server to add a new roster contact. - * - * Parameters: - * (String) jid - The Jabber ID of the user being added - * (String) name - The name of that user - * (Array of Strings) groups - Any roster groups the user might belong to - * (Function) callback - A function to call once the IQ is returned - * (Function) errback - A function to call if an error occured - */ - name = _.isEmpty(name) ? jid : name; - var iq = $iq({ - type: 'set' - }).c('query', { - xmlns: Strophe.NS.ROSTER - }).c('item', { - jid: jid, - name: name - }); - - _.each(groups, function (group) { - iq.c('group').t(group).up(); - }); - - _converse.connection.sendIQ(iq, callback, errback); - }, - addContactToRoster: function addContactToRoster(jid, name, groups, attributes) { - var _this4 = this; - - /* Adds a RosterContact instance to _converse.roster and - * registers the contact on the XMPP server. - * Returns a promise which is resolved once the XMPP server has - * responded. - * - * Parameters: - * (String) jid - The Jabber ID of the user being added and subscribed to. - * (String) name - The name of that user - * (Array of Strings) groups - Any roster groups the user might belong to - * (Object) attributes - Any additional attributes to be stored on the user's model. - */ - return new Promise(function (resolve, reject) { - groups = groups || []; - name = _.isEmpty(name) ? jid : name; - - _this4.sendContactAddIQ(jid, name, groups, function () { - var contact = _this4.create(_.assignIn({ - ask: undefined, - fullname: name, - groups: groups, - jid: jid, - requesting: false, - subscription: 'none' - }, attributes), { - sort: false - }); - - resolve(contact); - }, function (err) { - alert(__('Sorry, there was an error while trying to add %1$s as a contact.', name)); - - _converse.log(err, Strophe.LogLevel.ERROR); - - resolve(err); - }); - }); - }, - subscribeBack: function subscribeBack(bare_jid, presence) { - var contact = this.get(bare_jid); - - if (contact instanceof _converse.RosterContact) { - contact.authorize().subscribe(); - } else { - // Can happen when a subscription is retried or roster was deleted - var handler = function handler(contact) { - if (contact instanceof _converse.RosterContact) { - contact.authorize().subscribe(); - } - }; - - var nickname = _.get(sizzle("nick[xmlns=\"".concat(Strophe.NS.NICK, "\"]"), presence).pop(), 'textContent', null); - - this.addContactToRoster(bare_jid, nickname, [], { - 'subscription': 'from' - }).then(handler, handler); - } - }, - getNumOnlineContacts: function getNumOnlineContacts() { - var ignored = ['offline', 'unavailable']; - - if (_converse.show_only_online_users) { - ignored = _.union(ignored, ['dnd', 'xa', 'away']); - } - - return _.sum(this.models.filter(function (model) { - return !_.includes(ignored, model.get('chat_status')); - })); - }, - 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. - */ - 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; - } - - _converse.connection.send($iq({ - type: 'result', - id: id, - from: _converse.connection.jid - })); - - var items = sizzle("query[xmlns=\"".concat(Strophe.NS.ROSTER, "\"] item"), iq); - - _.each(items, this.updateContact.bind(this)); - - _converse.emit('rosterPush', iq); - - return true; - }, - fetchFromServer: function fetchFromServer() { - var _this5 = this; - - /* Fetch the roster from the XMPP server */ - return new Promise(function (resolve, reject) { - var iq = $iq({ - 'type': 'get', - 'id': _converse.connection.getUniqueId('roster') - }).c('query', { - xmlns: Strophe.NS.ROSTER - }); - - var callback = _.flow(_this5.onReceivedFromServer.bind(_this5), resolve); - - var errback = function errback(iq) { - var errmsg = "Error while trying to fetch roster from the server"; - - _converse.log(errmsg, Strophe.LogLevel.ERROR); - - reject(new Error(errmsg)); - }; - - return _converse.connection.sendIQ(iq, callback, errback); - }); - }, - onReceivedFromServer: function onReceivedFromServer(iq) { - /* An IQ stanza containing the roster has been received from - * the XMPP server. - */ - var items = sizzle("query[xmlns=\"".concat(Strophe.NS.ROSTER, "\"] item"), iq); - - _.each(items, this.updateContact.bind(this)); - - _converse.emit('roster', iq); - }, - updateContact: function updateContact(item) { - /* Update or create RosterContact models based on items - * received in the IQ from the server. - */ - var jid = item.getAttribute('jid'); - - if (this.isSelf(jid)) { - return; - } - - var contact = this.get(jid), - subscription = item.getAttribute("subscription"), - ask = item.getAttribute("ask"), - groups = _.map(item.getElementsByTagName('group'), Strophe.getText); - - if (!contact) { - if (subscription === "none" && ask === null || subscription === "remove") { - return; // We're lazy when adding contacts. - } - - this.create({ - 'ask': ask, - 'fullname': item.getAttribute("name") || jid, - 'groups': groups, - 'jid': jid, - 'subscription': subscription - }, { - sort: false - }); - } else { - if (subscription === "remove") { - return contact.destroy(); - } // We only find out about requesting contacts via the - // presence handler, so if we receive a contact - // here, we know they aren't requesting anymore. - // see docs/DEVELOPER.rst - - - contact.save({ - 'subscription': subscription, - 'ask': ask, - 'requesting': null, - 'groups': groups - }); - } - }, - createRequestingContact: function createRequestingContact(presence) { - /* Creates a Requesting Contact. - * - * Note: this method gets completely overridden by converse-vcard.js - */ - var bare_jid = Strophe.getBareJidFromJid(presence.getAttribute('from')), - nickname = _.get(sizzle("nick[xmlns=\"".concat(Strophe.NS.NICK, "\"]"), presence).pop(), 'textContent', null); - - var user_data = { - 'jid': bare_jid, - 'subscription': 'none', - 'ask': null, - 'requesting': true, - 'fullname': nickname - }; - this.create(user_data); - - _converse.emit('contactRequest', user_data); - }, - handleIncomingSubscription: function handleIncomingSubscription(presence) { - var jid = presence.getAttribute('from'), - bare_jid = Strophe.getBareJidFromJid(jid), - contact = this.get(bare_jid); - - if (!_converse.allow_contact_requests) { - _converse.rejectPresenceSubscription(jid, __("This client does not allow presence subscriptions")); - } - - if (_converse.auto_subscribe) { - if (!contact || contact.get('subscription') !== 'to') { - this.subscribeBack(bare_jid, presence); - } else { - contact.authorize(); - } - } else { - if (contact) { - if (contact.get('subscription') !== 'none') { - contact.authorize(); - } else if (contact.get('ask') === "subscribe") { - contact.authorize(); - } - } else { - this.createRequestingContact(presence); - } - } - }, - presenceHandler: function presenceHandler(presence) { - var presence_type = presence.getAttribute('type'); - - if (presence_type === 'error') { - return true; - } - - var jid = presence.getAttribute('from'), - bare_jid = Strophe.getBareJidFromJid(jid), - resource = Strophe.getResourceFromJid(jid), - chat_status = _.propertyOf(presence.querySelector('show'))('textContent') || 'online', - status_message = _.propertyOf(presence.querySelector('status'))('textContent'), - contact = this.get(bare_jid); - - if (this.isSelf(bare_jid)) { - if (_converse.connection.jid !== jid && presence_type !== 'unavailable' && (_converse.synchronize_availability === true || _converse.synchronize_availability === resource)) { - // Another resource has changed its status and - // synchronize_availability option set to update, - // we'll update ours as well. - _converse.xmppstatus.save({ - 'status': chat_status - }); - - if (status_message) { - _converse.xmppstatus.save({ - 'status_message': status_message - }); - } - } - - if (_converse.jid === jid && presence_type === 'unavailable') { - // XXX: We've received an "unavailable" presence from our - // own resource. Apparently this happens due to a - // Prosody bug, whereby we send an IQ stanza to remove - // a roster contact, and Prosody then sends - // "unavailable" globally, instead of directed to the - // particular user that's removed. - // - // Here is the bug report: https://prosody.im/issues/1121 - // - // I'm not sure whether this might legitimately happen - // in other cases. - // - // As a workaround for now we simply send our presence again, - // otherwise we're treated as offline. - _converse.xmppstatus.sendPresence(); - } - - return; - } else if (sizzle("query[xmlns=\"".concat(Strophe.NS.MUC, "\"]"), presence).length) { - return; // Ignore MUC - } - - if (contact && status_message !== contact.get('status')) { - contact.save({ - 'status': status_message - }); - } - - if (presence_type === 'subscribed' && contact) { - contact.ackSubscribe(); - } else if (presence_type === 'unsubscribed' && contact) { - contact.ackUnsubscribe(); - } else if (presence_type === 'unsubscribe') { - return; - } else if (presence_type === 'subscribe') { - this.handleIncomingSubscription(presence); - } else if (presence_type === 'unavailable' && contact) { - contact.removeResource(resource); - } else if (contact) { - // presence_type is undefined - contact.addResource(presence); - } - } - }); - this.RosterGroup = Backbone.Model.extend({ - initialize: function initialize(attributes) { - this.set(_.assignIn({ - description: __('Click to hide these contacts'), - state: _converse.OPENED - }, attributes)); // Collection of contacts belonging to this group. - - this.contacts = new _converse.RosterContacts(); - } - }); - this.RosterGroups = Backbone.Collection.extend({ - model: _converse.RosterGroup, - fetchRosterGroups: function fetchRosterGroups() { - var _this6 = this; - - /* Fetches all the roster groups from sessionStorage. - * - * Returns a promise which resolves once the groups have been - * returned. - */ - return new Promise(function (resolve, reject) { - _this6.fetch({ - silent: true, - // We need to first have all groups before - // we can start positioning them, so we set - // 'silent' to true. - success: resolve - }); - }); - } - }); this.ConnectionFeedback = Backbone.Model.extend({ defaults: { 'connection_status': Strophe.Status.DISCONNECTED, @@ -12335,32 +11605,6 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat } }); this.connfeedback = new this.ConnectionFeedback(); - this.ModelWithDefaultAvatar = Backbone.Model.extend({ - defaults: { - 'image': _converse.DEFAULT_IMAGE, - 'image_type': _converse.DEFAULT_IMAGE_TYPE - }, - set: function set(key, val, options) { - // Override Backbone.Model.prototype.set to make sure that the - // default `image` and `image_type` values are maintained. - var attrs; - - if (_typeof(key) === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - if (_.has(attrs, 'image') && _.isUndefined(attrs['image'])) { - attrs['image'] = _converse.DEFAULT_IMAGE; - attrs['image_type'] = _converse.DEFAULT_IMAGE_TYPE; - return Backbone.Model.prototype.set.call(this, attrs, options); - } else { - return Backbone.Model.prototype.set.apply(this, arguments); - } - } - }); this.XMPPStatus = this.ModelWithDefaultAvatar.extend({ defaults: function defaults() { return { @@ -12373,19 +11617,19 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat }; }, initialize: function initialize() { - var _this7 = this; + var _this3 = this; this.on('change:status', function (item) { - var status = _this7.get('status'); + var status = _this3.get('status'); - _this7.sendPresence(status); + _this3.sendPresence(status); _converse.emit('statusChanged', status); }); this.on('change:status_message', function () { - var status_message = _this7.get('status_message'); + var status_message = _this3.get('status_message'); - _this7.sendPresence(_this7.get('status'), status_message); + _this3.sendPresence(_this3.get('status'), status_message); _converse.emit('statusMessageChanged', status_message); }); @@ -12625,6 +11869,8 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat }; this.initConnection = function () { + /* Creates a new Strophe.Connection instance if we don't already have one. + */ if (!this.connection) { if (!this.bosh_service_url && !this.websocket_url) { throw new Error("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both."); @@ -12652,11 +11898,6 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat _converse.unregisterPresenceHandler(); - if (_converse.roster) { - _converse.roster.off().reset(); // Removes roster contacts - - } - if (!_.isUndefined(_converse.session)) { _converse.session.destroy(); } @@ -12686,7 +11927,7 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat if (_converse.view_mode === 'embedded') { _.forEach([// eslint-disable-line lodash/prefer-map - "converse-bookmarks", "converse-controlbox", "converse-dragresize", "converse-headline", "converse-minimize", "converse-otr", "converse-register", "converse-vcard"], function (name) { + "converse-bookmarks", "converse-controlbox", "converse-headline", "converse-register"], function (name) { _converse.blacklisted_plugins.push(name); }); } @@ -12758,8 +11999,6 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat return _converse.connection.jid; }, 'login': function login(credentials) { - _converse.initConnection(); - _converse.logIn(credentials); }, 'logout': function logout() { @@ -12828,28 +12067,6 @@ function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterat _.each(promises, addPromise); } }, - 'contacts': { - 'get': function get(jids) { - var _getter = function _getter(jid) { - return _converse.roster.get(Strophe.getBareJidFromJid(jid)) || null; - }; - - if (_.isUndefined(jids)) { - jids = _converse.roster.pluck('jid'); - } else if (_.isString(jids)) { - return _getter(jids); - } - - return _.map(jids, _getter); - }, - 'add': function add(jid, name) { - if (!_.isString(jid) || !_.includes(jid, '@')) { - throw new TypeError('contacts.add: invalid jid'); - } - - _converse.roster.addAndSubscribe(jid, _.isEmpty(name) ? jid : name); - } - }, 'tokens': { 'get': function get(id) { if (!_converse.expose_rid_and_sid || _.isUndefined(_converse.connection)) { @@ -13824,6 +13041,3718 @@ define("emojione", (function (global) { }; }(this))); +// Converse.js (A browser based XMPP chat client) +// http://conversejs.org +// +// Copyright (c) 2012-2017, Jan-Carel Brand +// Licensed under the Mozilla Public License (MPLv2) +// + +/* This is a Converse.js plugin which add support for XEP-0030: Service Discovery */ + +/*global Backbone, define, window */ +(function (root, factory) { + define('converse-disco',["converse-core", "sizzle"], factory); +})(this, function (converse, sizzle) { + var _converse$env = converse.env, + Backbone = _converse$env.Backbone, + Promise = _converse$env.Promise, + Strophe = _converse$env.Strophe, + $iq = _converse$env.$iq, + b64_sha1 = _converse$env.b64_sha1, + utils = _converse$env.utils, + _ = _converse$env._, + f = _converse$env.f; + converse.plugins.add('converse-disco', { + initialize: function initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + var _converse = this._converse; // Promises exposed by this plugin + + _converse.api.promises.add('discoInitialized'); + + _converse.DiscoEntity = Backbone.Model.extend({ + /* A Disco Entity is a JID addressable entity that can be queried + * for features. + * + * See XEP-0030: https://xmpp.org/extensions/xep-0030.html + */ + idAttribute: 'jid', + 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.features = new Backbone.Collection(); + this.features.browserStorage = new Backbone.BrowserStorage[_converse.storage](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.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.fetch(); + }, + getIdentity: function getIdentity(category, type) { + /* Returns a Promise which resolves with a map indicating + * whether a given identity is provided. + * + * Parameters: + * (String) category - The identity category + * (String) type - The identity type + */ + var entity = this; + return new Promise(function (resolve, reject) { + function fulfillPromise() { + var model = entity.identities.findWhere({ + 'category': category, + 'type': type + }); + resolve(model); + } + + entity.waitUntilFeaturesDiscovered.then(fulfillPromise).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }); + }, + hasFeature: function hasFeature(feature) { + /* Returns a Promise which resolves with a map indicating + * whether a given feature is supported. + * + * Parameters: + * (String) feature - The feature that might be supported. + */ + var entity = this; + return new Promise(function (resolve, reject) { + function fulfillPromise() { + if (entity.features.findWhere({ + 'var': feature + })) { + resolve(entity); + } else { + resolve(); + } + } + + entity.waitUntilFeaturesDiscovered.then(fulfillPromise).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }); + }, + onFeatureAdded: function onFeatureAdded(feature) { + feature.entity = this; + + _converse.emit('serviceDiscovered', feature); + }, + fetchFeatures: function fetchFeatures() { + var _this = this; + + if (this.features.browserStorage.records.length === 0) { + this.queryInfo(); + } else { + this.features.fetch({ + add: true, + success: function success() { + _this.waitUntilFeaturesDiscovered.resolve(); + + _this.trigger('featuresDiscovered'); + } + }); + this.identities.fetch({ + add: true + }); + } + }, + queryInfo: function queryInfo() { + _converse.api.disco.info(this.get('jid'), null, this.onInfo.bind(this)); + }, + onDiscoItems: function onDiscoItems(stanza) { + var _this2 = this; + + _.each(sizzle("query[xmlns=\"".concat(Strophe.NS.DISCO_ITEMS, "\"] item"), stanza), function (item) { + if (item.getAttribute("node")) { + // XXX: ignore nodes for now. + // See: https://xmpp.org/extensions/xep-0030.html#items-nodes + return; + } + + var jid = item.getAttribute('jid'); + + if (_.isUndefined(_this2.items.get(jid))) { + _this2.items.create({ + 'jid': jid + }); + } + }); + }, + queryForItems: function queryForItems() { + if (_.isEmpty(this.identities.where({ + 'category': 'server' + }))) { + // Don't fetch features and items if this is not a + // server or a conference component. + return; + } + + _converse.api.disco.items(this.get('jid'), null, this.onDiscoItems.bind(this)); + }, + onInfo: function onInfo(stanza) { + var _this3 = this; + + _.forEach(stanza.querySelectorAll('identity'), function (identity) { + _this3.identities.create({ + 'category': identity.getAttribute('category'), + 'type': identity.getAttribute('type'), + 'name': identity.getAttribute('name') + }); + }); + + _.each(sizzle("x[type=\"result\"][xmlns=\"".concat(Strophe.NS.XFORM, "\"]"), stanza), function (form) { + var data = {}; + + _.each(form.querySelectorAll('field'), function (field) { + data[field.getAttribute('var')] = { + 'value': _.get(field.querySelector('value'), 'textContent'), + 'type': field.getAttribute('type') + }; + }); + + _this3.dataforms.create(data); + }); + + if (stanza.querySelector("feature[var=\"".concat(Strophe.NS.DISCO_ITEMS, "\"]"))) { + this.queryForItems(); + } + + _.forEach(stanza.querySelectorAll('feature'), function (feature) { + _this3.features.create({ + 'var': feature.getAttribute('var'), + 'from': stanza.getAttribute('from') + }); + }); + + this.waitUntilFeaturesDiscovered.resolve(); + this.trigger('featuresDiscovered'); + } + }); + _converse.DiscoEntities = Backbone.Collection.extend({ + model: _converse.DiscoEntity, + fetchEntities: function fetchEntities() { + var _this4 = this; + + return new Promise(function (resolve, reject) { + _this4.fetch({ + add: true, + success: resolve, + error: function error() { + reject(new Error("Could not fetch disco entities")); + } + }); + }); + } + }); + + function addClientFeatures() { + // See http://xmpp.org/registrar/disco-categories.html + _converse.api.disco.addIdentity('client', 'web', 'Converse.js'); + + _converse.api.disco.addFeature(Strophe.NS.BOSH); + + _converse.api.disco.addFeature(Strophe.NS.CHATSTATES); + + _converse.api.disco.addFeature(Strophe.NS.DISCO_INFO); + + _converse.api.disco.addFeature(Strophe.NS.ROSTERX); // Limited support + + + if (_converse.message_carbons) { + _converse.api.disco.addFeature(Strophe.NS.CARBONS); + } + + _converse.emit('addClientFeatures'); + + return this; + } + + 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.fetchEntities().then(function (collection) { + if (collection.length === 0 || !collection.get(_converse.domain)) { + // If we don't have an entity for our own XMPP server, + // create one. + _converse.disco_entities.create({ + 'jid': _converse.domain + }); + } + + _converse.emit('discoInitialized'); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + } + + _converse.api.listen.on('reconnected', initializeDisco); + + _converse.api.listen.on('connected', initializeDisco); + + _converse.api.listen.on('beforeTearDown', function () { + if (_converse.disco_entities) { + _converse.disco_entities.each(function (entity) { + entity.features.reset(); + + entity.features.browserStorage._clear(); + }); + + _converse.disco_entities.reset(); + + _converse.disco_entities.browserStorage._clear(); + } + }); + + var plugin = this; + plugin._identities = []; + plugin._features = []; + + function onDiscoInfoRequest(stanza) { + var node = stanza.getElementsByTagName('query')[0].getAttribute('node'); + var attrs = { + xmlns: Strophe.NS.DISCO_INFO + }; + + if (node) { + attrs.node = node; + } + + var iqresult = $iq({ + 'type': 'result', + 'id': stanza.getAttribute('id') + }); + var from = stanza.getAttribute('from'); + + if (from !== null) { + iqresult.attrs({ + 'to': from + }); + } + + _.each(plugin._identities, function (identity) { + var attrs = { + 'category': identity.category, + 'type': identity.type + }; + + if (identity.name) { + attrs.name = identity.name; + } + + if (identity.lang) { + attrs['xml:lang'] = identity.lang; + } + + iqresult.c('identity', attrs).up(); + }); + + _.each(plugin._features, function (feature) { + iqresult.c('feature', { + 'var': feature + }).up(); + }); + + _converse.connection.send(iqresult.tree()); + + return true; + } + /* We extend the default converse.js API to add methods specific to service discovery */ + + + _.extend(_converse.api, { + 'disco': { + 'info': function info(jid, node, callback, errback, timeout) { + var attrs = { + xmlns: Strophe.NS.DISCO_INFO + }; + + if (node) { + attrs.node = node; + } + + var info = $iq({ + 'from': _converse.connection.jid, + 'to': jid, + 'type': 'get' + }).c('query', attrs); + + _converse.connection.sendIQ(info, callback, errback, timeout); + }, + 'items': function items(jid, node, callback, errback, timeout) { + var attrs = { + 'xmlns': Strophe.NS.DISCO_ITEMS + }; + + if (node) { + attrs.node = node; + } + + var items = $iq({ + 'from': _converse.connection.jid, + 'to': jid, + 'type': 'get' + }).c('query', attrs); + + _converse.connection.sendIQ(items, callback, errback, timeout); + }, + 'entities': { + 'get': function get(entity_jid) { + var create = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + return _converse.api.waitUntil('discoInitialized').then(function () { + if (_.isNil(entity_jid)) { + return _converse.disco_entities; + } + + var entity = _converse.disco_entities.get(entity_jid); + + if (entity || !create) { + return entity; + } + + return _converse.disco_entities.create({ + 'jid': entity_jid + }); + }); + } + }, + 'supports': function supports(feature, entity_jid) { + /* Returns a Promise which resolves with a list containing + * _converse.Entity instances representing the entity + * itself or those items associated with the entity if + * they support the given feature. + * + * Parameters: + * (String) feature - The feature that might be + * supported. In the XML stanza, this is the `var` + * attribute of the `` element. For + * example: 'http://jabber.org/protocol/muc' + * (String) entity_jid - The JID of the entity + * (and its associated items) which should be queried + */ + if (_.isNil(entity_jid)) { + throw new TypeError('disco.supports: You need to provide an entity JID'); + } + + return _converse.api.waitUntil('discoInitialized').then(function () { + return new Promise(function (resolve, reject) { + _converse.api.disco.entities.get(entity_jid, true).then(function (entity) { + entity.waitUntilFeaturesDiscovered.then(function () { + var promises = _.concat(entity.items.map(function (item) { + return item.hasFeature(feature); + }), entity.hasFeature(feature)); + + Promise.all(promises).then(function (result) { + resolve(f.filter(f.isObject, result)); + }).catch(reject); + }).catch(reject); + }); + }); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }, + 'addIdentity': function addIdentity(category, type, name, lang) { + for (var i = 0; i < plugin._identities.length; i++) { + if (plugin._identities[i].category == category && plugin._identities[i].type == type && plugin._identities[i].name == name && plugin._identities[i].lang == lang) { + return false; + } + } + + plugin._identities.push({ + category: category, + type: type, + name: name, + lang: lang + }); + }, + 'addFeature': function addFeature(name) { + for (var i = 0; i < plugin._features.length; i++) { + if (plugin._features[i] == name) { + return false; + } + } + + plugin._features.push(name); + }, + 'getIdentity': function getIdentity(category, type, entity_jid) { + /* Returns a Promise which resolves with a map indicating + * whether an identity with a given type is provided by + * the entity. + * + * Parameters: + * (String) category - The identity category. + * In the XML stanza, this is the `category` + * attribute of the `` element. + * For example: 'pubsub' + * (String) type - The identity type. + * In the XML stanza, this is the `type` + * attribute of the `` element. + * For example: 'pep' + * (String) entity_jid - The JID of the entity which might have the identity + */ + return new Promise(function (resolve, reject) { + _converse.api.waitUntil('discoInitialized').then(function () { + _converse.api.disco.entities.get(entity_jid, true).then(function (entity) { + return resolve(entity.getIdentity(category, type)); + }); + }); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + } + } + }); + } + }); +}); +//# sourceMappingURL=converse-disco.js.map; +/*! + * Backbone.OrderedListView + * + * Copyright (c) 2017, JC Brand + * Licensed under the Mozilla Public License (MPL) + */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define('backbone.orderedlistview',["underscore", "backbone", "backbone.overview"], factory); + } else { + // RequireJS isn't being used. + // Assume underscore and backbone are loaded in