diff --git a/Makefile b/Makefile index 4fb8cf3d7..8bad0117c 100644 --- a/Makefile +++ b/Makefile @@ -159,8 +159,6 @@ transpile: stamp-npm src BUILDS = dist/converse.js \ dist/converse.min.js \ - dist/converse-esnext.js \ - dist/converse-esnext.min.js \ dist/converse-muc-embedded.js \ dist/converse-muc-embedded.min.js \ dist/converse-no-jquery.js \ @@ -168,6 +166,9 @@ BUILDS = dist/converse.js \ dist/converse-no-dependencies.min.js \ dist/converse-no-dependencies.js +# dist/converse-esnext.js \ +# dist/converse-esnext.min.js \ + dist/converse.js: transpile src node_modules $(RJS) -o src/build.js include=converse out=dist/converse.js optimize=none dist/converse.min.js: transpile src node_modules diff --git a/dist/converse-no-dependencies.js b/dist/converse-no-dependencies.js index 7b8c800b6..b3886b75e 100644 --- a/dist/converse-no-dependencies.js +++ b/dist/converse-no-dependencies.js @@ -6333,8 +6333,6 @@ return uk; }))); - - // Converse.js (A browser based XMPP chat client) // http://conversejs.org // @@ -6343,125 +6341,138 @@ return uk; // Copyright (c) 2012-2017, Jan-Carel Brand // Licensed under the Mozilla Public License (MPLv2) // + /*global define */ - (function (root, factory) { - define('i18n',["es6-promise", "jed", "lodash.noconflict", "moment", 'moment/locale/af', 'moment/locale/ca', 'moment/locale/de', 'moment/locale/es', 'moment/locale/fr', 'moment/locale/he', 'moment/locale/hu', 'moment/locale/id', 'moment/locale/it', 'moment/locale/ja', 'moment/locale/nb', 'moment/locale/nl', 'moment/locale/pl', 'moment/locale/pt-br', 'moment/locale/ru', 'moment/locale/uk'], factory); -})(undefined, function (Promise, Jed, _, moment) { - 'use strict'; + define('i18n',["es6-promise", "jed", "lodash.noconflict", "moment", 'moment/locale/af', 'moment/locale/ca', 'moment/locale/de', 'moment/locale/es', 'moment/locale/fr', 'moment/locale/he', 'moment/locale/hu', 'moment/locale/id', 'moment/locale/it', 'moment/locale/ja', 'moment/locale/nb', 'moment/locale/nl', 'moment/locale/pl', 'moment/locale/pt-br', 'moment/locale/ru', 'moment/locale/uk'], factory); +})(this, function (Promise, Jed, _, moment) { + 'use strict'; - function detectLocale(library_check) { - /* Determine which locale is supported by the user's system as well - * as by the relevant library (e.g. converse.js or moment.js). - * - * Parameters: - * (Function) library_check - Returns a boolean indicating whether - * the locale is supported. - */ - var locale, i; - if (window.navigator.userLanguage) { - locale = isLocaleAvailable(window.navigator.userLanguage, library_check); - } - if (window.navigator.languages && !locale) { - for (i = 0; i < window.navigator.languages.length && !locale; i++) { - locale = isLocaleAvailable(window.navigator.languages[i], library_check); - } - } - if (window.navigator.browserLanguage && !locale) { - locale = isLocaleAvailable(window.navigator.browserLanguage, library_check); - } - if (window.navigator.language && !locale) { - locale = isLocaleAvailable(window.navigator.language, library_check); - } - if (window.navigator.systemLanguage && !locale) { - locale = isLocaleAvailable(window.navigator.systemLanguage, library_check); - } - return locale || 'en'; + function detectLocale(library_check) { + /* Determine which locale is supported by the user's system as well + * as by the relevant library (e.g. converse.js or moment.js). + * + * Parameters: + * (Function) library_check - Returns a boolean indicating whether + * the locale is supported. + */ + var locale, i; + + if (window.navigator.userLanguage) { + locale = isLocaleAvailable(window.navigator.userLanguage, library_check); } - function isMomentLocale(locale) { - return _.isString(locale) && moment.locale() === moment.locale(locale); + if (window.navigator.languages && !locale) { + for (i = 0; i < window.navigator.languages.length && !locale; i++) { + locale = isLocaleAvailable(window.navigator.languages[i], library_check); + } } - function isConverseLocale(locale, supported_locales) { - return _.isString(locale) && _.includes(supported_locales, locale); + if (window.navigator.browserLanguage && !locale) { + locale = isLocaleAvailable(window.navigator.browserLanguage, library_check); } - function getLocale(preferred_locale, isSupportedByLibrary) { - if (_.isString(preferred_locale)) { - if (preferred_locale === 'en' || isSupportedByLibrary(preferred_locale)) { - return preferred_locale; - } + if (window.navigator.language && !locale) { + locale = isLocaleAvailable(window.navigator.language, library_check); + } + + if (window.navigator.systemLanguage && !locale) { + locale = isLocaleAvailable(window.navigator.systemLanguage, library_check); + } + + return locale || 'en'; + } + + function isMomentLocale(locale) { + return _.isString(locale) && moment.locale() === moment.locale(locale); + } + + function isConverseLocale(locale, supported_locales) { + return _.isString(locale) && _.includes(supported_locales, locale); + } + + function getLocale(preferred_locale, isSupportedByLibrary) { + if (_.isString(preferred_locale)) { + if (preferred_locale === 'en' || isSupportedByLibrary(preferred_locale)) { + return preferred_locale; + } + } + + return detectLocale(isSupportedByLibrary) || 'en'; + } + + function isLocaleAvailable(locale, available) { + /* Check whether the locale or sub locale (e.g. en-US, en) is supported. + * + * Parameters: + * (String) locale - The locale to check for + * (Function) available - returns a boolean indicating whether the locale is supported + */ + if (available(locale)) { + return locale; + } else { + var sublocale = locale.split("-")[0]; + + if (sublocale !== locale && available(sublocale)) { + return sublocale; + } + } + } + + var jed_instance; + return { + setLocales: function setLocales(preferred_locale, _converse) { + _converse.locale = getLocale(preferred_locale, _.partial(isConverseLocale, _, _converse.locales)); + moment.locale(getLocale(preferred_locale, isMomentLocale)); + }, + translate: function translate(str) { + if (_.isNil(jed_instance)) { + return Jed.sprintf.apply(Jed, arguments); + } + + var t = jed_instance.translate(str); + + if (arguments.length > 1) { + return t.fetch.apply(t, [].slice.call(arguments, 1)); + } else { + return t.fetch(); + } + }, + fetchTranslations: function fetchTranslations(locale, supported_locales, locale_url) { + /* Fetch the translations for the given local at the given URL. + * + * Parameters: + * (String) locale: The given i18n locale + * (Array) supported_locales: List of locales supported + * (String) locale_url: The URL from which the translations + * should be fetched. + */ + return new Promise(function (resolve, reject) { + if (!isConverseLocale(locale, supported_locales) || locale === 'en') { + return resolve(); } - return detectLocale(isSupportedByLibrary) || 'en'; + + var xhr = new XMLHttpRequest(); + xhr.open('GET', locale_url, true); + xhr.setRequestHeader('Accept', "application/json, text/javascript"); + + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 400) { + jed_instance = new Jed(window.JSON.parse(xhr.responseText)); + resolve(); + } else { + xhr.onerror(); + } + }; + + xhr.onerror = function () { + reject(xhr.statusText); + }; + + xhr.send(); + }); } - - function isLocaleAvailable(locale, available) { - /* Check whether the locale or sub locale (e.g. en-US, en) is supported. - * - * Parameters: - * (String) locale - The locale to check for - * (Function) available - returns a boolean indicating whether the locale is supported - */ - if (available(locale)) { - return locale; - } else { - var sublocale = locale.split("-")[0]; - if (sublocale !== locale && available(sublocale)) { - return sublocale; - } - } - } - - var jed_instance = void 0; - - return { - setLocales: function setLocales(preferred_locale, _converse) { - _converse.locale = getLocale(preferred_locale, _.partial(isConverseLocale, _, _converse.locales)); - moment.locale(getLocale(preferred_locale, isMomentLocale)); - }, - translate: function translate(str) { - if (_.isNil(jed_instance)) { - return Jed.sprintf.apply(Jed, arguments); - } - var t = jed_instance.translate(str); - if (arguments.length > 1) { - return t.fetch.apply(t, [].slice.call(arguments, 1)); - } else { - return t.fetch(); - } - }, - fetchTranslations: function fetchTranslations(locale, supported_locales, locale_url) { - /* Fetch the translations for the given local at the given URL. - * - * Parameters: - * (String) locale: The given i18n locale - * (Array) supported_locales: List of locales supported - * (String) locale_url: The URL from which the translations - * should be fetched. - */ - return new Promise(function (resolve, reject) { - if (!isConverseLocale(locale, supported_locales) || locale === 'en') { - return resolve(); - } - var xhr = new XMLHttpRequest(); - xhr.open('GET', locale_url, true); - xhr.setRequestHeader('Accept', "application/json, text/javascript"); - xhr.onload = function () { - if (xhr.status >= 200 && xhr.status < 400) { - jed_instance = new Jed(window.JSON.parse(xhr.responseText)); - resolve(); - } else { - xhr.onerror(); - } - }; - xhr.onerror = function () { - reject(xhr.statusText); - }; - xhr.send(); - }); - } - }; + }; }); //# sourceMappingURL=i18n.js.map; /*! @@ -6658,9 +6669,7 @@ return uk; return window.jQBrowser; })); - - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; +function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } // Converse.js (A browser based XMPP chat client) // http://conversejs.org @@ -6670,502 +6679,557 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol // Copyright (c) 2012-2017, Jan-Carel Brand // Licensed under the Mozilla Public License (MPLv2) // + /*global define, escape, window */ (function (root, factory) { - define('utils',["sizzle", "es6-promise", "jquery.browser", "lodash.noconflict", "strophe"], factory); -})(undefined, function (sizzle, Promise, jQBrowser, _, Strophe) { - "use strict"; + define('utils',["sizzle", "es6-promise", "jquery.browser", "lodash.noconflict", "strophe"], factory); +})(this, function (sizzle, Promise, jQBrowser, _, Strophe) { + "use strict"; - var b64_sha1 = Strophe.SHA1.b64_sha1; - Strophe = Strophe.Strophe; + var b64_sha1 = Strophe.SHA1.b64_sha1; + Strophe = Strophe.Strophe; + var URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b/g; - var URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b/g; + var logger = _.assign({ + 'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop, + 'error': _.get(console, 'log') ? console.log.bind(console) : _.noop, + 'info': _.get(console, 'log') ? console.log.bind(console) : _.noop, + 'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop + }, console); - var logger = _.assign({ - 'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop, - 'error': _.get(console, 'log') ? console.log.bind(console) : _.noop, - 'info': _.get(console, 'log') ? console.log.bind(console) : _.noop, - 'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop - }, console); + var afterAnimationEnd = function afterAnimationEnd(el, callback) { + el.classList.remove('visible'); - var afterAnimationEnd = function afterAnimationEnd(el, callback) { - el.classList.remove('visible'); - if (_.isFunction(callback)) { - callback(); - } - }; + if (_.isFunction(callback)) { + callback(); + } + }; - var unescapeHTML = function unescapeHTML(htmlEscapedText) { - /* Helper method that replace HTML-escaped symbols with equivalent characters - * (e.g. transform occurrences of '&' to '&') - * - * Parameters: - * (String) htmlEscapedText: a String containing the HTML-escaped symbols. - */ - var div = document.createElement('div'); - div.innerHTML = htmlEscapedText; - return div.innerText; - }; + var unescapeHTML = function unescapeHTML(htmlEscapedText) { + /* Helper method that replace HTML-escaped symbols with equivalent characters + * (e.g. transform occurrences of '&' to '&') + * + * Parameters: + * (String) htmlEscapedText: a String containing the HTML-escaped symbols. + */ + var div = document.createElement('div'); + div.innerHTML = htmlEscapedText; + return div.innerText; + }; - var isImage = function isImage(url) { - return new Promise(function (resolve, reject) { - var img = new Image(); - var timer = window.setTimeout(function () { - reject(new Error("Could not determine whether it's an image")); - img = null; - }, 3000); - img.onerror = img.onabort = function () { - clearTimeout(timer); - reject(new Error("Could not determine whether it's an image")); - }; - img.onload = function () { - clearTimeout(timer); - resolve(img); - }; - img.src = url; + var isImage = function isImage(url) { + return new Promise(function (resolve, reject) { + var img = new Image(); + var timer = window.setTimeout(function () { + reject(new Error("Could not determine whether it's an image")); + img = null; + }, 3000); + + img.onerror = img.onabort = function () { + clearTimeout(timer); + reject(new Error("Could not determine whether it's an image")); + }; + + img.onload = function () { + clearTimeout(timer); + resolve(img); + }; + + img.src = url; + }); + }; + + function calculateSlideStep(height) { + if (height > 100) { + return 10; + } else if (height > 50) { + return 5; + } else { + return 1; + } + } + + function calculateElementHeight(el) { + /* Return the height of the passed in DOM element, + * based on the heights of its children. + */ + return _.reduce(el.children, function (result, child) { + return result + child.offsetHeight; + }, 0); + } + + function slideOutWrapup(el) { + /* Wrapup function for slideOut. */ + el.removeAttribute('data-slider-marker'); + el.classList.remove('collapsed'); + el.style.overflow = ""; + el.style.height = ""; + } + + var u = {}; + + u.addHyperlinks = function (text) { + var list = text.match(URL_REGEX) || []; + var links = []; + + _.each(list, function (match) { + var prot = match.indexOf('http://') === 0 || match.indexOf('https://') === 0 ? '' : 'http://'; + var url = prot + encodeURI(decodeURI(match)).replace(/[!'()]/g, escape).replace(/\*/g, "%2A"); + var a = '' + _.escape(match) + ''; // We first insert a hash of the code that will be inserted, and + // then later replace that with the code itself. That way we avoid + // issues when some matches are substrings of others. + + links.push(a); + text = text.replace(match, b64_sha1(a)); + }); + + while (links.length) { + var a = links.pop(); + text = text.replace(b64_sha1(a), a); + } + + return text; + }; + + u.renderImageURLs = function (obj) { + var list = obj.textContent.match(URL_REGEX) || []; + + _.forEach(list, function (url) { + isImage(url).then(function (img) { + img.className = 'chat-image'; + var anchors = sizzle("a[href=\"".concat(url, "\"]"), obj); + + _.each(anchors, function (a) { + a.innerHTML = img.outerHTML; }); - }; + }); + }); - function calculateSlideStep(height) { - if (height > 100) { - return 10; - } else if (height > 50) { - return 5; - } else { - return 1; - } + return obj; + }; + + u.slideInAllElements = function (elements) { + return Promise.all(_.map(elements, _.partial(u.slideIn, _, 600))); + }; + + u.slideToggleElement = function (el) { + if (_.includes(el.classList, 'collapsed')) { + return u.slideOut(el); + } else { + return u.slideIn(el); } + }; - function calculateElementHeight(el) { - /* Return the height of the passed in DOM element, - * based on the heights of its children. - */ - return _.reduce(el.children, function (result, child) { - return result + child.offsetHeight; - }, 0); - } + u.slideOut = function (el) { + var duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 900; - function slideOutWrapup(el) { - /* Wrapup function for slideOut. */ + /* Shows/expands an element by sliding it out of itself + * + * Parameters: + * (HTMLElement) el - The HTML string + * (Number) duration - The duration amount in milliseconds + */ + return new Promise(function (resolve, reject) { + if (_.isNil(el)) { + var err = "Undefined or null element passed into slideOut"; + logger.warn(err); + reject(new Error(err)); + return; + } + + var interval_marker = el.getAttribute('data-slider-marker'); + + if (interval_marker) { el.removeAttribute('data-slider-marker'); - el.classList.remove('collapsed'); - el.style.overflow = ""; + window.clearInterval(interval_marker); + } + + var end_height = calculateElementHeight(el); + + if (window.converse_disable_effects) { + // Effects are disabled (for tests) + el.style.height = end_height + 'px'; + slideOutWrapup(el); + resolve(); + return; + } + + var step = calculateSlideStep(end_height), + interval = end_height / duration * step; + var h = 0; + interval_marker = window.setInterval(function () { + h += step; + + if (h < end_height) { + el.style.height = h + 'px'; + } else { + // We recalculate the height to work around an apparent + // browser bug where browsers don't know the correct + // offsetHeight beforehand. + el.style.height = calculateElementHeight(el) + 'px'; + window.clearInterval(interval_marker); + slideOutWrapup(el); + resolve(); + } + }, interval); + el.setAttribute('data-slider-marker', interval_marker); + }); + }; + + u.slideIn = function (el) { + var duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 600; + + /* Hides/collapses an element by sliding it into itself. */ + return new Promise(function (resolve, reject) { + if (_.isNil(el)) { + var err = "Undefined or null element passed into slideIn"; + logger.warn(err); + return reject(new Error(err)); + } else if (_.includes(el.classList, 'collapsed')) { + return resolve(); + } else if (window.converse_disable_effects) { + // Effects are disabled (for tests) + el.classList.add('collapsed'); el.style.height = ""; + return resolve(); + } + + var interval_marker = el.getAttribute('data-slider-marker'); + + if (interval_marker) { + el.removeAttribute('data-slider-marker'); + window.clearInterval(interval_marker); + } + + var h = el.offsetHeight; + var step = calculateSlideStep(h), + interval = h / duration * step; + el.style.overflow = 'hidden'; + interval_marker = window.setInterval(function () { + h -= step; + + if (h > 0) { + el.style.height = h + 'px'; + } else { + el.removeAttribute('data-slider-marker'); + window.clearInterval(interval_marker); + el.classList.add('collapsed'); + el.style.height = ""; + resolve(); + } + }, interval); + el.setAttribute('data-slider-marker', interval_marker); + }); + }; + + u.fadeIn = function (el, callback) { + if (_.isNil(el)) { + logger.warn("Undefined or null element passed into fadeIn"); } - var u = {}; + if (window.converse_disable_effects) { + // Effects are disabled (for tests) + el.classList.remove('hidden'); - u.addHyperlinks = function (text) { - var list = text.match(URL_REGEX) || []; - var links = []; - _.each(list, function (match) { - var prot = match.indexOf('http://') === 0 || match.indexOf('https://') === 0 ? '' : 'http://'; - var url = prot + encodeURI(decodeURI(match)).replace(/[!'()]/g, escape).replace(/\*/g, "%2A"); - var a = '' + _.escape(match) + ''; - // We first insert a hash of the code that will be inserted, and - // then later replace that with the code itself. That way we avoid - // issues when some matches are substrings of others. - links.push(a); - text = text.replace(match, b64_sha1(a)); + if (_.isFunction(callback)) { + callback(); + } + + return; + } + + if (_.includes(el.classList, 'hidden')) { + /* XXX: This doesn't appear to be working... + el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnd, el, callback), false); + el.addEventListener("animationend", _.partial(afterAnimationEnd, el, callback), false); + */ + setTimeout(_.partial(afterAnimationEnd, el, callback), 351); + el.classList.add('visible'); + el.classList.remove('hidden'); + } else { + afterAnimationEnd(el, callback); + } + }; + + u.isValidJID = function (jid) { + return _.filter(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@'); + }; + + u.isSameBareJID = function (jid1, jid2) { + return Strophe.getBareJidFromJid(jid1).toLowerCase() === Strophe.getBareJidFromJid(jid2).toLowerCase(); + }; + + u.getMostRecentMessage = function (model) { + var messages = model.messages.filter('message'); + return messages[messages.length - 1]; + }; + + u.isNewMessage = function (message) { + /* Given a stanza, determine whether it's a new + * message, i.e. not a MAM archived one. + */ + if (message instanceof Element) { + return !sizzle('result[xmlns="' + Strophe.NS.MAM + '"]', message).length; + } else { + return !message.get('archive_id'); + } + }; + + u.isOTRMessage = function (message) { + var body = message.querySelector('body'), + text = !_.isNull(body) ? body.textContent : undefined; + return text && !!text.match(/^\?OTR/); + }; + + u.isHeadlineMessage = function (message) { + var from_jid = message.getAttribute('from'); + + if (message.getAttribute('type') === 'headline') { + return true; + } + + if (message.getAttribute('type') !== 'error' && !_.isNil(from_jid) && !_.includes(from_jid, '@')) { + // Some servers (I'm looking at you Prosody) don't set the message + // type to "headline" when sending server messages. For now we + // check if an @ signal is included, and if not, we assume it's + // a headline message. + return true; + } + + return false; + }; + + u.merge = function merge(first, second) { + /* Merge the second object into the first one. + */ + for (var k in second) { + if (_.isObject(first[k])) { + merge(first[k], second[k]); + } else { + first[k] = second[k]; + } + } + }; + + u.applyUserSettings = function applyUserSettings(context, settings, user_settings) { + /* Configuration settings might be nested objects. We only want to + * add settings which are whitelisted. + */ + for (var k in settings) { + if (_.isUndefined(user_settings[k])) { + continue; + } + + if (_.isObject(settings[k]) && !_.isArray(settings[k])) { + applyUserSettings(context[k], settings[k], user_settings[k]); + } else { + context[k] = user_settings[k]; + } + } + }; + + u.refreshWebkit = function () { + /* This works around a webkit bug. Refreshes the browser's viewport, + * otherwise chatboxes are not moved along when one is closed. + */ + if (jQBrowser.webkit && window.requestAnimationFrame) { + window.requestAnimationFrame(function () { + var conversejs = document.getElementById('conversejs'); + conversejs.style.display = 'none'; + var tmp = conversejs.offsetHeight; // jshint ignore:line + + conversejs.style.display = 'block'; + }); + } + }; + + u.stringToDOM = function (s) { + /* Converts an HTML string into a DOM element. + * + * Parameters: + * (String) s - The HTML string + */ + var div = document.createElement('div'); + div.innerHTML = s; + return div.childNodes; + }; + + u.matchesSelector = function (el, selector) { + /* Checks whether the DOM element matches the given selector. + * + * Parameters: + * (DOMElement) el - The DOM element + * (String) selector - The selector + */ + return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); + }; + + u.queryChildren = function (el, selector) { + /* Returns a list of children of the DOM element that match the + * selector. + * + * Parameters: + * (DOMElement) el - the DOM element + * (String) selector - the selector they should be matched + * against. + */ + return _.filter(el.children, _.partial(u.matchesSelector, _, selector)); + }; + + u.contains = function (attr, query) { + return function (item) { + if (_typeof(attr) === 'object') { + var value = false; + + _.forEach(attr, function (a) { + value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase()); }); - while (links.length) { - var a = links.pop(); - text = text.replace(b64_sha1(a), a); - } - return text; - }; - u.renderImageURLs = function (obj) { - var list = obj.textContent.match(URL_REGEX) || []; - _.forEach(list, function (url) { - isImage(url).then(function (img) { - img.className = 'chat-image'; - var anchors = sizzle("a[href=\"" + url + "\"]", obj); - _.each(anchors, function (a) { - a.innerHTML = img.outerHTML; - }); - }); + return value; + } else if (typeof attr === 'string') { + return _.includes(item.get(attr).toLowerCase(), query.toLowerCase()); + } else { + throw new TypeError('contains: wrong attribute type. Must be string or array.'); + } + }; + }; + + u.isOfType = function (type, item) { + return item.get('type') == type; + }; + + u.isInstance = function (type, item) { + return item instanceof type; + }; + + u.getAttribute = function (key, item) { + return item.get(key); + }; + + u.contains.not = function (attr, query) { + return function (item) { + return !u.contains(attr, query)(item); + }; + }; + + u.createFragmentFromText = function (markup) { + /* Returns a DocumentFragment containing DOM nodes based on the + * passed-in markup text. + */ + // http://stackoverflow.com/questions/9334645/create-node-from-markup-string + var frag = document.createDocumentFragment(), + tmp = document.createElement('body'), + child; + tmp.innerHTML = markup; // Append elements in a loop to a DocumentFragment, so that the + // browser does not re-render the document for each node. + + while (child = tmp.firstChild) { + // eslint-disable-line no-cond-assign + frag.appendChild(child); + } + + return frag; + }; + + u.addEmoji = function (_converse, emojione, text) { + if (_converse.use_emojione) { + return emojione.toImage(text); + } else { + return emojione.shortnameToUnicode(text); + } + }; + + u.getEmojisByCategory = function (_converse, emojione) { + /* Return a dict of emojis with the categories as keys and + * lists of emojis in that category as values. + */ + if (_.isUndefined(_converse.emojis_by_category)) { + var emojis = _.values(_.mapValues(emojione.emojioneList, function (value, key, o) { + value._shortname = key; + return value; + })); + + var tones = [':tone1:', ':tone2:', ':tone3:', ':tone4:', ':tone5:']; + var excluded = [':kiss_ww:', ':kiss_mm:', ':kiss_woman_man:']; + var excluded_substrings = [':woman', ':man', ':women_', ':men_', '_man_', '_woman_', '_woman:', '_man:']; + var excluded_categories = ['modifier', 'regional']; + + var categories = _.difference(_.uniq(_.map(emojis, _.partial(_.get, _, 'category'))), excluded_categories); + + var emojis_by_category = {}; + + _.forEach(categories, function (cat) { + var list = _.sortBy(_.filter(emojis, ['category', cat]), ['uc_base']); + + list = _.filter(list, function (item) { + return !_.includes(_.concat(tones, excluded), item._shortname) && !_.some(excluded_substrings, _.partial(_.includes, item._shortname)); }); - return obj; - }; - u.slideInAllElements = function (elements) { - return Promise.all(_.map(elements, _.partial(u.slideIn, _, 600))); - }; + if (cat === 'people') { + var idx = _.findIndex(list, ['uc_base', '1f600']); - u.slideToggleElement = function (el) { - if (_.includes(el.classList, 'collapsed')) { - return u.slideOut(el); - } else { - return u.slideIn(el); + list = _.union(_.slice(list, idx), _.slice(list, 0, idx + 1)); + } else if (cat === 'activity') { + list = _.union(_.slice(list, 27 - 1), _.slice(list, 0, 27)); + } else if (cat === 'objects') { + list = _.union(_.slice(list, 24 - 1), _.slice(list, 0, 24)); + } else if (cat === 'travel') { + list = _.union(_.slice(list, 17 - 1), _.slice(list, 0, 17)); + } else if (cat === 'symbols') { + list = _.union(_.slice(list, 60 - 1), _.slice(list, 0, 60)); } - }; - u.slideOut = function (el) { - var duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 900; + emojis_by_category[cat] = list; + }); - /* Shows/expands an element by sliding it out of itself - * - * Parameters: - * (HTMLElement) el - The HTML string - * (Number) duration - The duration amount in milliseconds - */ - return new Promise(function (resolve, reject) { - if (_.isNil(el)) { - var err = "Undefined or null element passed into slideOut"; - logger.warn(err); - reject(new Error(err)); - return; - } - var interval_marker = el.getAttribute('data-slider-marker'); - if (interval_marker) { - el.removeAttribute('data-slider-marker'); - window.clearInterval(interval_marker); - } - var end_height = calculateElementHeight(el); - if (window.converse_disable_effects) { - // Effects are disabled (for tests) - el.style.height = end_height + 'px'; - slideOutWrapup(el); - resolve(); - return; - } + _converse.emojis_by_category = emojis_by_category; + } - var step = calculateSlideStep(end_height), - interval = end_height / duration * step; - var h = 0; + return _converse.emojis_by_category; + }; - interval_marker = window.setInterval(function () { - h += step; - if (h < end_height) { - el.style.height = h + 'px'; - } else { - // We recalculate the height to work around an apparent - // browser bug where browsers don't know the correct - // offsetHeight beforehand. - el.style.height = calculateElementHeight(el) + 'px'; - window.clearInterval(interval_marker); - slideOutWrapup(el); - resolve(); - } - }, interval); - el.setAttribute('data-slider-marker', interval_marker); - }); - }; + u.getTonedEmojis = function (_converse) { + _converse.toned_emojis = _.uniq(_.map(_.filter(u.getEmojisByCategory(_converse).people, function (person) { + return _.includes(person._shortname, '_tone'); + }), function (person) { + return person._shortname.replace(/_tone[1-5]/, ''); + })); + return _converse.toned_emojis; + }; - u.slideIn = function (el) { - var duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 600; + u.isPersistableModel = function (model) { + return model.collection && model.collection.browserStorage; + }; - /* Hides/collapses an element by sliding it into itself. */ - return new Promise(function (resolve, reject) { - if (_.isNil(el)) { - var err = "Undefined or null element passed into slideIn"; - logger.warn(err); - return reject(new Error(err)); - } else if (_.includes(el.classList, 'collapsed')) { - return resolve(); - } else if (window.converse_disable_effects) { - // Effects are disabled (for tests) - el.classList.add('collapsed'); - el.style.height = ""; - return resolve(); - } - var interval_marker = el.getAttribute('data-slider-marker'); - if (interval_marker) { - el.removeAttribute('data-slider-marker'); - window.clearInterval(interval_marker); - } - var h = el.offsetHeight; - var step = calculateSlideStep(h), - interval = h / duration * step; + u.getResolveablePromise = function () { + /* Returns a promise object on which `resolve` or `reject` can be + * called. + */ + var wrapper = {}; + var promise = new Promise(function (resolve, reject) { + wrapper.resolve = resolve; + wrapper.reject = reject; + }); - el.style.overflow = 'hidden'; + _.assign(promise, wrapper); - interval_marker = window.setInterval(function () { - h -= step; - if (h > 0) { - el.style.height = h + 'px'; - } else { - el.removeAttribute('data-slider-marker'); - window.clearInterval(interval_marker); - el.classList.add('collapsed'); - el.style.height = ""; - resolve(); - } - }, interval); - el.setAttribute('data-slider-marker', interval_marker); - }); - }; + return promise; + }; - u.fadeIn = function (el, callback) { - if (_.isNil(el)) { - logger.warn("Undefined or null element passed into fadeIn"); - } - if (window.converse_disable_effects) { - // Effects are disabled (for tests) - el.classList.remove('hidden'); - if (_.isFunction(callback)) { - callback(); - } - return; - } - if (_.includes(el.classList, 'hidden')) { - /* XXX: This doesn't appear to be working... - el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnd, el, callback), false); - el.addEventListener("animationend", _.partial(afterAnimationEnd, el, callback), false); - */ - setTimeout(_.partial(afterAnimationEnd, el, callback), 351); - el.classList.add('visible'); - el.classList.remove('hidden'); - } else { - afterAnimationEnd(el, callback); - } - }; + u.safeSave = function (model, attributes) { + if (u.isPersistableModel(model)) { + model.save(attributes); + } else { + model.set(attributes); + } + }; - u.isValidJID = function (jid) { - return _.filter(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@'); - }; + u.isVisible = function (el) { + // XXX: Taken from jQuery's "visible" implementation + return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0; + }; - u.isSameBareJID = function (jid1, jid2) { - return Strophe.getBareJidFromJid(jid1).toLowerCase() === Strophe.getBareJidFromJid(jid2).toLowerCase(); - }; - - u.isNewMessage = function (message) { - /* Given a stanza, determine whether it's a new - * message, i.e. not a MAM archived one. - */ - if (message instanceof Element) { - return !sizzle('result[xmlns="' + Strophe.NS.MAM + '"]', message).length; - } else { - return !message.get('archive_id'); - } - }; - - u.isOTRMessage = function (message) { - var body = message.querySelector('body'), - text = !_.isNull(body) ? body.textContent : undefined; - return text && !!text.match(/^\?OTR/); - }; - - u.isHeadlineMessage = function (message) { - var from_jid = message.getAttribute('from'); - if (message.getAttribute('type') === 'headline') { - return true; - } - if (message.getAttribute('type') !== 'error' && !_.isNil(from_jid) && !_.includes(from_jid, '@')) { - // Some servers (I'm looking at you Prosody) don't set the message - // type to "headline" when sending server messages. For now we - // check if an @ signal is included, and if not, we assume it's - // a headline message. - return true; - } - return false; - }; - - u.merge = function merge(first, second) { - /* Merge the second object into the first one. - */ - for (var k in second) { - if (_.isObject(first[k])) { - merge(first[k], second[k]); - } else { - first[k] = second[k]; - } - } - }; - - u.applyUserSettings = function applyUserSettings(context, settings, user_settings) { - /* Configuration settings might be nested objects. We only want to - * add settings which are whitelisted. - */ - for (var k in settings) { - if (_.isUndefined(user_settings[k])) { - continue; - } - if (_.isObject(settings[k]) && !_.isArray(settings[k])) { - applyUserSettings(context[k], settings[k], user_settings[k]); - } else { - context[k] = user_settings[k]; - } - } - }; - - u.refreshWebkit = function () { - /* This works around a webkit bug. Refreshes the browser's viewport, - * otherwise chatboxes are not moved along when one is closed. - */ - if (jQBrowser.webkit && window.requestAnimationFrame) { - window.requestAnimationFrame(function () { - var conversejs = document.getElementById('conversejs'); - conversejs.style.display = 'none'; - var tmp = conversejs.offsetHeight; // jshint ignore:line - conversejs.style.display = 'block'; - }); - } - }; - - u.stringToDOM = function (s) { - /* Converts an HTML string into a DOM element. - * - * Parameters: - * (String) s - The HTML string - */ - var div = document.createElement('div'); - div.innerHTML = s; - return div.childNodes; - }; - - u.matchesSelector = function (el, selector) { - /* Checks whether the DOM element matches the given selector. - * - * Parameters: - * (DOMElement) el - The DOM element - * (String) selector - The selector - */ - return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); - }; - - u.queryChildren = function (el, selector) { - /* Returns a list of children of the DOM element that match the - * selector. - * - * Parameters: - * (DOMElement) el - the DOM element - * (String) selector - the selector they should be matched - * against. - */ - return _.filter(el.children, _.partial(u.matchesSelector, _, selector)); - }; - - u.contains = function (attr, query) { - return function (item) { - if ((typeof attr === "undefined" ? "undefined" : _typeof(attr)) === 'object') { - var value = false; - _.forEach(attr, function (a) { - value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase()); - }); - return value; - } else if (typeof attr === 'string') { - return _.includes(item.get(attr).toLowerCase(), query.toLowerCase()); - } else { - throw new TypeError('contains: wrong attribute type. Must be string or array.'); - } - }; - }; - - u.isOfType = function (type, item) { - return item.get('type') == type; - }; - - u.isInstance = function (type, item) { - return item instanceof type; - }; - - u.getAttribute = function (key, item) { - return item.get(key); - }; - - u.contains.not = function (attr, query) { - return function (item) { - return !u.contains(attr, query)(item); - }; - }; - - u.createFragmentFromText = function (markup) { - /* Returns a DocumentFragment containing DOM nodes based on the - * passed-in markup text. - */ - // http://stackoverflow.com/questions/9334645/create-node-from-markup-string - var frag = document.createDocumentFragment(), - tmp = document.createElement('body'), - child; - tmp.innerHTML = markup; - // Append elements in a loop to a DocumentFragment, so that the - // browser does not re-render the document for each node. - while (child = tmp.firstChild) { - // eslint-disable-line no-cond-assign - frag.appendChild(child); - } - return frag; - }; - - u.addEmoji = function (_converse, emojione, text) { - if (_converse.use_emojione) { - return emojione.toImage(text); - } else { - return emojione.shortnameToUnicode(text); - } - }; - - u.getEmojisByCategory = function (_converse, emojione) { - /* Return a dict of emojis with the categories as keys and - * lists of emojis in that category as values. - */ - if (_.isUndefined(_converse.emojis_by_category)) { - var emojis = _.values(_.mapValues(emojione.emojioneList, function (value, key, o) { - value._shortname = key; - return value; - })); - var tones = [':tone1:', ':tone2:', ':tone3:', ':tone4:', ':tone5:']; - var excluded = [':kiss_ww:', ':kiss_mm:', ':kiss_woman_man:']; - var excluded_substrings = [':woman', ':man', ':women_', ':men_', '_man_', '_woman_', '_woman:', '_man:']; - var excluded_categories = ['modifier', 'regional']; - var categories = _.difference(_.uniq(_.map(emojis, _.partial(_.get, _, 'category'))), excluded_categories); - var emojis_by_category = {}; - _.forEach(categories, function (cat) { - var list = _.sortBy(_.filter(emojis, ['category', cat]), ['uc_base']); - list = _.filter(list, function (item) { - return !_.includes(_.concat(tones, excluded), item._shortname) && !_.some(excluded_substrings, _.partial(_.includes, item._shortname)); - }); - if (cat === 'people') { - var idx = _.findIndex(list, ['uc_base', '1f600']); - list = _.union(_.slice(list, idx), _.slice(list, 0, idx + 1)); - } else if (cat === 'activity') { - list = _.union(_.slice(list, 27 - 1), _.slice(list, 0, 27)); - } else if (cat === 'objects') { - list = _.union(_.slice(list, 24 - 1), _.slice(list, 0, 24)); - } else if (cat === 'travel') { - list = _.union(_.slice(list, 17 - 1), _.slice(list, 0, 17)); - } else if (cat === 'symbols') { - list = _.union(_.slice(list, 60 - 1), _.slice(list, 0, 60)); - } - emojis_by_category[cat] = list; - }); - _converse.emojis_by_category = emojis_by_category; - } - return _converse.emojis_by_category; - }; - - u.getTonedEmojis = function (_converse) { - _converse.toned_emojis = _.uniq(_.map(_.filter(u.getEmojisByCategory(_converse).people, function (person) { - return _.includes(person._shortname, '_tone'); - }), function (person) { - return person._shortname.replace(/_tone[1-5]/, ''); - })); - return _converse.toned_emojis; - }; - - u.isPersistableModel = function (model) { - return model.collection && model.collection.browserStorage; - }; - - u.getWrappedPromise = function () { - var wrapper = {}; - wrapper.promise = new Promise(function (resolve, reject) { - wrapper.resolve = resolve; - wrapper.reject = reject; - }); - return wrapper; - }; - - u.safeSave = function (model, attributes) { - if (u.isPersistableModel(model)) { - model.save(attributes); - } else { - model.set(attributes); - } - }; - return u; + return u; }); //# sourceMappingURL=utils.js.map; (function (global, factory) { @@ -7454,2359 +7518,2691 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol }); //# sourceMappingURL=pluggable.js.map; - - // 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) // + /*global Backbone, define, window, document, JSON */ (function (root, factory) { - define('converse-core',["sizzle", "es6-promise", "lodash.noconflict", "polyfill", "i18n", "utils", "moment", "strophe", "pluggable", "backbone.noconflict", "backbone.browserStorage", "backbone.overview"], factory); -})(undefined, function (sizzle, Promise, _, polyfill, i18n, utils, moment, Strophe, pluggable, Backbone) { + define('converse-core',["sizzle", "es6-promise", "lodash.noconflict", "polyfill", "i18n", "utils", "moment", "strophe", "pluggable", "backbone.noconflict", "backbone.browserStorage", "backbone.overview"], factory); +})(this, function (sizzle, Promise, _, polyfill, i18n, utils, moment, Strophe, pluggable, Backbone) { + /* Cannot use this due to Safari bug. + * See https://github.com/jcbrand/converse.js/issues/196 + */ + // "use strict"; + // Strophe globals + var _Strophe = Strophe, + $build = _Strophe.$build, + $iq = _Strophe.$iq, + $msg = _Strophe.$msg, + $pres = _Strophe.$pres; + var b64_sha1 = Strophe.SHA1.b64_sha1; + Strophe = Strophe.Strophe; // Add Strophe Namespaces - /* Cannot use this due to Safari bug. - * See https://github.com/jcbrand/converse.js/issues/196 + Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2'); + Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates'); + Strophe.addNamespace('CSI', 'urn:xmpp:csi:0'); + Strophe.addNamespace('DELAY', 'urn:xmpp:delay'); + Strophe.addNamespace('HINTS', 'urn:xmpp:hints'); + Strophe.addNamespace('MAM', 'urn:xmpp:mam:2'); + Strophe.addNamespace('SID', 'urn:xmpp:sid:0'); + Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick'); + Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub'); + Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx'); + Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm'); + Strophe.addNamespace('XFORM', 'jabber:x:data'); // 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. + */ + + _.templateSettings = { + 'escape': /\{\{\{([\s\S]+?)\}\}\}/g, + 'evaluate': /\{\[([\s\S]+?)\]\}/g, + 'interpolate': /\{\{([\s\S]+?)\}\}/g, + 'imports': { + '_': _ + } + }; + var _converse = { + 'templates': {}, + 'promises': {} + }; + + _.extend(_converse, Backbone.Events); + + _converse.core_plugins = ['converse-bookmarks', 'converse-chatboxes', 'converse-chatview', 'converse-controlbox', 'converse-core', 'converse-disco', 'converse-dragresize', 'converse-fullscreen', 'converse-headline', 'converse-mam', 'converse-minimize', 'converse-muc', 'converse-notification', 'converse-otr', 'converse-ping', 'converse-profile', 'converse-register', 'converse-roomslist', 'converse-rosterview', 'converse-singleton', 'converse-vcard']; // Make converse pluggable + + pluggable.enable(_converse, '_converse', 'pluggable'); // Module-level constants + + _converse.STATUS_WEIGHTS = { + 'offline': 6, + 'unavailable': 5, + 'xa': 4, + 'away': 3, + 'dnd': 2, + 'chat': 1, + // We currently don't differentiate between "chat" and "online" + 'online': 1 + }; + _converse.PRETTY_CHAT_STATUS = { + 'offline': 'Offline', + 'unavailable': 'Unavailable', + 'xa': 'Extended Away', + 'away': 'Away', + 'dnd': 'Do not disturb', + 'chat': 'Chattty', + 'online': 'Online' + }; + _converse.ANONYMOUS = "anonymous"; + _converse.CLOSED = 'closed'; + _converse.EXTERNAL = "external"; + _converse.LOGIN = "login"; + _converse.LOGOUT = "logout"; + _converse.OPENED = 'opened'; + _converse.PREBIND = "prebind"; + _converse.CONNECTION_STATUS = { + 0: 'ERROR', + 1: 'CONNECTING', + 2: 'CONNFAIL', + 3: 'AUTHENTICATING', + 4: 'AUTHFAIL', + 5: 'CONNECTED', + 6: 'DISCONNECTED', + 7: 'DISCONNECTING', + 8: 'ATTACHED', + 9: 'REDIRECT', + 10: 'RECONNECTING' + }; + _converse.DEFAULT_IMAGE_TYPE = 'image/png'; + _converse.DEFAULT_IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAIAAABt+uBvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gwHCy455JBsggAABkJJREFUeNrtnM1PE1sUwHvvTD8otWLHST/Gimi1CEgr6M6FEWuIBo2pujDVsNDEP8GN/4MbN7oxrlipG2OCgZgYlxAbkRYw1KqkIDRCSkM7nXvvW8x7vjyNeQ9m7p1p3z1LQk/v/Dhz7vkEXL161cHl9wI5Ag6IA+KAOCAOiAPigDggLhwQB2S+iNZ+PcYY/SWEEP2HAAAIoSAIoihCCP+ngDDGtVotGAz29/cfOXJEUZSOjg6n06lp2sbGRqlUWlhYyGazS0tLbrdbEASrzgksyeYJId3d3el0uqenRxRFAAAA4KdfIIRgjD9+/Pj8+fOpqSndslofEIQwHA6Pjo4mEon//qmFhYXHjx8vLi4ihBgDEnp7e9l8E0Jo165dQ0NDd+/eDYVC2/qsJElDQ0OEkKWlpa2tLZamxAhQo9EIBoOjo6MXL17csZLe3l5FUT59+lQul5l5JRaAVFWNRqN37tw5ceKEQVWRSOTw4cOFQuHbt2+iKLYCIISQLMu3b99OJpOmKAwEAgcPHszn8+vr6wzsiG6UQQhxuVyXLl0aGBgwUW0sFstkMl6v90fo1KyAMMYDAwPnzp0zXfPg4GAqlWo0Gk0MiBAiy/L58+edTqf5Aa4onj59OhaLYYybFRCEMBaL0fNxBw4cSCQStN0QRUBut3t4eJjq6U+dOiVJElVPRBFQIBDo6+ujCqirqyscDlONGykC2lYyYSR6pBoQQapHZwAoHo/TuARYAOrs7GQASFEUqn6aIiBJkhgA6ujooFpUo6iaTa7koFwnaoWadLNe81tbWwzoaJrWrICWl5cZAFpbW6OabVAEtLi4yABQsVjUNK0pAWWzWQaAcrlcswKanZ1VVZUqHYRQEwOq1Wpv3ryhCmh6erpcLjdrNl+v1ycnJ+l5UELI27dvv3//3qxxEADgy5cvExMT9Mznw4cPtFtAdAPFarU6Pj5eKpVM17yxsfHy5cvV1VXazXu62gVBKBQKT58+rdVqJqrFGL948eLdu3dU8/g/H4FBUaJYLAqC0NPTY9brMD4+PjY25mDSracOCABACJmZmXE6nUePHjWu8NWrV48ePSKEsGlAs7Agfd5nenq6Wq0mk0kjDzY2NvbkyRMIIbP2PLvhBUEQ8vl8NpuNx+M+n29bzhVjvLKycv/+/YmJCcazQuwA6YzW1tYmJyf1SY+2trZ/rRk1Go1SqfT69esHDx4UCgVmNaa/zZ/9ABUhRFXVYDB48uTJeDweiUQkSfL7/T9MA2NcqVTK5fLy8vL8/PzU1FSxWHS5XJaM4wGr9sUwxqqqer3eUCgkSZJuUBBCfTRvc3OzXC6vrKxUKhWn02nhCJ5lM4oQQo/HgxD6+vXr58+fHf8sDOp+HQDg8XgclorFU676dKLlo6yWRdItIBwQB8QBcUCtfosRQjRNQwhhjPUC4w46WXryBSHU1zgEQWBz99EFhDGu1+t+v//48ePxeFxRlD179ng8nh0Efgiher2+vr6ur3HMzMysrq7uTJVdACGEurq6Ll++nEgkPB7Pj9jPoDHqOxyqqubz+WfPnuVyuV9XPeyeagAAAoHArVu3BgcHab8CuVzu4cOHpVKJUnfA5GweY+xyuc6cOXPv3r1IJMLAR8iyPDw8XK/Xi8Wiqqqmm5KZgBBC7e3tN27cuHbtGuPVpf7+/lAoNDs7W61WzfVKpgHSSzw3b95MpVKW3MfRaDQSiczNzVUqFRMZmQOIEOL1eq9fv3727FlL1t50URRFluX5+flqtWpWEGAOIFEUU6nUlStXLKSjy759+xwOx9zcnKZpphzGHMzhcDiTydgk9r1w4YIp7RPTAAmCkMlk2FeLf/tIEKbTab/fbwtAhJBoNGrutpNx6e7uPnTokC1eMU3T0um0DZPMkZER6wERQnw+n/FFSxpy7Nix3bt3WwwIIcRgIWnHkkwmjecfRgGx7DtuV/r6+iwGhDHev3+/bQF1dnYaH6E2CkiWZdsC2rt3r8WAHA5HW1ubbQGZcjajgOwTH/4qNko1Wlg4IA6IA+KAOKBWBUQIsfNojyliKIoRRfH9+/dut9umf3wzpoUNNQ4BAJubmwz+ic+OxefzWWlBhJD29nbug7iT5sIBcUAcEAfEAXFAHBAHxOVn+QMrmWpuPZx12gAAAABJRU5ErkJggg=="; + + _converse.log = function (message, level) { + var style = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; + + /* Logs messages to the browser's developer console. + * + * Parameters: + * (String) message - The message to be logged. + * (Integer) level - The loglevel which allows for filtering of log + * messages. + * + * Available loglevels are 0 for 'debug', 1 for 'info', 2 for 'warn', + * 3 for 'error' and 4 for 'fatal'. + * + * When using the 'error' or 'warn' loglevels, a full stacktrace will be + * logged as well. */ - // "use strict"; - - // Strophe globals - var _Strophe = Strophe, - $build = _Strophe.$build, - $iq = _Strophe.$iq, - $msg = _Strophe.$msg, - $pres = _Strophe.$pres; - - var b64_sha1 = Strophe.SHA1.b64_sha1; - Strophe = Strophe.Strophe; - - // Add Strophe Namespaces - Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2'); - Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates'); - Strophe.addNamespace('CSI', 'urn:xmpp:csi:0'); - Strophe.addNamespace('DELAY', 'urn:xmpp:delay'); - Strophe.addNamespace('HINTS', 'urn:xmpp:hints'); - Strophe.addNamespace('MAM', 'urn:xmpp:mam:2'); - Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick'); - Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub'); - Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx'); - Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm'); - Strophe.addNamespace('XFORM', 'jabber:x:data'); - - // 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. - */ - _.templateSettings = { - 'escape': /\{\{\{([\s\S]+?)\}\}\}/g, - 'evaluate': /\{\[([\s\S]+?)\]\}/g, - 'interpolate': /\{\{([\s\S]+?)\}\}/g, - 'imports': { '_': _ } - }; - - var _converse = { - 'templates': {}, - 'promises': {} - }; - - _.extend(_converse, Backbone.Events); - - _converse.core_plugins = ['converse-bookmarks', 'converse-chatboxes', 'converse-chatview', 'converse-controlbox', 'converse-core', 'converse-disco', 'converse-dragresize', 'converse-fullscreen', 'converse-headline', 'converse-mam', 'converse-minimize', 'converse-muc', 'converse-notification', 'converse-otr', 'converse-ping', 'converse-register', 'converse-roomslist', 'converse-rosterview', 'converse-singleton', 'converse-vcard']; - - // Make converse pluggable - pluggable.enable(_converse, '_converse', 'pluggable'); - - // Module-level constants - _converse.STATUS_WEIGHTS = { - 'offline': 6, - 'unavailable': 5, - 'xa': 4, - 'away': 3, - 'dnd': 2, - 'chat': 1, // We currently don't differentiate between "chat" and "online" - 'online': 1 - }; - _converse.PRETTY_CHAT_STATUS = { - 'offline': 'Offline', - 'unavailable': 'Unavailable', - 'xa': 'Extended Away', - 'away': 'Away', - 'dnd': 'Do not disturb', - 'chat': 'Chattty', - 'online': 'Online' - }; - _converse.ANONYMOUS = "anonymous"; - _converse.CLOSED = 'closed'; - _converse.EXTERNAL = "external"; - _converse.LOGIN = "login"; - _converse.LOGOUT = "logout"; - _converse.OPENED = 'opened'; - _converse.PREBIND = "prebind"; - - _converse.CONNECTION_STATUS = { - 0: 'ERROR', - 1: 'CONNECTING', - 2: 'CONNFAIL', - 3: 'AUTHENTICATING', - 4: 'AUTHFAIL', - 5: 'CONNECTED', - 6: 'DISCONNECTED', - 7: 'DISCONNECTING', - 8: 'ATTACHED', - 9: 'REDIRECT', - 10: 'RECONNECTING' - }; - - _converse.DEFAULT_IMAGE_TYPE = 'image/png'; - _converse.DEFAULT_IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAIAAABt+uBvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gwHCy455JBsggAABkJJREFUeNrtnM1PE1sUwHvvTD8otWLHST/Gimi1CEgr6M6FEWuIBo2pujDVsNDEP8GN/4MbN7oxrlipG2OCgZgYlxAbkRYw1KqkIDRCSkM7nXvvW8x7vjyNeQ9m7p1p3z1LQk/v/Dhz7vkEXL161cHl9wI5Ag6IA+KAOCAOiAPigDggLhwQB2S+iNZ+PcYY/SWEEP2HAAAIoSAIoihCCP+ngDDGtVotGAz29/cfOXJEUZSOjg6n06lp2sbGRqlUWlhYyGazS0tLbrdbEASrzgksyeYJId3d3el0uqenRxRFAAAA4KdfIIRgjD9+/Pj8+fOpqSndslofEIQwHA6Pjo4mEon//qmFhYXHjx8vLi4ihBgDEnp7e9l8E0Jo165dQ0NDd+/eDYVC2/qsJElDQ0OEkKWlpa2tLZamxAhQo9EIBoOjo6MXL17csZLe3l5FUT59+lQul5l5JRaAVFWNRqN37tw5ceKEQVWRSOTw4cOFQuHbt2+iKLYCIISQLMu3b99OJpOmKAwEAgcPHszn8+vr6wzsiG6UQQhxuVyXLl0aGBgwUW0sFstkMl6v90fo1KyAMMYDAwPnzp0zXfPg4GAqlWo0Gk0MiBAiy/L58+edTqf5Aa4onj59OhaLYYybFRCEMBaL0fNxBw4cSCQStN0QRUBut3t4eJjq6U+dOiVJElVPRBFQIBDo6+ujCqirqyscDlONGykC2lYyYSR6pBoQQapHZwAoHo/TuARYAOrs7GQASFEUqn6aIiBJkhgA6ujooFpUo6iaTa7koFwnaoWadLNe81tbWwzoaJrWrICWl5cZAFpbW6OabVAEtLi4yABQsVjUNK0pAWWzWQaAcrlcswKanZ1VVZUqHYRQEwOq1Wpv3ryhCmh6erpcLjdrNl+v1ycnJ+l5UELI27dvv3//3qxxEADgy5cvExMT9Mznw4cPtFtAdAPFarU6Pj5eKpVM17yxsfHy5cvV1VXazXu62gVBKBQKT58+rdVqJqrFGL948eLdu3dU8/g/H4FBUaJYLAqC0NPTY9brMD4+PjY25mDSracOCABACJmZmXE6nUePHjWu8NWrV48ePSKEsGlAs7Agfd5nenq6Wq0mk0kjDzY2NvbkyRMIIbP2PLvhBUEQ8vl8NpuNx+M+n29bzhVjvLKycv/+/YmJCcazQuwA6YzW1tYmJyf1SY+2trZ/rRk1Go1SqfT69esHDx4UCgVmNaa/zZ/9ABUhRFXVYDB48uTJeDweiUQkSfL7/T9MA2NcqVTK5fLy8vL8/PzU1FSxWHS5XJaM4wGr9sUwxqqqer3eUCgkSZJuUBBCfTRvc3OzXC6vrKxUKhWn02nhCJ5lM4oQQo/HgxD6+vXr58+fHf8sDOp+HQDg8XgclorFU676dKLlo6yWRdItIBwQB8QBcUCtfosRQjRNQwhhjPUC4w46WXryBSHU1zgEQWBz99EFhDGu1+t+v//48ePxeFxRlD179ng8nh0Efgiher2+vr6ur3HMzMysrq7uTJVdACGEurq6Ll++nEgkPB7Pj9jPoDHqOxyqqubz+WfPnuVyuV9XPeyeagAAAoHArVu3BgcHab8CuVzu4cOHpVKJUnfA5GweY+xyuc6cOXPv3r1IJMLAR8iyPDw8XK/Xi8Wiqqqmm5KZgBBC7e3tN27cuHbtGuPVpf7+/lAoNDs7W61WzfVKpgHSSzw3b95MpVKW3MfRaDQSiczNzVUqFRMZmQOIEOL1eq9fv3727FlL1t50URRFluX5+flqtWpWEGAOIFEUU6nUlStXLKSjy759+xwOx9zcnKZpphzGHMzhcDiTydgk9r1w4YIp7RPTAAmCkMlk2FeLf/tIEKbTab/fbwtAhJBoNGrutpNx6e7uPnTokC1eMU3T0um0DZPMkZER6wERQnw+n/FFSxpy7Nix3bt3WwwIIcRgIWnHkkwmjecfRgGx7DtuV/r6+iwGhDHev3+/bQF1dnYaH6E2CkiWZdsC2rt3r8WAHA5HW1ubbQGZcjajgOwTH/4qNko1Wlg4IA6IA+KAOKBWBUQIsfNojyliKIoRRfH9+/dut9umf3wzpoUNNQ4BAJubmwz+ic+OxefzWWlBhJD29nbug7iT5sIBcUAcEAfEAXFAHBAHxOVn+QMrmWpuPZx12gAAAABJRU5ErkJggg=="; - - _converse.log = function (message, level) { - var style = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; - - /* Logs messages to the browser's developer console. - * - * Parameters: - * (String) message - The message to be logged. - * (Integer) level - The loglevel which allows for filtering of log - * messages. - * - * Available loglevels are 0 for 'debug', 1 for 'info', 2 for 'warn', - * 3 for 'error' and 4 for 'fatal'. - * - * When using the 'error' or 'warn' loglevels, a full stacktrace will be - * logged as well. - */ - if (level === Strophe.LogLevel.ERROR || level === Strophe.LogLevel.FATAL) { - style = style || 'color: maroon'; - } - if (message instanceof Error) { - message = message.stack; - } - var prefix = style ? '%c' : ''; - var logger = _.assign({ - 'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop, - 'error': _.get(console, 'log') ? console.log.bind(console) : _.noop, - 'info': _.get(console, 'log') ? console.log.bind(console) : _.noop, - 'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop - }, console); - if (level === Strophe.LogLevel.ERROR) { - if (_converse.debug) { - logger.trace(prefix + " " + moment().format() + " ERROR: " + message, style); - } else { - logger.error(prefix + " ERROR: " + message, style); - } - } else if (level === Strophe.LogLevel.WARN) { - if (_converse.debug) { - logger.warn(prefix + " " + moment().format() + " WARNING: " + message, style); - } else { - logger.warn(prefix + " WARNING: " + message, style); - } - } else if (level === Strophe.LogLevel.FATAL) { - if (_converse.debug) { - logger.trace(prefix + " " + moment().format() + " FATAL: " + message, style); - } else { - logger.error(prefix + " FATAL: " + message, style); - } - } else if (_converse.debug) { - if (level === Strophe.LogLevel.DEBUG) { - logger.debug(prefix + " " + moment().format() + " DEBUG: " + message, style); - } else { - logger.info(prefix + " " + moment().format() + " INFO: " + message, style); - } - } - }; - - Strophe.log = function (level, msg) { - _converse.log(level + ' ' + msg, level); - }; - Strophe.error = function (msg) { - _converse.log(msg, Strophe.LogLevel.ERROR); - }; - - _converse.__ = function (str) { - /* Translate the given string based on the current locale. - * - * Parameters: - * (String) str - The string to translate. - */ - if (_.isUndefined(i18n)) { - return str; - } - return i18n.translate.apply(i18n, arguments); - }; - - var __ = _converse.__; - - var PROMISES = ['initialized', 'cachedRoster', 'connectionInitialized', 'pluginsInitialized', 'roster', 'rosterContactsFetched', 'rosterGroupsFetched', 'rosterInitialized', 'statusInitialized']; - - function addPromise(promise) { - /* Private function, used to add a new promise to the ones already - * available via the `waitUntil` api method. - */ - _converse.promises[promise] = utils.getWrappedPromise(); + if (level === Strophe.LogLevel.ERROR || level === Strophe.LogLevel.FATAL) { + style = style || 'color: maroon'; } - _converse.emit = function (name) { - /* Event emitter and promise resolver */ - _converse.trigger.apply(this, arguments); - var promise = _converse.promises[name]; - if (!_.isUndefined(promise)) { - promise.resolve(); - } + if (message instanceof Error) { + message = message.stack; + } + + var prefix = style ? '%c' : ''; + + var logger = _.assign({ + 'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop, + 'error': _.get(console, 'log') ? console.log.bind(console) : _.noop, + 'info': _.get(console, 'log') ? console.log.bind(console) : _.noop, + 'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop + }, console); + + if (level === Strophe.LogLevel.ERROR) { + if (_converse.debug) { + logger.trace("".concat(prefix, " ").concat(moment().format(), " ERROR: ").concat(message), style); + } else { + logger.error("".concat(prefix, " ERROR: ").concat(message), style); + } + } else if (level === Strophe.LogLevel.WARN) { + if (_converse.debug) { + logger.warn("".concat(prefix, " ").concat(moment().format(), " WARNING: ").concat(message), style); + } else { + logger.warn("".concat(prefix, " WARNING: ").concat(message), style); + } + } else if (level === Strophe.LogLevel.FATAL) { + if (_converse.debug) { + logger.trace("".concat(prefix, " ").concat(moment().format(), " FATAL: ").concat(message), style); + } else { + logger.error("".concat(prefix, " FATAL: ").concat(message), style); + } + } else if (_converse.debug) { + if (level === Strophe.LogLevel.DEBUG) { + logger.debug("".concat(prefix, " ").concat(moment().format(), " DEBUG: ").concat(message), style); + } else { + logger.info("".concat(prefix, " ").concat(moment().format(), " INFO: ").concat(message), style); + } + } + }; + + Strophe.log = function (level, msg) { + _converse.log(level + ' ' + msg, level); + }; + + Strophe.error = function (msg) { + _converse.log(msg, Strophe.LogLevel.ERROR); + }; + + _converse.__ = function (str) { + /* Translate the given string based on the current locale. + * + * Parameters: + * (String) str - The string to translate. + */ + if (_.isUndefined(i18n)) { + return str; + } + + return i18n.translate.apply(i18n, arguments); + }; + + var __ = _converse.__; + var PROMISES = ['initialized', 'cachedRoster', 'connectionInitialized', 'pluginsInitialized', 'roster', 'rosterContactsFetched', 'rosterGroupsFetched', 'rosterInitialized', 'statusInitialized']; + + function addPromise(promise) { + /* Private function, used to add a new promise to the ones already + * available via the `waitUntil` api method. + */ + _converse.promises[promise] = utils.getResolveablePromise(); + } + + _converse.emit = function (name) { + /* Event emitter and promise resolver */ + _converse.trigger.apply(this, arguments); + + var promise = _converse.promises[name]; + + if (!_.isUndefined(promise)) { + promise.resolve(); + } + }; + + _converse.router = new Backbone.Router(); + + _converse.initialize = function (settings, callback) { + "use strict"; + + var _this = this; + + settings = !_.isUndefined(settings) ? settings : {}; + var init_promise = utils.getResolveablePromise(); + + _.each(PROMISES, addPromise); + + if (!_.isUndefined(_converse.connection)) { + // Looks like _converse.initialized was called again without logging + // out or disconnecting in the previous session. + // This happens in tests. We therefore first clean up. + Backbone.history.stop(); + delete _converse.controlboxtoggle; + + _converse.connection.reset(); + + _converse.off(); + + _converse.stopListening(); + + _converse._tearDown(); + } + + var unloadevent; + + if ('onpagehide' in window) { + // Pagehide gets thrown in more cases than unload. Specifically it + // gets thrown when the page is cached and not just + // closed/destroyed. It's the only viable event on mobile Safari. + // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/ + unloadevent = 'pagehide'; + } else if ('onbeforeunload' in window) { + unloadevent = 'beforeunload'; + } else if ('onunload' in window) { + unloadevent = 'unload'; + } // Instance level constants + + + this.TIMEOUTS = { + // Set as module attr so that we can override in tests. + 'PAUSED': 10000, + 'INACTIVE': 90000 + }; // XEP-0085 Chat states + // http://xmpp.org/extensions/xep-0085.html + + this.INACTIVE = 'inactive'; + this.ACTIVE = 'active'; + this.COMPOSING = 'composing'; + this.PAUSED = 'paused'; + this.GONE = 'gone'; // Default configuration values + // ---------------------------- + + this.default_settings = { + allow_contact_requests: true, + allow_non_roster_messaging: false, + animate: true, + authentication: 'login', + // Available values are "login", "prebind", "anonymous" and "external". + auto_away: 0, + // Seconds after which user status is set to 'away' + auto_login: false, + // Currently only used in connection with anonymous login + auto_reconnect: false, + auto_subscribe: false, + auto_xa: 0, + // Seconds after which user status is set to 'xa' + blacklisted_plugins: [], + bosh_service_url: undefined, + connection_options: {}, + credentials_url: null, + // URL from where login credentials can be fetched + csi_waiting_time: 0, + // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out. + debug: false, + default_state: 'online', + expose_rid_and_sid: false, + filter_by_resource: false, + forward_messages: false, + hide_offline_users: false, + include_offline_state: false, + jid: undefined, + keepalive: true, + locales_url: '/locale/{{{locale}}}/LC_MESSAGES/converse.json', + locales: ['af', 'ca', 'de', 'es', 'en', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'nb', 'nl', 'pl', 'pt_BR', 'ru', 'uk', 'zh'], + message_carbons: true, + message_storage: 'session', + password: undefined, + prebind_url: null, + priority: 0, + registration_domain: '', + rid: undefined, + roster_groups: true, + show_only_online_users: false, + show_send_button: false, + sid: undefined, + storage: 'session', + strict_plugin_dependencies: false, + synchronize_availability: true, + view_mode: 'overlayed', + // Choices are 'overlayed', 'fullscreen', 'mobile' + websocket_url: undefined, + whitelisted_plugins: [], + xhr_custom_status: false, + xhr_custom_status_url: '' }; - _converse.router = new Backbone.Router(); + _.assignIn(this, this.default_settings); // Allow only whitelisted configuration attributes to be overwritten - _converse.initialize = function (settings, callback) { - "use strict"; - var _this = this; + _.assignIn(this, _.pick(settings, _.keys(this.default_settings))); - settings = !_.isUndefined(settings) ? settings : {}; - var init_promise = utils.getWrappedPromise(); + if (this.authentication === _converse.ANONYMOUS) { + if (this.auto_login && !this.jid) { + throw new Error("Config Error: you need to provide the server's " + "domain via the 'jid' option when using anonymous " + "authentication with auto_login."); + } + } + /* Localisation */ - _.each(PROMISES, addPromise); - if (!_.isUndefined(_converse.connection)) { - // Looks like _converse.initialized was called again without logging - // out or disconnecting in the previous session. - // This happens in tests. We therefore first clean up. - Backbone.history.stop(); - delete _converse.controlboxtoggle; - _converse.connection.reset(); - _converse.off(); - _converse.stopListening(); - _converse._tearDown(); - } + if (!_.isUndefined(i18n)) { + i18n.setLocales(settings.i18n, _converse); + } else { + _converse.locale = 'en'; + } // Module-level variables + // ---------------------- - var unloadevent = void 0; - if ('onpagehide' in window) { - // Pagehide gets thrown in more cases than unload. Specifically it - // gets thrown when the page is cached and not just - // closed/destroyed. It's the only viable event on mobile Safari. - // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/ - unloadevent = 'pagehide'; - } else if ('onbeforeunload' in window) { - unloadevent = 'beforeunload'; - } else if ('onunload' in window) { - unloadevent = 'unload'; - } - // Instance level constants - this.TIMEOUTS = { // Set as module attr so that we can override in tests. - 'PAUSED': 10000, - 'INACTIVE': 90000 - }; + this.callback = callback || _.noop; + /* When reloading the page: + * For new sessions, we need to send out a presence stanza to notify + * the server/network that we're online. + * When re-attaching to an existing session (e.g. via the keepalive + * option), we don't need to again send out a presence stanza, because + * it's as if "we never left" (see onConnectStatusChanged). + * https://github.com/jcbrand/converse.js/issues/521 + */ - // XEP-0085 Chat states - // http://xmpp.org/extensions/xep-0085.html - this.INACTIVE = 'inactive'; - this.ACTIVE = 'active'; - this.COMPOSING = 'composing'; - this.PAUSED = 'paused'; - this.GONE = 'gone'; + this.send_initial_presence = true; + this.msg_counter = 0; + this.user_settings = settings; // Save the user settings so that they can be used by plugins + // Module-level functions + // ---------------------- - // Default configuration values - // ---------------------------- - this.default_settings = { - allow_contact_requests: true, - allow_non_roster_messaging: false, - animate: true, - authentication: 'login', // Available values are "login", "prebind", "anonymous" and "external". - auto_away: 0, // Seconds after which user status is set to 'away' - auto_login: false, // Currently only used in connection with anonymous login - auto_reconnect: false, - auto_subscribe: false, - auto_xa: 0, // Seconds after which user status is set to 'xa' - blacklisted_plugins: [], - bosh_service_url: undefined, - connection_options: {}, - credentials_url: null, // URL from where login credentials can be fetched - csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out. - debug: false, - default_state: 'online', - expose_rid_and_sid: false, - filter_by_resource: false, - forward_messages: false, - hide_offline_users: false, - include_offline_state: false, - jid: undefined, - keepalive: true, - locales_url: '/locale/{{{locale}}}/LC_MESSAGES/converse.json', - locales: ['af', 'ca', 'de', 'es', 'en', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'nb', 'nl', 'pl', 'pt_BR', 'ru', 'uk', 'zh'], - message_carbons: true, - message_storage: 'session', - password: undefined, - prebind_url: null, - priority: 0, - registration_domain: '', - rid: undefined, - roster_groups: true, - show_only_online_users: false, - show_send_button: false, - sid: undefined, - storage: 'session', - strict_plugin_dependencies: false, - synchronize_availability: true, - view_mode: 'overlayed', // Choices are 'overlayed', 'fullscreen', 'mobile' - websocket_url: undefined, - whitelisted_plugins: [], - xhr_custom_status: false, - xhr_custom_status_url: '' - }; - _.assignIn(this, this.default_settings); - // Allow only whitelisted configuration attributes to be overwritten - _.assignIn(this, _.pick(settings, _.keys(this.default_settings))); + this.generateResource = function () { + return "/converse.js-".concat(Math.floor(Math.random() * 139749825).toString()); + }; - if (this.authentication === _converse.ANONYMOUS) { - if (this.auto_login && !this.jid) { - throw new Error("Config Error: you need to provide the server's " + "domain via the 'jid' option when using anonymous " + "authentication with auto_login."); - } - } + this.sendCSI = function (stat) { + /* Send out a Chat Status Notification (XEP-0352) + * + * Parameters: + * (String) stat: The user's chat status + */ - /* Localisation */ - if (!_.isUndefined(i18n)) { - i18n.setLocales(settings.i18n, _converse); + /* Send out a Chat Status Notification (XEP-0352) */ + // XXX if (converse.features[Strophe.NS.CSI] || true) { + _converse.connection.send($build(stat, { + xmlns: Strophe.NS.CSI + })); + + _converse.inactive = stat === _converse.INACTIVE ? true : false; + }; + + this.onUserActivity = function () { + /* Resets counters and flags relating to CSI and auto_away/auto_xa */ + if (_converse.idle_seconds > 0) { + _converse.idle_seconds = 0; + } + + if (!_converse.connection.authenticated) { + // We can't send out any stanzas when there's no authenticated connection. + // converse can happen when the connection reconnects. + return; + } + + if (_converse.inactive) { + _converse.sendCSI(_converse.ACTIVE); + } + + if (_converse.auto_changed_status === true) { + _converse.auto_changed_status = false; // XXX: we should really remember the original state here, and + // then set it back to that... + + _converse.xmppstatus.setStatus(_converse.default_state); + } + }; + + this.onEverySecond = function () { + /* An interval handler running every second. + * Used for CSI and the auto_away and auto_xa features. + */ + if (!_converse.connection.authenticated) { + // We can't send out any stanzas when there's no authenticated connection. + // This can happen when the connection reconnects. + return; + } + + var stat = _converse.xmppstatus.getStatus(); + + _converse.idle_seconds++; + + if (_converse.csi_waiting_time > 0 && _converse.idle_seconds > _converse.csi_waiting_time && !_converse.inactive) { + _converse.sendCSI(_converse.INACTIVE); + } + + if (_converse.auto_away > 0 && _converse.idle_seconds > _converse.auto_away && stat !== 'away' && stat !== 'xa' && stat !== 'dnd') { + _converse.auto_changed_status = true; + + _converse.xmppstatus.setStatus('away'); + } else if (_converse.auto_xa > 0 && _converse.idle_seconds > _converse.auto_xa && stat !== 'xa' && stat !== 'dnd') { + _converse.auto_changed_status = true; + + _converse.xmppstatus.setStatus('xa'); + } + }; + + this.registerIntervalHandler = function () { + /* Set an interval of one second and register a handler for it. + * Required for the auto_away, auto_xa and csi_waiting_time features. + */ + if (_converse.auto_away < 1 && _converse.auto_xa < 1 && _converse.csi_waiting_time < 1) { + // Waiting time of less then one second means features aren't used. + return; + } + + _converse.idle_seconds = 0; + _converse.auto_changed_status = false; // Was the user's status changed by _converse.js? + + window.addEventListener('click', _converse.onUserActivity); + window.addEventListener('focus', _converse.onUserActivity); + window.addEventListener('keypress', _converse.onUserActivity); + window.addEventListener('mousemove', _converse.onUserActivity); + window.addEventListener(unloadevent, _converse.onUserActivity); + _converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000); + }; + + this.setConnectionStatus = function (connection_status, message) { + _converse.connfeedback.set({ + 'connection_status': connection_status, + 'message': message + }); + }; + + this.rejectPresenceSubscription = function (jid, message) { + /* Reject or cancel another user's subscription to our presence updates. + * + * Parameters: + * (String) jid - The Jabber ID of the user whose subscription + * is being canceled. + * (String) message - An optional message to the user + */ + var pres = $pres({ + to: jid, + type: "unsubscribed" + }); + + if (message && message !== "") { + pres.c("status").t(message); + } + + _converse.connection.send(pres); + }; + + this.reconnect = _.debounce(function () { + _converse.log('RECONNECTING'); + + _converse.log('The connection has dropped, attempting to reconnect.'); + + _converse.setConnectionStatus(Strophe.Status.RECONNECTING, __('The connection has dropped, attempting to reconnect.')); + + _converse.connection.reconnecting = true; + + _converse._tearDown(); + + _converse.logIn(null, true); + }, 3000, { + 'leading': true + }); + + this.disconnect = function () { + _converse.log('DISCONNECTED'); + + delete _converse.connection.reconnecting; + + _converse.connection.reset(); + + _converse._tearDown(); + + _converse.emit('disconnected'); + }; + + this.onDisconnected = function () { + /* Gets called once strophe's status reaches Strophe.Status.DISCONNECTED. + * Will either start a teardown process for converse.js or attempt + * to reconnect. + */ + var reason = _converse.disconnection_reason; + + if (_converse.disconnection_cause === Strophe.Status.AUTHFAIL) { + if (_converse.credentials_url && _converse.auto_reconnect) { + /* In this case, we reconnect, because we might be receiving + * expirable tokens from the credentials_url. + */ + _converse.emit('will-reconnect'); + + return _converse.reconnect(); } else { - _converse.locale = 'en'; + return _converse.disconnect(); + } + } else if (_converse.disconnection_cause === _converse.LOGOUT || !_.isUndefined(reason) && reason === _.get(Strophe, 'ErrorCondition.NO_AUTH_MECH') || reason === "host-unknown" || reason === "remote-connection-failed" || !_converse.auto_reconnect) { + return _converse.disconnect(); + } + + _converse.emit('will-reconnect'); + + _converse.reconnect(); + }; + + this.setDisconnectionCause = function (cause, reason, override) { + /* Used to keep track of why we got disconnected, so that we can + * decide on what the next appropriate action is (in onDisconnected) + */ + if (_.isUndefined(cause)) { + delete _converse.disconnection_cause; + delete _converse.disconnection_reason; + } else if (_.isUndefined(_converse.disconnection_cause) || override) { + _converse.disconnection_cause = cause; + _converse.disconnection_reason = reason; + } + }; + + this.onConnectStatusChanged = function (status, message) { + /* Callback method called by Strophe as the Strophe.Connection goes + * through various states while establishing or tearing down a + * connection. + */ + _converse.log("Status changed to: ".concat(_converse.CONNECTION_STATUS[status])); + + if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) { + _converse.setConnectionStatus(status); // By default we always want to send out an initial presence stanza. + + + _converse.send_initial_presence = true; + + _converse.setDisconnectionCause(); + + if (_converse.connection.reconnecting) { + _converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached'); + + _converse.onConnected(true); + } else { + _converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached'); + + if (_converse.connection.restored) { + // No need to send an initial presence stanza when + // we're restoring an existing session. + _converse.send_initial_presence = false; + } + + _converse.onConnected(); + } + } else if (status === Strophe.Status.DISCONNECTED) { + _converse.setDisconnectionCause(status, message); + + _converse.onDisconnected(); + } else if (status === Strophe.Status.ERROR) { + _converse.setConnectionStatus(status, __('An error occurred while connecting to the chat server.')); + } else if (status === Strophe.Status.CONNECTING) { + _converse.setConnectionStatus(status); + } else if (status === Strophe.Status.AUTHENTICATING) { + _converse.setConnectionStatus(status); + } else if (status === Strophe.Status.AUTHFAIL) { + if (!message) { + message = __('Your Jabber ID and/or password is incorrect. Please try again.'); } - // Module-level variables - // ---------------------- - this.callback = callback || _.noop; - /* When reloading the page: - * For new sessions, we need to send out a presence stanza to notify - * the server/network that we're online. - * When re-attaching to an existing session (e.g. via the keepalive - * option), we don't need to again send out a presence stanza, because - * it's as if "we never left" (see onConnectStatusChanged). - * https://github.com/jcbrand/converse.js/issues/521 + _converse.setConnectionStatus(status, message); + + _converse.setDisconnectionCause(status, message, true); + + _converse.onDisconnected(); + } else if (status === Strophe.Status.CONNFAIL) { + var feedback = message; + + if (message === "host-unknown" || message == "remote-connection-failed") { + feedback = __("Sorry, we could not connect to the XMPP host with domain: ") + "\"".concat(Strophe.getDomainFromJid(_converse.connection.jid), "\""); + } else if (!_.isUndefined(message) && message === _.get(Strophe, 'ErrorCondition.NO_AUTH_MECH')) { + feedback = __("The XMPP server did not offer a supported authentication mechanism"); + } + + _converse.setConnectionStatus(status, feedback); + + _converse.setDisconnectionCause(status, message); + } else if (status === Strophe.Status.DISCONNECTING) { + _converse.setDisconnectionCause(status, message); + } + }; + + this.incrementMsgCounter = function () { + this.msg_counter += 1; + var unreadMsgCount = this.msg_counter; + + if (document.title.search(/^Messages \(\d+\) /) === -1) { + document.title = "Messages (".concat(unreadMsgCount, ") ").concat(document.title); + } else { + document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (".concat(unreadMsgCount, ") ")); + } + }; + + this.clearMsgCounter = function () { + this.msg_counter = 0; + + if (document.title.search(/^Messages \(\d+\) /) !== -1) { + document.title = document.title.replace(/^Messages \(\d+\) /, ""); + } + }; + + this.initStatus = function () { + return new Promise(function (resolve, reject) { + var promise = new utils.getResolveablePromise(); + _this.xmppstatus = new _this.XMPPStatus(); + var id = b64_sha1("converse.xmppstatus-".concat(_converse.bare_jid)); + _this.xmppstatus.id = id; // Appears to be necessary for backbone.browserStorage + + _this.xmppstatus.browserStorage = new Backbone.BrowserStorage[_converse.storage](id); + + _this.xmppstatus.fetch({ + success: resolve, + error: resolve + }); + + _converse.emit('statusInitialized'); + }); + }; + + this.initSession = function () { + _converse.session = new Backbone.Model(); + var id = b64_sha1('converse.bosh-session'); + _converse.session.id = id; // Appears to be necessary for backbone.browserStorage + + _converse.session.browserStorage = new Backbone.BrowserStorage[_converse.storage](id); + + _converse.session.fetch(); + }; + + this.clearSession = function () { + if (!_.isUndefined(this.roster)) { + this.roster.browserStorage._clear(); + } + + if (!_.isUndefined(this.session) && this.session.browserStorage) { + this.session.browserStorage._clear(); + } + }; + + this.logOut = function () { + _converse.clearSession(); + + _converse.setDisconnectionCause(_converse.LOGOUT, undefined, true); + + if (!_.isUndefined(_converse.connection)) { + _converse.connection.disconnect(); + } else { + _converse._tearDown(); + } + + _converse.emit('logout'); + }; + + this.saveWindowState = function (ev, hidden) { + // XXX: eventually we should be able to just use + // document.visibilityState (when we drop support for older + // browsers). + var state; + var event_map = { + 'focus': "visible", + 'focusin': "visible", + 'pageshow': "visible", + 'blur': "hidden", + 'focusout': "hidden", + 'pagehide': "hidden" + }; + ev = ev || document.createEvent('Events'); + + if (ev.type in event_map) { + state = event_map[ev.type]; + } else { + state = document[hidden] ? "hidden" : "visible"; + } + + if (state === 'visible') { + _converse.clearMsgCounter(); + } + + _converse.windowState = state; + + _converse.emit('windowStateChanged', { + state: state + }); + }; + + this.registerGlobalEventHandlers = function () { + // Taken from: + // http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active + var hidden = "hidden"; // Standards: + + if (hidden in document) { + document.addEventListener("visibilitychange", _.partial(_converse.saveWindowState, _, hidden)); + } else if ((hidden = "mozHidden") in document) { + document.addEventListener("mozvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); + } else if ((hidden = "webkitHidden") in document) { + document.addEventListener("webkitvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); + } else if ((hidden = "msHidden") in document) { + document.addEventListener("msvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); + } else if ("onfocusin" in document) { + // IE 9 and lower: + document.onfocusin = document.onfocusout = _.partial(_converse.saveWindowState, _, hidden); + } else { + // All others: + window.onpageshow = window.onpagehide = window.onfocus = window.onblur = _.partial(_converse.saveWindowState, _, hidden); + } // set the initial state (but only if browser supports the Page Visibility API) + + + if (document[hidden] !== undefined) { + _.partial(_converse.saveWindowState, _, hidden)({ + type: document[hidden] ? "blur" : "focus" + }); + } + }; + + this.enableCarbons = function () { + var _this2 = this; + + /* Ask the XMPP server to enable Message Carbons + * See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling + */ + if (!this.message_carbons || this.session.get('carbons_enabled')) { + return; + } + + var carbons_iq = new Strophe.Builder('iq', { + from: this.connection.jid, + id: 'enablecarbons', + type: 'set' + }).c('enable', { + xmlns: Strophe.NS.CARBONS + }); + this.connection.addHandler(function (iq) { + if (iq.querySelectorAll('error').length > 0) { + _converse.log('An error occured while trying to enable message carbons.', Strophe.LogLevel.ERROR); + } else { + _this2.session.save({ + carbons_enabled: true + }); + + _converse.log('Message carbons have been enabled.'); + } + }, null, "iq", null, "enablecarbons"); + 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(_converse.sendInitialPresence); + } else { + _converse.rostergroups.fetchRosterGroups().then(function () { + _converse.emit('rosterGroupsFetched'); + + return _converse.roster.fetchRosterContacts(); + }).then(function () { + _converse.emit('rosterContactsFetched'); + + _converse.sendInitialPresence(); + }).catch(function () { + _converse.sendInitialPresence(); + }); + } + }; + + this.unregisterPresenceHandler = function () { + if (!_.isUndefined(_converse.presence_ref)) { + _converse.connection.deleteHandler(_converse.presence_ref); + + delete _converse.presence_ref; + } + }; + + 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(); + } + }; + + this.onStatusInitialized = function (reconnecting) { + /* Continue with session establishment (e.g. fetching chat boxes, + * populating the roster etc.) necessary once the connection has + * been established. + */ + 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(); + + if (!reconnecting) { + init_promise.resolve(); + + _converse.emit('initialized'); + } + }; + + this.setUserJid = function () { + _converse.jid = _converse.connection.jid; + _converse.bare_jid = Strophe.getBareJidFromJid(_converse.connection.jid); + _converse.resource = Strophe.getResourceFromJid(_converse.connection.jid); + _converse.domain = Strophe.getDomainFromJid(_converse.connection.jid); + }; + + this.onConnected = function (reconnecting) { + /* Called as soon as a new connection has been established, either + * by logging in or by attaching to an existing BOSH session. + */ + // Solves problem of returned PubSub BOSH response not received + // by browser. + _converse.connection.flush(); + + _converse.setUserJid(); + + _converse.initSession(); + + _converse.enableCarbons(); // If there's no xmppstatus obj, then we were never connected to + // begin with, so we set reconnecting to false. + + + reconnecting = _.isUndefined(_converse.xmppstatus) ? false : reconnecting; + + if (reconnecting) { + _converse.onStatusInitialized(true); + + _converse.emit('reconnected'); + } else { + _converse.initStatus().then(_.partial(_converse.onStatusInitialized, false), _.partial(_converse.onStatusInitialized, false)).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + + _converse.emit('connected'); + } + }; + + this.RosterContact = Backbone.Model.extend({ + defaults: { + 'bookmarked': false, + 'chat_state': undefined, + 'chat_status': 'offline', + 'groups': [], + 'image': _converse.DEFAULT_IMAGE, + 'image_type': _converse.DEFAULT_IMAGE_TYPE, + 'num_unread': 0, + 'status': '' + }, + initialize: function initialize(attributes) { + var _this3 = this; + + var jid = attributes.jid; + var bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(); + var resource = Strophe.getResourceFromJid(jid); + attributes.jid = bare_jid; + this.set(_.assignIn({ + 'id': bare_jid, + 'jid': bare_jid, + 'fullname': bare_jid, + 'user_id': Strophe.getNodeFromJid(jid), + 'resources': resource ? { + resource: 0 + } : {} + }, attributes)); + this.on('destroy', function () { + _this3.removeFromRoster(); + }); + 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. */ - this.send_initial_presence = true; - this.msg_counter = 0; - this.user_settings = settings; // Save the user settings so that they can be used by plugins + this.save('ask', "subscribe"); // ask === 'subscribe' Means we have ask to subscribe to them. - // Module-level functions - // ---------------------- + var pres = $pres({ + to: this.get('jid'), + type: "subscribe" + }); - this.generateResource = function () { - return "/converse.js-" + Math.floor(Math.random() * 139749825).toString(); + if (message && message !== "") { + pres.c("status").t(message).up(); + } + + var nick = _converse.xmppstatus.get('fullname'); + + if (nick && nick !== "") { + pres.c('nick', { + 'xmlns': Strophe.NS.NICK + }).t(nick).up(); + } + + _converse.connection.send(pres); + + 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.destroy(); // Will cause removeFromRoster to be called. + }, + 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] = { + 'priority': priority, + 'status': chat_status, + 'timestamp': timestamp }; - - this.sendCSI = function (stat) { - /* Send out a Chat Status Notification (XEP-0352) - * - * Parameters: - * (String) stat: The user's chat status - */ - /* Send out a Chat Status Notification (XEP-0352) */ - // XXX if (converse.features[Strophe.NS.CSI] || true) { - _converse.connection.send($build(stat, { xmlns: Strophe.NS.CSI })); - _converse.inactive = stat === _converse.INACTIVE ? true : false; + var changed = { + 'resources': resources }; + var hpr = this.getHighestPriorityResource(); - this.onUserActivity = function () { - /* Resets counters and flags relating to CSI and auto_away/auto_xa */ - if (_converse.idle_seconds > 0) { - _converse.idle_seconds = 0; - } - if (!_converse.connection.authenticated) { - // We can't send out any stanzas when there's no authenticated connection. - // converse can happen when the connection reconnects. - return; - } - if (_converse.inactive) { - _converse.sendCSI(_converse.ACTIVE); - } - if (_converse.auto_changed_status === true) { - _converse.auto_changed_status = false; - // XXX: we should really remember the original state here, and - // then set it back to that... - _converse.xmppstatus.setStatus(_converse.default_state); - } - }; + 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.onEverySecond = function () { - /* An interval handler running every second. - * Used for CSI and the auto_away and auto_xa features. - */ - if (!_converse.connection.authenticated) { - // We can't send out any stanzas when there's no authenticated connection. - // This can happen when the connection reconnects. - return; - } - var stat = _converse.xmppstatus.getStatus(); - _converse.idle_seconds++; - if (_converse.csi_waiting_time > 0 && _converse.idle_seconds > _converse.csi_waiting_time && !_converse.inactive) { - _converse.sendCSI(_converse.INACTIVE); - } - if (_converse.auto_away > 0 && _converse.idle_seconds > _converse.auto_away && stat !== 'away' && stat !== 'xa' && stat !== 'dnd') { - _converse.auto_changed_status = true; - _converse.xmppstatus.setStatus('away'); - } else if (_converse.auto_xa > 0 && _converse.idle_seconds > _converse.auto_xa && stat !== 'xa' && stat !== 'dnd') { - _converse.auto_changed_status = true; - _converse.xmppstatus.setStatus('xa'); - } - }; + 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'); - this.registerIntervalHandler = function () { - /* Set an interval of one second and register a handler for it. - * Required for the auto_away, auto_xa and csi_waiting_time features. - */ - if (_converse.auto_away < 1 && _converse.auto_xa < 1 && _converse.csi_waiting_time < 1) { - // Waiting time of less then one second means features aren't used. - return; - } - _converse.idle_seconds = 0; - _converse.auto_changed_status = false; // Was the user's status changed by _converse.js? - window.addEventListener('click', _converse.onUserActivity); - window.addEventListener('focus', _converse.onUserActivity); - window.addEventListener('keypress', _converse.onUserActivity); - window.addEventListener('mousemove', _converse.onUserActivity); - window.addEventListener(unloadevent, _converse.onUserActivity); - _converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000); - }; + if (!_.isObject(resources)) { + resources = {}; + } else { + delete resources[resource]; + } - this.setConnectionStatus = function (connection_status, message) { - _converse.connfeedback.set({ - 'connection_status': connection_status, - 'message': message - }); - }; + 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'); - this.rejectPresenceSubscription = function (jid, message) { - /* Reject or cancel another user's subscription to our presence updates. - * - * Parameters: - * (String) jid - The Jabber ID of the user whose subscription - * is being canceled. - * (String) message - An optional message to the user - */ - var pres = $pres({ to: jid, type: "unsubscribed" }); - if (message && message !== "") { - pres.c("status").t(message); - } - _converse.connection.send(pres); - }; + if (_.isObject(resources) && _.size(resources)) { + var val = _.flow(_.values, _.partial(_.sortBy, _, ['priority', 'timestamp']), _.reverse)(resources)[0]; - this.reconnect = _.debounce(function () { - _converse.log('RECONNECTING'); - _converse.log('The connection has dropped, attempting to reconnect.'); - _converse.setConnectionStatus(Strophe.Status.RECONNECTING, __('The connection has dropped, attempting to reconnect.')); - _converse.connection.reconnecting = true; - _converse._tearDown(); - _converse.logIn(null, true); - }, 3000, { 'leading': true }); + if (!_.isUndefined(val)) { + return val; + } + } + }, + removeFromRoster: function removeFromRoster(callback) { + /* 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" + }); - this.disconnect = function () { - _converse.log('DISCONNECTED'); - delete _converse.connection.reconnecting; - _converse.connection.reset(); - _converse._tearDown(); - _converse.emit('disconnected'); - }; + _converse.connection.sendIQ(iq, callback, callback); - this.onDisconnected = function () { - /* Gets called once strophe's status reaches Strophe.Status.DISCONNECTED. - * Will either start a teardown process for converse.js or attempt - * to reconnect. - */ - var reason = _converse.disconnection_reason; + 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.disconnection_cause === Strophe.Status.AUTHFAIL) { - if (_converse.credentials_url && _converse.auto_reconnect) { - /* In this case, we reconnect, because we might be receiving - * expirable tokens from the credentials_url. - */ - _converse.emit('will-reconnect'); - return _converse.reconnect(); - } else { - return _converse.disconnect(); - } - } else if (_converse.disconnection_cause === _converse.LOGOUT || !_.isUndefined(reason) && reason === _.get(Strophe, 'ErrorCondition.NO_AUTH_MECH') || reason === "host-unknown" || reason === "remote-connection-failed" || !_converse.auto_reconnect) { - return _converse.disconnect(); - } - _converse.emit('will-reconnect'); - _converse.reconnect(); - }; + 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; - this.setDisconnectionCause = function (cause, reason, override) { - /* Used to keep track of why we got disconnected, so that we can - * decide on what the next appropriate action is (in onDisconnected) - */ - if (_.isUndefined(cause)) { - delete _converse.disconnection_cause; - delete _converse.disconnection_reason; - } else if (_.isUndefined(_converse.disconnection_cause) || override) { - _converse.disconnection_cause = cause; - _converse.disconnection_reason = reason; - } - }; - - this.onConnectStatusChanged = function (status, message) { - /* Callback method called by Strophe as the Strophe.Connection goes - * through various states while establishing or tearing down a - * connection. - */ - _converse.log("Status changed to: " + _converse.CONNECTION_STATUS[status]); - if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) { - _converse.setConnectionStatus(status); - // By default we always want to send out an initial presence stanza. - _converse.send_initial_presence = true; - _converse.setDisconnectionCause(); - if (_converse.connection.reconnecting) { - _converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached'); - _converse.onConnected(true); - } else { - _converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached'); - if (_converse.connection.restored) { - // No need to send an initial presence stanza when - // we're restoring an existing session. - _converse.send_initial_presence = false; - } - _converse.onConnected(); - } - } else if (status === Strophe.Status.DISCONNECTED) { - _converse.setDisconnectionCause(status, message); - _converse.onDisconnected(); - } else if (status === Strophe.Status.ERROR) { - _converse.setConnectionStatus(status, __('An error occurred while connecting to the chat server.')); - } else if (status === Strophe.Status.CONNECTING) { - _converse.setConnectionStatus(status); - } else if (status === Strophe.Status.AUTHENTICATING) { - _converse.setConnectionStatus(status); - } else if (status === Strophe.Status.AUTHFAIL) { - if (!message) { - message = __('Your Jabber ID and/or password is incorrect. Please try again.'); - } - _converse.setConnectionStatus(status, message); - _converse.setDisconnectionCause(status, message, true); - _converse.onDisconnected(); - } else if (status === Strophe.Status.CONNFAIL) { - var feedback = message; - if (message === "host-unknown" || message == "remote-connection-failed") { - feedback = __("Sorry, we could not connect to the XMPP host with domain: ") + ("\"" + Strophe.getDomainFromJid(_converse.connection.jid) + "\""); - } else if (!_.isUndefined(message) && message === _.get(Strophe, 'ErrorCondition.NO_AUTH_MECH')) { - feedback = __("The XMPP server did not offer a supported authentication mechanism"); - } - _converse.setConnectionStatus(status, feedback); - _converse.setDisconnectionCause(status, message); - } else if (status === Strophe.Status.DISCONNECTING) { - _converse.setDisconnectionCause(status, message); - } - }; - - this.incrementMsgCounter = function () { - this.msg_counter += 1; - var unreadMsgCount = this.msg_counter; - if (document.title.search(/^Messages \(\d+\) /) === -1) { - document.title = "Messages (" + unreadMsgCount + ") " + document.title; - } else { - document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + unreadMsgCount + ") "); - } - }; - - this.clearMsgCounter = function () { - this.msg_counter = 0; - if (document.title.search(/^Messages \(\d+\) /) !== -1) { - document.title = document.title.replace(/^Messages \(\d+\) /, ""); - } - }; - - this.initStatus = function () { - return new Promise(function (resolve, reject) { - var promise = new utils.getWrappedPromise(); - _this.xmppstatus = new _this.XMPPStatus(); - var id = b64_sha1("converse.xmppstatus-" + _converse.bare_jid); - _this.xmppstatus.id = id; // Appears to be necessary for backbone.browserStorage - _this.xmppstatus.browserStorage = new Backbone.BrowserStorage[_converse.storage](id); - _this.xmppstatus.fetch({ - success: resolve, - error: resolve - }); - _converse.emit('statusInitialized'); - }); - }; - - this.initSession = function () { - _converse.session = new Backbone.Model(); - var id = b64_sha1('converse.bosh-session'); - _converse.session.id = id; // Appears to be necessary for backbone.browserStorage - _converse.session.browserStorage = new Backbone.BrowserStorage[_converse.storage](id); - _converse.session.fetch(); - }; - - this.clearSession = function () { - if (!_.isUndefined(this.roster)) { - this.roster.browserStorage._clear(); - } - if (!_.isUndefined(this.session) && this.session.browserStorage) { - this.session.browserStorage._clear(); - } - }; - - this.logOut = function () { - _converse.clearSession(); - _converse.setDisconnectionCause(_converse.LOGOUT, undefined, true); - if (!_.isUndefined(_converse.connection)) { - _converse.connection.disconnect(); - } else { - _converse._tearDown(); - } - _converse.emit('logout'); - }; - - this.saveWindowState = function (ev, hidden) { - // XXX: eventually we should be able to just use - // document.visibilityState (when we drop support for older - // browsers). - var state = void 0; - var event_map = { - 'focus': "visible", - 'focusin': "visible", - 'pageshow': "visible", - 'blur': "hidden", - 'focusout': "hidden", - 'pagehide': "hidden" - }; - ev = ev || document.createEvent('Events'); - if (ev.type in event_map) { - state = event_map[ev.type]; - } else { - state = document[hidden] ? "hidden" : "visible"; - } - if (state === 'visible') { - _converse.clearMsgCounter(); - } - _converse.windowState = state; - _converse.emit('windowStateChanged', { state: state }); - }; - - this.registerGlobalEventHandlers = function () { - // Taken from: - // http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active - var hidden = "hidden"; - // Standards: - if (hidden in document) { - document.addEventListener("visibilitychange", _.partial(_converse.saveWindowState, _, hidden)); - } else if ((hidden = "mozHidden") in document) { - document.addEventListener("mozvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); - } else if ((hidden = "webkitHidden") in document) { - document.addEventListener("webkitvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); - } else if ((hidden = "msHidden") in document) { - document.addEventListener("msvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); - } else if ("onfocusin" in document) { - // IE 9 and lower: - document.onfocusin = document.onfocusout = _.partial(_converse.saveWindowState, _, hidden); - } else { - // All others: - window.onpageshow = window.onpagehide = window.onfocus = window.onblur = _.partial(_converse.saveWindowState, _, hidden); - } - // set the initial state (but only if browser supports the Page Visibility API) - if (document[hidden] !== undefined) { - _.partial(_converse.saveWindowState, _, hidden)({ type: document[hidden] ? "blur" : "focus" }); - } - }; - - this.enableCarbons = function () { - var _this2 = this; - - /* Ask the XMPP server to enable Message Carbons - * See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling - */ - if (!this.message_carbons || this.session.get('carbons_enabled')) { - return; - } - var carbons_iq = new Strophe.Builder('iq', { - from: this.connection.jid, - id: 'enablecarbons', - type: 'set' - }).c('enable', { xmlns: Strophe.NS.CARBONS }); - this.connection.addHandler(function (iq) { - if (iq.querySelectorAll('error').length > 0) { - _converse.log('An error occured while trying to enable message carbons.', Strophe.LogLevel.ERROR); - } else { - _this2.session.save({ carbons_enabled: true }); - _converse.log('Message carbons have been enabled.'); - } - }, null, "iq", null, "enablecarbons"); - 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-" + _converse.bare_jid)); - _converse.rostergroups = new _converse.RosterGroups(); - _converse.rostergroups.browserStorage = new Backbone.BrowserStorage.session(b64_sha1("converse.roster.groups" + _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(_converse.sendInitialPresence); - } else { - _converse.rostergroups.fetchRosterGroups().then(function () { - _converse.emit('rosterGroupsFetched'); - return _converse.roster.fetchRosterContacts(); - }).then(function () { - _converse.emit('rosterContactsFetched'); - _converse.sendInitialPresence(); - }).catch(function () { - _converse.sendInitialPresence(); - }); - } - }; - - this.unregisterPresenceHandler = function () { - if (!_.isUndefined(_converse.presence_ref)) { - _converse.connection.deleteHandler(_converse.presence_ref); - delete _converse.presence_ref; - } - }; - - 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(); - } - }; - - this.onStatusInitialized = function (reconnecting) { - /* Continue with session establishment (e.g. fetching chat boxes, - * populating the roster etc.) necessary once the connection has - * been established. - */ - 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(); - if (!reconnecting) { - init_promise.resolve(); - _converse.emit('initialized'); - } - }; - - this.setUserJid = function () { - _converse.jid = _converse.connection.jid; - _converse.bare_jid = Strophe.getBareJidFromJid(_converse.connection.jid); - _converse.resource = Strophe.getResourceFromJid(_converse.connection.jid); - _converse.domain = Strophe.getDomainFromJid(_converse.connection.jid); - }; - - this.onConnected = function (reconnecting) { - /* Called as soon as a new connection has been established, either - * by logging in or by attaching to an existing BOSH session. - */ - // Solves problem of returned PubSub BOSH response not received - // by browser. + _converse.connection.addHandler(function (msg) { + window.setTimeout(function () { _converse.connection.flush(); - _converse.setUserJid(); - _converse.initSession(); - _converse.enableCarbons(); + _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 _this4 = this; - // If there's no xmppstatus obj, then we were never connected to - // begin with, so we set reconnecting to false. - reconnecting = _.isUndefined(_converse.xmppstatus) ? false : reconnecting; - if (reconnecting) { - _converse.onStatusInitialized(true); - _converse.emit('reconnected'); - } else { - _converse.initStatus().then(_.partial(_converse.onStatusInitialized, false), _.partial(_converse.onStatusInitialized, false)).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - _converse.emit('connected'); + /* 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) { + _this4.fetch({ + add: 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'), null, _converse.xmppstatus.get('fullname')); + } + }); + + return true; + }, + isSelf: function isSelf(jid) { + return utils.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.RosterContact = Backbone.Model.extend({ - - defaults: { - 'bookmarked': false, - 'chat_state': undefined, - 'chat_status': 'offline', - 'groups': [], - 'image': _converse.DEFAULT_IMAGE, - 'image_type': _converse.DEFAULT_IMAGE_TYPE, - 'num_unread': 0, - 'status': '' - }, - - initialize: function initialize(attributes) { - var _this3 = this; - - var jid = attributes.jid; - - var bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(); - var resource = Strophe.getResourceFromJid(jid); - attributes.jid = bare_jid; - this.set(_.assignIn({ - 'id': bare_jid, - 'jid': bare_jid, - 'fullname': bare_jid, - 'user_id': Strophe.getNodeFromJid(jid), - 'resources': resource ? { resource: 0 } : {} - }, attributes)); - - this.on('destroy', function () { - _this3.removeFromRoster(); - }); - 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. - */ - this.save('ask', "subscribe"); // ask === 'subscribe' Means we have ask to subscribe to them. - var pres = $pres({ to: this.get('jid'), type: "subscribe" }); - if (message && message !== "") { - pres.c("status").t(message).up(); - } - var nick = _converse.xmppstatus.get('fullname'); - if (nick && nick !== "") { - pres.c('nick', { 'xmlns': Strophe.NS.NICK }).t(nick).up(); - } - _converse.connection.send(pres); - 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.destroy(); // Will cause removeFromRoster to be called. - }, - 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=\"" + 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] = { - '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) { - /* 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, callback); - return this; - } + this.addContact(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 }); - 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 _this4 = 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) { - _this4.fetch({ - add: 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'), null, _converse.xmppstatus.get('fullname')); - } - }); - return true; - }, - isSelf: function isSelf(jid) { - return utils.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.addContact(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); - }, - addContact: function addContact(jid, name, groups, attributes) { - var _this5 = 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; - _this5.sendContactAddIQ(jid, name, groups, function () { - var contact = _this5.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) { - 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(); - } - }; - this.addContact(bare_jid, '', [], { '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=\"" + Strophe.NS.ROSTER + "\"] item", iq); - _.each(items, this.updateContact.bind(this)); - _converse.emit('rosterPush', iq); - return true; - }, - fetchFromServer: function fetchFromServer() { - var _this6 = 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(_this6.onReceivedFromServer.bind(_this6), 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=\"" + 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(); // will trigger removeFromRoster - } - // 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')), - nick_el = presence.querySelector("nick[xmlns=\"" + Strophe.NS.NICK + "\"]"); - var user_data = { - jid: bare_jid, - subscription: 'none', - ask: null, - requesting: true, - fullname: nick_el && nick_el.textContent || bare_jid - }; - 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); - } 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 }); - } - } - return; - } else if (sizzle("query[xmlns=\"" + 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); - } - } + _.each(groups, function (group) { + iq.c('group').t(group).up(); }); - 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(); - } - }); + _converse.connection.sendIQ(iq, callback, errback); + }, + addContact: function addContact(jid, name, groups, attributes) { + var _this5 = this; - this.RosterGroups = Backbone.Collection.extend({ - model: _converse.RosterGroup, + /* 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; - fetchRosterGroups: function fetchRosterGroups() { - var _this7 = 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) { - _this7.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.Message = Backbone.Model.extend({ - defaults: function defaults() { - return { - msgid: _converse.connection.getUniqueId() - }; - } - }); - - this.Messages = Backbone.Collection.extend({ - model: _converse.Message, - comparator: 'time' - }); - - this.ChatBox = Backbone.Model.extend({ - defaults: { - 'type': 'chatbox', - 'bookmarked': false, - 'chat_state': undefined, - 'num_unread': 0, - 'url': '' - }, - - initialize: function initialize() { - this.messages = new _converse.Messages(); - this.messages.browserStorage = new Backbone.BrowserStorage[_converse.message_storage](b64_sha1("converse.messages" + this.get('jid') + _converse.bare_jid)); - this.save({ - // The chat_state will be set to ACTIVE once the chat box is opened - // and we listen for change:chat_state, so shouldn't set it to ACTIVE here. - 'box_id': b64_sha1(this.get('jid')), - 'time_opened': this.get('time_opened') || moment().valueOf(), - 'user_id': Strophe.getNodeFromJid(this.get('jid')) - }); - }, - getMessageBody: function getMessageBody(message) { - var type = message.getAttribute('type'); - return type === 'error' ? _.propertyOf(message.querySelector('error text'))('textContent') : _.propertyOf(message.querySelector('body'))('textContent'); - }, - getMessageAttributes: function getMessageAttributes(message, delay, original_stanza) { - delay = delay || message.querySelector('delay'); - var type = message.getAttribute('type'), - body = this.getMessageBody(message); - - var delayed = !_.isNull(delay), - is_groupchat = type === 'groupchat', - chat_state = message.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING || message.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED || message.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE || message.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE || message.getElementsByTagName(_converse.GONE).length && _converse.GONE; - - var from = void 0; - if (is_groupchat) { - from = Strophe.unescapeNode(Strophe.getResourceFromJid(message.getAttribute('from'))); - } else { - from = Strophe.getBareJidFromJid(message.getAttribute('from')); - } - var time = delayed ? delay.getAttribute('stamp') : moment().format(); - var sender = void 0, - fullname = void 0; - if (is_groupchat && from === this.get('nick') || !is_groupchat && from === _converse.bare_jid) { - sender = 'me'; - fullname = _converse.xmppstatus.get('fullname') || from; - } else { - sender = 'them'; - fullname = this.get('fullname') || from; - } - return { - 'type': type, - 'chat_state': chat_state, - 'delayed': delayed, - 'fullname': fullname, - 'message': body || undefined, - 'msgid': message.getAttribute('id'), - 'sender': sender, - 'time': time - }; - }, - createMessage: function createMessage(message, delay, original_stanza) { - return this.messages.create(this.getMessageAttributes.apply(this, arguments)); - }, - newMessageWillBeHidden: function newMessageWillBeHidden() { - /* Returns a boolean to indicate whether a newly received - * message will be visible to the user or not. - */ - return this.get('hidden') || this.get('minimized') || this.isScrolledUp() || _converse.windowState === 'hidden'; - }, - incrementUnreadMsgCounter: function incrementUnreadMsgCounter(stanza) { - /* Given a newly received message, update the unread counter if - * necessary. - */ - if (_.isNull(stanza.querySelector('body'))) { - return; // The message has no text - } - if (utils.isNewMessage(stanza) && this.newMessageWillBeHidden()) { - this.save({ 'num_unread': this.get('num_unread') + 1 }); - _converse.incrementMsgCounter(); - } - }, - clearUnreadMsgCounter: function clearUnreadMsgCounter() { - this.save({ 'num_unread': 0 }); - }, - isScrolledUp: function isScrolledUp() { - return this.get('scrolled', true); - } - }); - - this.ConnectionFeedback = Backbone.Model.extend({ - - defaults: { - 'connection_status': undefined, - 'message': '' - }, - - initialize: function initialize() { - this.on('change', function () { - _converse.emit('connfeedback', _converse.connfeedback); - }); - } - }); - this.connfeedback = new this.ConnectionFeedback(); - - this.XMPPStatus = Backbone.Model.extend({ - initialize: function initialize() { - var _this8 = this; - - this.set({ - 'status': this.getStatus() - }); - this.on('change', function (item) { - if (_.has(item.changed, 'status')) { - _converse.emit('statusChanged', _this8.get('status')); - } - if (_.has(item.changed, 'status_message')) { - _converse.emit('statusMessageChanged', _this8.get('status_message')); - } - }); - }, - constructPresence: function constructPresence(type, status_message) { - var presence = void 0; - type = _.isString(type) ? type : this.get('status') || _converse.default_state; - status_message = _.isString(status_message) ? status_message : undefined; - // Most of these presence types are actually not explicitly sent, - // but I add all of them here for reference and future proofing. - if (type === 'unavailable' || type === 'probe' || type === 'error' || type === 'unsubscribe' || type === 'unsubscribed' || type === 'subscribe' || type === 'subscribed') { - presence = $pres({ 'type': type }); - } else if (type === 'offline') { - presence = $pres({ 'type': 'unavailable' }); - } else if (type === 'online') { - presence = $pres(); - } else { - presence = $pres().c('show').t(type).up(); - } - if (status_message) { - presence.c('status').t(status_message).up(); - } - presence.c('priority').t(_.isNaN(Number(_converse.priority)) ? 0 : _converse.priority); - return presence; - }, - sendPresence: function sendPresence(type, status_message) { - _converse.connection.send(this.constructPresence(type, status_message)); - }, - setStatus: function setStatus(value) { - this.sendPresence(value); - this.save({ 'status': value }); - }, - getStatus: function getStatus() { - return this.get('status') || _converse.default_state; - }, - setStatusMessage: function setStatusMessage(status_message) { - this.sendPresence(this.getStatus(), status_message); - this.save({ 'status_message': status_message }); - if (this.xhr_custom_status) { - var xhr = new XMLHttpRequest(); - xhr.open('POST', this.xhr_custom_status_url, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); - xhr.send({ 'msg': status_message }); - } - var prev_status = this.get('status_message'); - if (prev_status === status_message) { - this.trigger("update-status-ui", this); - } - } - }); - - this.setUpXMLLogging = function () { - Strophe.log = function (level, msg) { - _converse.log(msg, level); - }; - if (this.debug) { - this.connection.xmlInput = function (body) { - _converse.log(body.outerHTML, Strophe.LogLevel.DEBUG, 'color: darkgoldenrod'); - }; - this.connection.xmlOutput = function (body) { - _converse.log(body.outerHTML, Strophe.LogLevel.DEBUG, 'color: darkcyan'); - }; - } - }; - - this.fetchLoginCredentials = function () { - return new Promise(function (resolve, reject) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', _converse.credentials_url, true); - xhr.setRequestHeader('Accept', "application/json, text/javascript"); - xhr.onload = function () { - if (xhr.status >= 200 && xhr.status < 400) { - var data = JSON.parse(xhr.responseText); - resolve({ - 'jid': data.jid, - 'password': data.password - }); - } else { - xhr.onerror(); - } - }; - xhr.onerror = function () { - delete _converse.connection; - _converse.emit('noResumeableSession', this); - reject(xhr.responseText); - }; - xhr.send(); + _this5.sendContactAddIQ(jid, name, groups, function () { + var contact = _this5.create(_.assignIn({ + ask: undefined, + fullname: name, + groups: groups, + jid: jid, + requesting: false, + subscription: 'none' + }, attributes), { + sort: false }); - }; - this.startNewBOSHSession = function () { - var xhr = new XMLHttpRequest(); - xhr.open('GET', _converse.prebind_url, true); - xhr.setRequestHeader('Accept', "application/json, text/javascript"); - xhr.onload = function () { - if (xhr.status >= 200 && xhr.status < 400) { - var data = JSON.parse(xhr.responseText); - _converse.connection.attach(data.jid, data.sid, data.rid, _converse.onConnectStatusChanged); - } else { - xhr.onerror(); - } - }; - xhr.onerror = function () { - delete _converse.connection; - _converse.emit('noResumeableSession', this); - }; - xhr.send(); - }; + resolve(contact); + }, function (err) { + alert(__('Sorry, there was an error while trying to add %1$s as a contact.', name)); - this.restoreBOSHSession = function (jid_is_required) { - /* Tries to restore a cached BOSH session. */ - if (!this.jid) { - var msg = "restoreBOSHSession: tried to restore a \"keepalive\" session " + "but we don't have the JID for the user!"; - if (jid_is_required) { - throw new Error(msg); - } else { - _converse.log(msg); - } - } - try { - this.connection.restore(this.jid, this.onConnectStatusChanged); - return true; - } catch (e) { - _converse.log("Could not restore session for jid: " + this.jid + " Error message: " + e.message); - this.clearSession(); // If there's a roster, we want to clear it (see #555) - return false; - } - }; + _converse.log(err, Strophe.LogLevel.ERROR); - this.attemptPreboundSession = function (reconnecting) { - /* Handle session resumption or initialization when prebind is - * being used. - */ - if (!reconnecting) { - if (this.keepalive && this.restoreBOSHSession(true)) { - return; - } - // No keepalive, or session resumption has failed. - if (this.jid && this.sid && this.rid) { - return this.connection.attach(this.jid, this.sid, this.rid, this.onConnectStatusChanged); - } - } - if (this.prebind_url) { - return this.startNewBOSHSession(); - } else { - throw new Error("attemptPreboundSession: If you use prebind and not keepalive, " + "then you MUST supply JID, RID and SID values or a prebind_url."); - } - }; + resolve(err); + }); + }); + }, + subscribeBack: function subscribeBack(bare_jid) { + var contact = this.get(bare_jid); - this.attemptNonPreboundSession = function (credentials, reconnecting) { - /* Handle session resumption or initialization when prebind is not being used. - * - * Two potential options exist and are handled in this method: - * 1. keepalive - * 2. auto_login - */ - if (!reconnecting && this.keepalive && this.restoreBOSHSession()) { - return; - } - - if (credentials) { - // When credentials are passed in, they override prebinding - // or credentials fetching via HTTP - this.autoLogin(credentials); - } else if (this.auto_login) { - if (this.credentials_url) { - this.fetchLoginCredentials().then(this.autoLogin.bind(this), this.autoLogin.bind(this)); - } else if (!this.jid) { - throw new Error("attemptNonPreboundSession: If you use auto_login, " + "you also need to give either a jid value (and if " + "applicable a password) or you need to pass in a URL " + "from where the username and password can be fetched " + "(via credentials_url)."); - } else { - this.autoLogin(); // Probably ANONYMOUS login - } - } else if (reconnecting) { - this.autoLogin(); - } - }; - - this.autoLogin = function (credentials) { - if (credentials) { - // If passed in, the credentials come from credentials_url, - // so we set them on the converse object. - this.jid = credentials.jid; - } - if (this.authentication === _converse.ANONYMOUS) { - if (!this.jid) { - throw new Error("Config Error: when using anonymous login " + "you need to provide the server's domain via the 'jid' option. " + "Either when calling converse.initialize, or when calling " + "_converse.api.user.login."); - } - if (!this.connection.reconnecting) { - this.connection.reset(); - } - this.connection.connect(this.jid.toLowerCase(), null, this.onConnectStatusChanged); - } else if (this.authentication === _converse.LOGIN) { - var password = _.isNil(credentials) ? _converse.connection.pass || this.password : credentials.password; - if (!password) { - if (this.auto_login) { - throw new Error("initConnection: If you use auto_login and " + "authentication='login' then you also need to provide a password."); - } - _converse.setDisconnectionCause(Strophe.Status.AUTHFAIL, undefined, true); - _converse.disconnect(); - return; - } - var resource = Strophe.getResourceFromJid(this.jid); - if (!resource) { - this.jid = this.jid.toLowerCase() + _converse.generateResource(); - } else { - this.jid = Strophe.getBareJidFromJid(this.jid).toLowerCase() + '/' + resource; - } - if (!this.connection.reconnecting) { - this.connection.reset(); - } - this.connection.connect(this.jid, password, this.onConnectStatusChanged); - } - }; - - this.logIn = function (credentials, reconnecting) { - // We now try to resume or automatically set up a new session. - // Otherwise the user will be shown a login form. - if (this.authentication === _converse.PREBIND) { - this.attemptPreboundSession(reconnecting); - } else { - this.attemptNonPreboundSession(credentials, reconnecting); - } - }; - - this.initConnection = function () { - 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."); - } - if (('WebSocket' in window || 'MozWebSocket' in window) && this.websocket_url) { - this.connection = new Strophe.Connection(this.websocket_url, this.connection_options); - } else if (this.bosh_service_url) { - this.connection = new Strophe.Connection(this.bosh_service_url, _.assignIn(this.connection_options, { 'keepalive': this.keepalive })); - } else { - throw new Error("initConnection: this browser does not support websockets and bosh_service_url wasn't specified."); - } - } - _converse.emit('connectionInitialized'); - }; - - this._tearDown = function () { - /* Remove those views which are only allowed with a valid - * connection. - */ - _converse.emit('beforeTearDown'); - _converse.unregisterPresenceHandler(); - if (_converse.roster) { - _converse.roster.off().reset(); // Removes roster contacts - } - if (!_.isUndefined(_converse.session)) { - _converse.session.destroy(); - } - window.removeEventListener('click', _converse.onUserActivity); - window.removeEventListener('focus', _converse.onUserActivity); - window.removeEventListener('keypress', _converse.onUserActivity); - window.removeEventListener('mousemove', _converse.onUserActivity); - window.removeEventListener(unloadevent, _converse.onUserActivity); - window.clearInterval(_converse.everySecondTrigger); - _converse.emit('afterTearDown'); - return _converse; - }; - - this.initPlugins = function () { - // If initialize gets called a second time (e.g. during tests), then we - // need to re-apply all plugins (for a new converse instance), and we - // therefore need to clear this array that prevents plugins from being - // initialized twice. - // If initialize is called for the first time, then this array is empty - // in any case. - _converse.pluggable.initialized_plugins = []; - var whitelist = _converse.core_plugins.concat(_converse.whitelisted_plugins); - - _converse.pluggable.initializePlugins({ - 'updateSettings': function updateSettings() { - _converse.log("(DEPRECATION) " + "The `updateSettings` method has been deprecated. " + "Please use `_converse.api.settings.update` instead.", Strophe.LogLevel.WARN); - _converse.api.settings.update.apply(_converse, arguments); - }, - - '_converse': _converse - }, whitelist, _converse.blacklisted_plugins); - _converse.emit('pluginsInitialized'); - }; - - // Initialization - // -------------- - // This is the end of the initialize method. - if (settings.connection) { - this.connection = settings.connection; - } - - function finishInitialization() { - _converse.initPlugins(); - _converse.initConnection(); - _converse.setUpXMLLogging(); - _converse.logIn(); - _converse.registerGlobalEventHandlers(); - - if (!Backbone.history.started) { - Backbone.history.start(); - } - } - - if (!_.isUndefined(_converse.connection) && _converse.connection.service === 'jasmine tests') { - finishInitialization(); - return _converse; - } else if (_.isUndefined(i18n)) { - finishInitialization(); + if (contact instanceof _converse.RosterContact) { + contact.authorize().subscribe(); } else { - i18n.fetchTranslations(_converse.locale, _converse.locales, _.template(_converse.locales_url)({ 'locale': _converse.locale })).then(function () { - finishInitialization(); - }).catch(function (reason) { - finishInitialization(); - _converse.log(reason, Strophe.LogLevel.ERROR); + // Can happen when a subscription is retried or roster was deleted + var handler = function handler(contact) { + if (contact instanceof _converse.RosterContact) { + contact.authorize().subscribe(); + } + }; + + this.addContact(bare_jid, '', [], { + '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 _this6 = 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(_this6.onReceivedFromServer.bind(_this6), 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(); // will trigger removeFromRoster + } // 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')), + nick_el = presence.querySelector("nick[xmlns=\"".concat(Strophe.NS.NICK, "\"]")); + var user_data = { + jid: bare_jid, + subscription: 'none', + ask: null, + requesting: true, + fullname: nick_el && nick_el.textContent || bare_jid + }; + 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); + } 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 + }); + } + } + + return; + } else if (sizzle("query[xmlns=\"".concat(Strophe.NS.MUC, "\"]"), presence).length) { + return; // Ignore MUC } - return init_promise.promise; + + 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 _this7 = 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) { + _this7.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.Message = Backbone.Model.extend({ + defaults: function defaults() { + return { + msgid: _converse.connection.getUniqueId() + }; + } + }); + this.Messages = Backbone.Collection.extend({ + model: _converse.Message, + comparator: 'time' + }); + this.ChatBox = Backbone.Model.extend({ + defaults: { + 'type': 'chatbox', + 'bookmarked': false, + 'chat_state': undefined, + 'num_unread': 0, + 'url': '' + }, + initialize: function initialize() { + this.messages = new _converse.Messages(); + this.messages.browserStorage = new Backbone.BrowserStorage[_converse.message_storage](b64_sha1("converse.messages".concat(this.get('jid')).concat(_converse.bare_jid))); + this.save({ + // The chat_state will be set to ACTIVE once the chat box is opened + // and we listen for change:chat_state, so shouldn't set it to ACTIVE here. + 'box_id': b64_sha1(this.get('jid')), + 'time_opened': this.get('time_opened') || moment().valueOf(), + 'user_id': Strophe.getNodeFromJid(this.get('jid')) + }); + }, + getMessageBody: function getMessageBody(message) { + var type = message.getAttribute('type'); + return type === 'error' ? _.propertyOf(message.querySelector('error text'))('textContent') : _.propertyOf(message.querySelector('body'))('textContent'); + }, + getMessageAttributes: function getMessageAttributes(message, delay, original_stanza) { + delay = delay || message.querySelector('delay'); + var type = message.getAttribute('type'), + body = this.getMessageBody(message); + + var delayed = !_.isNull(delay), + is_groupchat = type === 'groupchat', + chat_state = message.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING || message.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED || message.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE || message.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE || message.getElementsByTagName(_converse.GONE).length && _converse.GONE; + + var from; + + if (is_groupchat) { + from = Strophe.unescapeNode(Strophe.getResourceFromJid(message.getAttribute('from'))); + } else { + from = Strophe.getBareJidFromJid(message.getAttribute('from')); + } + + var time = delayed ? delay.getAttribute('stamp') : moment().format(); + var sender, fullname; + + if (is_groupchat && from === this.get('nick') || !is_groupchat && from === _converse.bare_jid) { + sender = 'me'; + fullname = _converse.xmppstatus.get('fullname') || from; + } else { + sender = 'them'; + fullname = this.get('fullname') || from; + } + + return { + 'type': type, + 'chat_state': chat_state, + 'delayed': delayed, + 'fullname': fullname, + 'message': body || undefined, + 'msgid': message.getAttribute('id'), + 'sender': sender, + 'time': time + }; + }, + createMessage: function createMessage(message, delay, original_stanza) { + return this.messages.create(this.getMessageAttributes.apply(this, arguments)); + }, + newMessageWillBeHidden: function newMessageWillBeHidden() { + /* Returns a boolean to indicate whether a newly received + * message will be visible to the user or not. + */ + return this.get('hidden') || this.get('minimized') || this.isScrolledUp() || _converse.windowState === 'hidden'; + }, + incrementUnreadMsgCounter: function incrementUnreadMsgCounter(stanza) { + /* Given a newly received message, update the unread counter if + * necessary. + */ + if (_.isNull(stanza.querySelector('body'))) { + return; // The message has no text + } + + if (utils.isNewMessage(stanza) && this.newMessageWillBeHidden()) { + this.save({ + 'num_unread': this.get('num_unread') + 1 + }); + + _converse.incrementMsgCounter(); + } + }, + clearUnreadMsgCounter: function clearUnreadMsgCounter() { + this.save({ + 'num_unread': 0 + }); + }, + isScrolledUp: function isScrolledUp() { + return this.get('scrolled', true); + } + }); + this.ConnectionFeedback = Backbone.Model.extend({ + defaults: { + 'connection_status': undefined, + 'message': '' + }, + initialize: function initialize() { + this.on('change', function () { + _converse.emit('connfeedback', _converse.connfeedback); + }); + } + }); + this.connfeedback = new this.ConnectionFeedback(); + this.XMPPStatus = Backbone.Model.extend({ + initialize: function initialize() { + var _this8 = this; + + this.set({ + 'status': this.getStatus() + }); + this.on('change', function (item) { + if (_.has(item.changed, 'status')) { + _converse.emit('statusChanged', _this8.get('status')); + } + + if (_.has(item.changed, 'status_message')) { + _converse.emit('statusMessageChanged', _this8.get('status_message')); + } + }); + }, + constructPresence: function constructPresence(type, status_message) { + var presence; + type = _.isString(type) ? type : this.get('status') || _converse.default_state; + status_message = _.isString(status_message) ? status_message : undefined; // Most of these presence types are actually not explicitly sent, + // but I add all of them here for reference and future proofing. + + if (type === 'unavailable' || type === 'probe' || type === 'error' || type === 'unsubscribe' || type === 'unsubscribed' || type === 'subscribe' || type === 'subscribed') { + presence = $pres({ + 'type': type + }); + } else if (type === 'offline') { + presence = $pres({ + 'type': 'unavailable' + }); + } else if (type === 'online') { + presence = $pres(); + } else { + presence = $pres().c('show').t(type).up(); + } + + if (status_message) { + presence.c('status').t(status_message).up(); + } + + presence.c('priority').t(_.isNaN(Number(_converse.priority)) ? 0 : _converse.priority); + return presence; + }, + sendPresence: function sendPresence(type, status_message) { + _converse.connection.send(this.constructPresence(type, status_message)); + }, + setStatus: function setStatus(value) { + this.sendPresence(value); + this.save({ + 'status': value + }); + }, + getStatus: function getStatus() { + return this.get('status') || _converse.default_state; + }, + setStatusMessage: function setStatusMessage(status_message) { + this.sendPresence(this.getStatus(), status_message); + this.save({ + 'status_message': status_message + }); + + if (this.xhr_custom_status) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', this.xhr_custom_status_url, true); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); + xhr.send({ + 'msg': status_message + }); + } + + var prev_status = this.get('status_message'); + + if (prev_status === status_message) { + this.trigger("update-status-ui", this); + } + } + }); + + this.setUpXMLLogging = function () { + Strophe.log = function (level, msg) { + _converse.log(msg, level); + }; + + if (this.debug) { + this.connection.xmlInput = function (body) { + _converse.log(body.outerHTML, Strophe.LogLevel.DEBUG, 'color: darkgoldenrod'); + }; + + this.connection.xmlOutput = function (body) { + _converse.log(body.outerHTML, Strophe.LogLevel.DEBUG, 'color: darkcyan'); + }; + } }; - // API methods only available to plugins - _converse.api = { - 'connection': { - 'connected': function connected() { - return _converse.connection && _converse.connection.connected || false; - }, - 'disconnect': function disconnect() { - _converse.connection.disconnect(); - } - }, - 'emit': function emit() { - _converse.emit.apply(_converse, arguments); - }, + this.fetchLoginCredentials = function () { + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', _converse.credentials_url, true); + xhr.setRequestHeader('Accept', "application/json, text/javascript"); - 'user': { - 'jid': function jid() { - return _converse.connection.jid; - }, - 'login': function login(credentials) { - _converse.initConnection(); - _converse.logIn(credentials); - }, - 'logout': function logout() { - _converse.logOut(); - }, + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 400) { + var data = JSON.parse(xhr.responseText); + resolve({ + 'jid': data.jid, + 'password': data.password + }); + } else { + xhr.onerror(); + } + }; - 'status': { - 'get': function get() { - return _converse.xmppstatus.get('status'); - }, - 'set': function set(value, message) { - var data = { 'status': value }; - if (!_.includes(_.keys(_converse.STATUS_WEIGHTS), value)) { - throw new Error('Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'); - } - if (_.isString(message)) { - data.status_message = message; - } - _converse.xmppstatus.sendPresence(value); - _converse.xmppstatus.save(data); - }, + xhr.onerror = function () { + delete _converse.connection; - 'message': { - 'get': function get() { - return _converse.xmppstatus.get('status_message'); - }, - 'set': function set(stat) { - _converse.xmppstatus.save({ 'status_message': stat }); - } - } - } - }, - 'settings': { - 'update': function update(settings) { - utils.merge(_converse.default_settings, settings); - utils.merge(_converse, settings); - utils.applyUserSettings(_converse, settings, _converse.user_settings); - }, - 'get': function get(key) { - if (_.includes(_.keys(_converse.default_settings), key)) { - return _converse[key]; - } - }, - 'set': function set(key, val) { - var o = {}; - if (_.isObject(key)) { - _.assignIn(_converse, _.pick(key, _.keys(_converse.default_settings))); - } else if (_.isString("string")) { - o[key] = val; - _.assignIn(_converse, _.pick(o, _.keys(_converse.default_settings))); - } - } - }, - 'promises': { - 'add': function add(promises) { - promises = _.isArray(promises) ? promises : [promises]; - _.each(promises, addPromise); - } - }, - 'contacts': { - 'get': function get(jids) { - var _transform = function _transform(jid) { - var contact = _converse.roster.get(Strophe.getBareJidFromJid(jid)); - if (contact) { - return contact.attributes; - } - return null; - }; - if (_.isUndefined(jids)) { - jids = _converse.roster.pluck('jid'); - } else if (_.isString(jids)) { - return _transform(jids); - } - return _.map(jids, _transform); - }, - '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)) { - return null; - } - if (id.toLowerCase() === 'rid') { - return _converse.connection.rid || _converse.connection._proto.rid; - } else if (id.toLowerCase() === 'sid') { - return _converse.connection.sid || _converse.connection._proto.sid; - } - } - }, - 'listen': { - 'once': _converse.once.bind(_converse), - 'on': _converse.on.bind(_converse), - 'not': _converse.off.bind(_converse), - 'stanza': function stanza(name, options, handler) { - if (_.isFunction(options)) { - handler = options; - options = {}; - } else { - options = options || {}; - } - _converse.connection.addHandler(handler, options.ns, name, options.type, options.id, options.from, options); - } - }, - 'waitUntil': function waitUntil(name) { - var promise = _converse.promises[name]; - if (_.isUndefined(promise)) { - return null; - } - return promise.promise; - }, - 'send': function send(stanza) { - _converse.connection.send(stanza); - } + _converse.emit('noResumeableSession', this); + + reject(xhr.responseText); + }; + + xhr.send(); + }); }; - // The public API - return { - 'initialize': function initialize(settings, callback) { - return _converse.initialize(settings, callback); - }, + this.startNewBOSHSession = function () { + var xhr = new XMLHttpRequest(); + xhr.open('GET', _converse.prebind_url, true); + xhr.setRequestHeader('Accept', "application/json, text/javascript"); - 'plugins': { - 'add': function add(name, plugin) { - plugin.__name__ = name; - if (!_.isUndefined(_converse.pluggable.plugins[name])) { - throw new TypeError("Error: plugin with name \"" + name + "\" has already been " + 'registered!'); - } else { - _converse.pluggable.plugins[name] = plugin; - } - } - }, - 'env': { - '$build': $build, - '$iq': $iq, - '$msg': $msg, - '$pres': $pres, - 'Backbone': Backbone, - 'Promise': Promise, - 'Strophe': Strophe, - '_': _, - 'b64_sha1': b64_sha1, - 'moment': moment, - 'sizzle': sizzle, - 'utils': utils + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 400) { + var data = JSON.parse(xhr.responseText); + + _converse.connection.attach(data.jid, data.sid, data.rid, _converse.onConnectStatusChanged); + } else { + xhr.onerror(); } + }; + + xhr.onerror = function () { + delete _converse.connection; + + _converse.emit('noResumeableSession', this); + }; + + xhr.send(); }; + + this.restoreBOSHSession = function (jid_is_required) { + /* Tries to restore a cached BOSH session. */ + if (!this.jid) { + var msg = "restoreBOSHSession: tried to restore a \"keepalive\" session " + "but we don't have the JID for the user!"; + + if (jid_is_required) { + throw new Error(msg); + } else { + _converse.log(msg); + } + } + + try { + this.connection.restore(this.jid, this.onConnectStatusChanged); + return true; + } catch (e) { + _converse.log("Could not restore session for jid: " + this.jid + " Error message: " + e.message); + + this.clearSession(); // If there's a roster, we want to clear it (see #555) + + return false; + } + }; + + this.attemptPreboundSession = function (reconnecting) { + /* Handle session resumption or initialization when prebind is + * being used. + */ + if (!reconnecting) { + if (this.keepalive && this.restoreBOSHSession(true)) { + return; + } // No keepalive, or session resumption has failed. + + + if (this.jid && this.sid && this.rid) { + return this.connection.attach(this.jid, this.sid, this.rid, this.onConnectStatusChanged); + } + } + + if (this.prebind_url) { + return this.startNewBOSHSession(); + } else { + throw new Error("attemptPreboundSession: If you use prebind and not keepalive, " + "then you MUST supply JID, RID and SID values or a prebind_url."); + } + }; + + this.attemptNonPreboundSession = function (credentials, reconnecting) { + /* Handle session resumption or initialization when prebind is not being used. + * + * Two potential options exist and are handled in this method: + * 1. keepalive + * 2. auto_login + */ + if (!reconnecting && this.keepalive && this.restoreBOSHSession()) { + return; + } + + if (credentials) { + // When credentials are passed in, they override prebinding + // or credentials fetching via HTTP + this.autoLogin(credentials); + } else if (this.auto_login) { + if (this.credentials_url) { + this.fetchLoginCredentials().then(this.autoLogin.bind(this), this.autoLogin.bind(this)); + } else if (!this.jid) { + throw new Error("attemptNonPreboundSession: If you use auto_login, " + "you also need to give either a jid value (and if " + "applicable a password) or you need to pass in a URL " + "from where the username and password can be fetched " + "(via credentials_url)."); + } else { + this.autoLogin(); // Probably ANONYMOUS login + } + } else if (reconnecting) { + this.autoLogin(); + } + }; + + this.autoLogin = function (credentials) { + if (credentials) { + // If passed in, the credentials come from credentials_url, + // so we set them on the converse object. + this.jid = credentials.jid; + } + + if (this.authentication === _converse.ANONYMOUS) { + if (!this.jid) { + throw new Error("Config Error: when using anonymous login " + "you need to provide the server's domain via the 'jid' option. " + "Either when calling converse.initialize, or when calling " + "_converse.api.user.login."); + } + + if (!this.connection.reconnecting) { + this.connection.reset(); + } + + this.connection.connect(this.jid.toLowerCase(), null, this.onConnectStatusChanged); + } else if (this.authentication === _converse.LOGIN) { + var password = _.isNil(credentials) ? _converse.connection.pass || this.password : credentials.password; + + if (!password) { + if (this.auto_login) { + throw new Error("initConnection: If you use auto_login and " + "authentication='login' then you also need to provide a password."); + } + + _converse.setDisconnectionCause(Strophe.Status.AUTHFAIL, undefined, true); + + _converse.disconnect(); + + return; + } + + var resource = Strophe.getResourceFromJid(this.jid); + + if (!resource) { + this.jid = this.jid.toLowerCase() + _converse.generateResource(); + } else { + this.jid = Strophe.getBareJidFromJid(this.jid).toLowerCase() + '/' + resource; + } + + if (!this.connection.reconnecting) { + this.connection.reset(); + } + + this.connection.connect(this.jid, password, this.onConnectStatusChanged); + } + }; + + this.logIn = function (credentials, reconnecting) { + // We now try to resume or automatically set up a new session. + // Otherwise the user will be shown a login form. + if (this.authentication === _converse.PREBIND) { + this.attemptPreboundSession(reconnecting); + } else { + this.attemptNonPreboundSession(credentials, reconnecting); + } + }; + + this.initConnection = function () { + 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."); + } + + if (('WebSocket' in window || 'MozWebSocket' in window) && this.websocket_url) { + this.connection = new Strophe.Connection(this.websocket_url, this.connection_options); + } else if (this.bosh_service_url) { + this.connection = new Strophe.Connection(this.bosh_service_url, _.assignIn(this.connection_options, { + 'keepalive': this.keepalive + })); + } else { + throw new Error("initConnection: this browser does not support websockets and bosh_service_url wasn't specified."); + } + } + + _converse.emit('connectionInitialized'); + }; + + this._tearDown = function () { + /* Remove those views which are only allowed with a valid + * connection. + */ + _converse.emit('beforeTearDown'); + + _converse.unregisterPresenceHandler(); + + if (_converse.roster) { + _converse.roster.off().reset(); // Removes roster contacts + + } + + if (!_.isUndefined(_converse.session)) { + _converse.session.destroy(); + } + + window.removeEventListener('click', _converse.onUserActivity); + window.removeEventListener('focus', _converse.onUserActivity); + window.removeEventListener('keypress', _converse.onUserActivity); + window.removeEventListener('mousemove', _converse.onUserActivity); + window.removeEventListener(unloadevent, _converse.onUserActivity); + window.clearInterval(_converse.everySecondTrigger); + + _converse.emit('afterTearDown'); + + return _converse; + }; + + this.initPlugins = function () { + // If initialize gets called a second time (e.g. during tests), then we + // need to re-apply all plugins (for a new converse instance), and we + // therefore need to clear this array that prevents plugins from being + // initialized twice. + // If initialize is called for the first time, then this array is empty + // in any case. + _converse.pluggable.initialized_plugins = []; + + var whitelist = _converse.core_plugins.concat(_converse.whitelisted_plugins); + + _converse.pluggable.initializePlugins({ + 'updateSettings': function updateSettings() { + _converse.log("(DEPRECATION) " + "The `updateSettings` method has been deprecated. " + "Please use `_converse.api.settings.update` instead.", Strophe.LogLevel.WARN); + + _converse.api.settings.update.apply(_converse, arguments); + }, + '_converse': _converse + }, whitelist, _converse.blacklisted_plugins); + + _converse.emit('pluginsInitialized'); + }; // Initialization + // -------------- + // This is the end of the initialize method. + + + if (settings.connection) { + this.connection = settings.connection; + } + + function finishInitialization() { + _converse.initPlugins(); + + _converse.initConnection(); + + _converse.setUpXMLLogging(); + + _converse.logIn(); + + _converse.registerGlobalEventHandlers(); + + if (!Backbone.history.started) { + Backbone.history.start(); + } + } + + if (!_.isUndefined(_converse.connection) && _converse.connection.service === 'jasmine tests') { + finishInitialization(); + return _converse; + } else if (_.isUndefined(i18n)) { + finishInitialization(); + } else { + i18n.fetchTranslations(_converse.locale, _converse.locales, _.template(_converse.locales_url)({ + 'locale': _converse.locale + })).then(function () { + finishInitialization(); + }).catch(function (reason) { + finishInitialization(); + + _converse.log(reason, Strophe.LogLevel.ERROR); + }); + } + + return init_promise; + }; // API methods only available to plugins + + + _converse.api = { + 'connection': { + 'connected': function connected() { + return _converse.connection && _converse.connection.connected || false; + }, + 'disconnect': function disconnect() { + _converse.connection.disconnect(); + } + }, + 'emit': function emit() { + _converse.emit.apply(_converse, arguments); + }, + 'user': { + 'jid': function jid() { + return _converse.connection.jid; + }, + 'login': function login(credentials) { + _converse.initConnection(); + + _converse.logIn(credentials); + }, + 'logout': function logout() { + _converse.logOut(); + }, + 'status': { + 'get': function get() { + return _converse.xmppstatus.get('status'); + }, + 'set': function set(value, message) { + var data = { + 'status': value + }; + + if (!_.includes(_.keys(_converse.STATUS_WEIGHTS), value)) { + throw new Error('Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'); + } + + if (_.isString(message)) { + data.status_message = message; + } + + _converse.xmppstatus.sendPresence(value); + + _converse.xmppstatus.save(data); + }, + 'message': { + 'get': function get() { + return _converse.xmppstatus.get('status_message'); + }, + 'set': function set(stat) { + _converse.xmppstatus.save({ + 'status_message': stat + }); + } + } + } + }, + 'settings': { + 'update': function update(settings) { + utils.merge(_converse.default_settings, settings); + utils.merge(_converse, settings); + utils.applyUserSettings(_converse, settings, _converse.user_settings); + }, + 'get': function get(key) { + if (_.includes(_.keys(_converse.default_settings), key)) { + return _converse[key]; + } + }, + 'set': function set(key, val) { + var o = {}; + + if (_.isObject(key)) { + _.assignIn(_converse, _.pick(key, _.keys(_converse.default_settings))); + } else if (_.isString("string")) { + o[key] = val; + + _.assignIn(_converse, _.pick(o, _.keys(_converse.default_settings))); + } + } + }, + 'promises': { + 'add': function add(promises) { + promises = _.isArray(promises) ? promises : [promises]; + + _.each(promises, addPromise); + } + }, + 'contacts': { + 'get': function get(jids) { + var _transform = function _transform(jid) { + var contact = _converse.roster.get(Strophe.getBareJidFromJid(jid)); + + if (contact) { + return contact.attributes; + } + + return null; + }; + + if (_.isUndefined(jids)) { + jids = _converse.roster.pluck('jid'); + } else if (_.isString(jids)) { + return _transform(jids); + } + + return _.map(jids, _transform); + }, + '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)) { + return null; + } + + if (id.toLowerCase() === 'rid') { + return _converse.connection.rid || _converse.connection._proto.rid; + } else if (id.toLowerCase() === 'sid') { + return _converse.connection.sid || _converse.connection._proto.sid; + } + } + }, + 'listen': { + 'once': _converse.once.bind(_converse), + 'on': _converse.on.bind(_converse), + 'not': _converse.off.bind(_converse), + 'stanza': function stanza(name, options, handler) { + if (_.isFunction(options)) { + handler = options; + options = {}; + } else { + options = options || {}; + } + + _converse.connection.addHandler(handler, options.ns, name, options.type, options.id, options.from, options); + } + }, + 'waitUntil': function waitUntil(name) { + var promise = _converse.promises[name]; + + if (_.isUndefined(promise)) { + return null; + } + + return promise; + }, + 'send': function send(stanza) { + _converse.connection.send(stanza); + } + }; // The public API + + return { + 'initialize': function initialize(settings, callback) { + return _converse.initialize(settings, callback); + }, + 'plugins': { + 'add': function add(name, plugin) { + plugin.__name__ = name; + + if (!_.isUndefined(_converse.pluggable.plugins[name])) { + throw new TypeError("Error: plugin with name \"".concat(name, "\" has already been ") + 'registered!'); + } else { + _converse.pluggable.plugins[name] = plugin; + } + } + }, + 'env': { + '$build': $build, + '$iq': $iq, + '$msg': $msg, + '$pres': $pres, + 'Backbone': Backbone, + 'Promise': Promise, + 'Strophe': Strophe, + '_': _, + 'b64_sha1': b64_sha1, + 'moment': moment, + 'sizzle': sizzle, + 'utils': utils + } + }; }); //# sourceMappingURL=converse-core.js.map; - - // 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) // + /*global define */ - (function (root, factory) { - define('converse-chatboxes',["converse-core"], factory); -})(undefined, function (converse) { - "use strict"; + define('converse-chatboxes',["converse-core"], factory); +})(this, function (converse) { + "use strict"; - var _converse$env = converse.env, - Backbone = _converse$env.Backbone, - Promise = _converse$env.Promise, - Strophe = _converse$env.Strophe, - b64_sha1 = _converse$env.b64_sha1, - utils = _converse$env.utils, - _ = _converse$env._; + var _converse$env = converse.env, + Backbone = _converse$env.Backbone, + Promise = _converse$env.Promise, + Strophe = _converse$env.Strophe, + b64_sha1 = _converse$env.b64_sha1, + utils = _converse$env.utils, + _ = _converse$env._; + converse.plugins.add('converse-chatboxes', { + overrides: { + // Overrides mentioned here will be picked up by converse.js's + // plugin architecture they will replace existing methods on the + // relevant objects or classes. + disconnect: function disconnect() { + var _converse = this.__super__._converse; + _converse.chatboxviews.closeAllChatBoxes(); - converse.plugins.add('converse-chatboxes', { + return this.__super__.disconnect.apply(this, arguments); + }, + logOut: function logOut() { + var _converse = this.__super__._converse; - overrides: { - // Overrides mentioned here will be picked up by converse.js's - // plugin architecture they will replace existing methods on the - // relevant objects or classes. + _converse.chatboxviews.closeAllChatBoxes(); - disconnect: function disconnect() { - var _converse = this.__super__._converse; + return this.__super__.logOut.apply(this, arguments); + }, + initStatus: function initStatus() { + var _converse = this.__super__._converse; - _converse.chatboxviews.closeAllChatBoxes(); - return this.__super__.disconnect.apply(this, arguments); - }, + _converse.chatboxviews.closeAllChatBoxes(); - logOut: function logOut() { - var _converse = this.__super__._converse; + return this.__super__.initStatus.apply(this, arguments); + }, + onStatusInitialized: function onStatusInitialized() { + var _converse = this.__super__._converse; - _converse.chatboxviews.closeAllChatBoxes(); - return this.__super__.logOut.apply(this, arguments); - }, + _converse.chatboxes.onConnected(); - initStatus: function initStatus() { - var _converse = this.__super__._converse; + return this.__super__.onStatusInitialized.apply(this, arguments); + } + }, + 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; - _converse.chatboxviews.closeAllChatBoxes(); - return this.__super__.initStatus.apply(this, arguments); - }, + _converse.api.promises.add(['chatBoxesFetched', 'chatBoxesInitialized']); - onStatusInitialized: function onStatusInitialized() { - var _converse = this.__super__._converse; - - _converse.chatboxes.onConnected(); - return this.__super__.onStatusInitialized.apply(this, arguments); - } - }, - - 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; - - - _converse.api.promises.add(['chatBoxesFetched', 'chatBoxesInitialized']); - - function openChat(jid) { - if (!utils.isValidJID(jid)) { - return converse.log("Invalid JID \"" + jid + "\" provided in URL fragment", Strophe.LogLevel.WARN); - } - Promise.all([_converse.api.waitUntil('rosterContactsFetched'), _converse.api.waitUntil('chatBoxesFetched')]).then(function () { - _converse.api.chats.open(jid); - }); - } - _converse.router.route('converse/chat?jid=:jid', openChat); - - _converse.ChatBoxes = Backbone.Collection.extend({ - comparator: 'time_opened', - - model: function model(attrs, options) { - return new _converse.ChatBox(attrs, options); - }, - registerMessageHandler: function registerMessageHandler() { - _converse.connection.addHandler(this.onMessage.bind(this), null, 'message', 'chat'); - _converse.connection.addHandler(this.onErrorMessage.bind(this), null, 'message', 'error'); - }, - chatBoxMayBeShown: function chatBoxMayBeShown(chatbox) { - return true; - }, - onChatBoxesFetched: function onChatBoxesFetched(collection) { - var _this = this; - - /* Show chat boxes upon receiving them from sessionStorage - * - * This method gets overridden entirely in src/converse-controlbox.js - * if the controlbox plugin is active. - */ - collection.each(function (chatbox) { - if (_this.chatBoxMayBeShown(chatbox)) { - chatbox.trigger('show'); - } - }); - _converse.emit('chatBoxesFetched'); - }, - onConnected: function onConnected() { - this.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.chatboxes-" + _converse.bare_jid)); - this.registerMessageHandler(); - this.fetch({ - add: true, - success: this.onChatBoxesFetched.bind(this) - }); - }, - onErrorMessage: function onErrorMessage(message) { - /* Handler method for all incoming error message stanzas - */ - // TODO: we can likely just reuse "onMessage" below - var from_jid = Strophe.getBareJidFromJid(message.getAttribute('from')); - if (utils.isSameBareJID(from_jid, _converse.bare_jid)) { - return true; - } - // Get chat box, but only create a new one when the message has a body. - var chatbox = this.getChatBox(from_jid); - if (!chatbox) { - return true; - } - chatbox.createMessage(message, null, message); - return true; - }, - onMessage: function onMessage(message) { - /* Handler method for all incoming single-user chat "message" - * stanzas. - */ - var contact_jid = void 0, - delay = void 0, - resource = void 0, - from_jid = message.getAttribute('from'), - to_jid = message.getAttribute('to'); - - var original_stanza = message, - to_resource = Strophe.getResourceFromJid(to_jid), - is_carbon = !_.isNull(message.querySelector("received[xmlns=\"" + Strophe.NS.CARBONS + "\"]")); - - if (_converse.filter_by_resource && to_resource && to_resource !== _converse.resource) { - _converse.log("onMessage: Ignoring incoming message intended for a different resource: " + to_jid, Strophe.LogLevel.INFO); - return true; - } else if (utils.isHeadlineMessage(message)) { - // XXX: Ideally we wouldn't have to check for headline - // messages, but Prosody sends headline messages with the - // wrong type ('chat'), so we need to filter them out here. - _converse.log("onMessage: Ignoring incoming headline message sent with type 'chat' from JID: " + from_jid, Strophe.LogLevel.INFO); - return true; - } - var forwarded = message.querySelector('forwarded'); - if (!_.isNull(forwarded)) { - var forwarded_message = forwarded.querySelector('message'); - var forwarded_from = forwarded_message.getAttribute('from'); - if (is_carbon && Strophe.getBareJidFromJid(forwarded_from) !== from_jid) { - // Prevent message forging via carbons - // - // https://xmpp.org/extensions/xep-0280.html#security - return true; - } - message = forwarded_message; - delay = forwarded.querySelector('delay'); - from_jid = message.getAttribute('from'); - to_jid = message.getAttribute('to'); - } - - var from_bare_jid = Strophe.getBareJidFromJid(from_jid), - from_resource = Strophe.getResourceFromJid(from_jid), - is_me = from_bare_jid === _converse.bare_jid; - - if (is_me) { - // I am the sender, so this must be a forwarded message... - contact_jid = Strophe.getBareJidFromJid(to_jid); - resource = Strophe.getResourceFromJid(to_jid); - } else { - contact_jid = from_bare_jid; - resource = from_resource; - } - // Get chat box, but only create a new one when the message has a body. - var chatbox = this.getChatBox(contact_jid, !_.isNull(message.querySelector('body'))), - msgid = message.getAttribute('id'); - - if (chatbox) { - var messages = msgid && chatbox.messages.findWhere({ msgid: msgid }) || []; - if (_.isEmpty(messages)) { - // Only create the message when we're sure it's not a - // duplicate - chatbox.incrementUnreadMsgCounter(original_stanza); - chatbox.createMessage(message, delay, original_stanza); - } - } - _converse.emit('message', { 'stanza': original_stanza, 'chatbox': chatbox }); - return true; - }, - createChatBox: function createChatBox(jid, attrs) { - /* Creates a chat box - * - * Parameters: - * (String) jid - The JID of the user for whom a chat box - * gets created. - * (Object) attrs - Optional chat box atributes. - */ - var bare_jid = Strophe.getBareJidFromJid(jid), - roster_item = _converse.roster.get(bare_jid); - var roster_info = {}; - - if (!_.isUndefined(roster_item)) { - roster_info = { - 'fullname': _.isEmpty(roster_item.get('fullname')) ? jid : roster_item.get('fullname'), - 'image_type': roster_item.get('image_type'), - 'image': roster_item.get('image'), - 'url': roster_item.get('url') - }; - } else if (!_converse.allow_non_roster_messaging) { - _converse.log("Could not get roster item for JID " + bare_jid + ' and allow_non_roster_messaging is set to false', Strophe.LogLevel.ERROR); - return; - } - return this.create(_.assignIn({ - 'id': bare_jid, - 'jid': bare_jid, - 'fullname': jid, - 'image_type': _converse.DEFAULT_IMAGE_TYPE, - 'image': _converse.DEFAULT_IMAGE, - 'url': '' - }, roster_info, attrs || {})); - }, - getChatBox: function getChatBox(jid, create, attrs) { - /* Returns a chat box or optionally return a newly - * created one if one doesn't exist. - * - * Parameters: - * (String) jid - The JID of the user whose chat box we want - * (Boolean) create - Should a new chat box be created if none exists? - * (Object) attrs - Optional chat box atributes. - */ - jid = jid.toLowerCase(); - var chatbox = this.get(Strophe.getBareJidFromJid(jid)); - if (!chatbox && create) { - chatbox = this.createChatBox(jid, attrs); - } - return chatbox; - } - }); - - _converse.ChatBoxViews = Backbone.Overview.extend({ - initialize: function initialize() { - this.model.on("add", this.onChatBoxAdded, this); - this.model.on("destroy", this.removeChat, this); - }, - _ensureElement: function _ensureElement() { - /* Override method from backbone.js - * If the #conversejs element doesn't exist, create it. - */ - if (!this.el) { - var el = document.querySelector('#conversejs'); - if (_.isNull(el)) { - el = document.createElement('div'); - el.setAttribute('id', 'conversejs'); - // Converse.js expects a tag to be present. - document.querySelector('body').appendChild(el); - } - el.innerHTML = ''; - this.setElement(el, false); - } else { - this.setElement(_.result(this, 'el'), false); - } - }, - onChatBoxAdded: function onChatBoxAdded(item) { - // Views aren't created here, since the core code doesn't - // contain any views. Instead, they're created in overrides in - // plugins, such as in converse-chatview.js and converse-muc.js - return this.get(item.get('id')); - }, - removeChat: function removeChat(item) { - this.remove(item.get('id')); - }, - closeAllChatBoxes: function closeAllChatBoxes() { - /* This method gets overridden in src/converse-controlbox.js if - * the controlbox plugin is active. - */ - this.each(function (view) { - view.close(); - }); - return this; - }, - chatBoxMayBeShown: function chatBoxMayBeShown(chatbox) { - return this.model.chatBoxMayBeShown(chatbox); - }, - getChatBox: function getChatBox(attrs, create) { - var chatbox = this.model.get(attrs.jid); - if (!chatbox && create) { - chatbox = this.model.create(attrs, { - 'error': function error(model, response) { - _converse.log(response.responseText); - } - }); - } - return chatbox; - }, - showChat: function showChat(attrs) { - /* Find the chat box and show it (if it may be shown). - * If it doesn't exist, create it. - */ - var chatbox = this.getChatBox(attrs, true); - if (this.chatBoxMayBeShown(chatbox)) { - chatbox.trigger('show', true); - } - return chatbox; - } - }); - - // BEGIN: Event handlers - _converse.api.listen.on('pluginsInitialized', function () { - _converse.chatboxes = new _converse.ChatBoxes(); - _converse.chatboxviews = new _converse.ChatBoxViews({ - 'model': _converse.chatboxes - }); - _converse.emit('chatBoxesInitialized'); - }); - - _converse.api.listen.on('beforeTearDown', function () { - _converse.chatboxes.remove(); // Don't call off(), events won't get re-registered upon reconnect. - delete _converse.chatboxes.browserStorage; - }); - // END: Event handlers - - _converse.getViewForChatBox = function (chatbox) { - if (!chatbox) { - return; - } - return _converse.chatboxviews.get(chatbox.get('id')); - }; - - /* We extend the default converse.js API */ - _.extend(_converse.api, { - 'chats': { - 'open': function open(jids, attrs) { - if (_.isUndefined(jids)) { - _converse.log("chats.open: You need to provide at least one JID", Strophe.LogLevel.ERROR); - return null; - } else if (_.isString(jids)) { - var chatbox = _converse.chatboxes.getChatBox(jids, true, attrs); - if (_.isNil(chatbox)) { - _converse.log("Could not open chatbox for JID: " + jids); - return; - } - return _converse.getViewForChatBox(chatbox.trigger('show')); - } - return _.map(jids, function (jid) { - return _converse.getViewForChatBox(_converse.chatboxes.getChatBox(jid, true, attrs).trigger('show')); - }); - }, - 'get': function get(jids) { - if (_.isUndefined(jids)) { - var result = []; - _converse.chatboxes.each(function (chatbox) { - // FIXME: Leaky abstraction from MUC. We need to add a - // base type for chat boxes, and check for that. - if (chatbox.get('type') !== 'chatroom') { - result.push(_converse.getViewForChatBox(chatbox)); - } - }); - return result; - } else if (_.isString(jids)) { - return _converse.getViewForChatBox(_converse.chatboxes.getChatBox(jids)); - } - return _.map(jids, _.partial(_.flow(_converse.chatboxes.getChatBox.bind(_converse.chatboxes), _converse.getViewForChatBox.bind(_converse)), _, true)); - } - } - }); + function openChat(jid) { + if (!utils.isValidJID(jid)) { + return converse.log("Invalid JID \"".concat(jid, "\" provided in URL fragment"), Strophe.LogLevel.WARN); } - }); - return converse; + + Promise.all([_converse.api.waitUntil('rosterContactsFetched'), _converse.api.waitUntil('chatBoxesFetched')]).then(function () { + _converse.api.chats.open(jid); + }); + } + + _converse.router.route('converse/chat?jid=:jid', openChat); + + _converse.ChatBoxes = Backbone.Collection.extend({ + comparator: 'time_opened', + model: function model(attrs, options) { + return new _converse.ChatBox(attrs, options); + }, + registerMessageHandler: function registerMessageHandler() { + _converse.connection.addHandler(this.onMessage.bind(this), null, 'message', 'chat'); + + _converse.connection.addHandler(this.onErrorMessage.bind(this), null, 'message', 'error'); + }, + chatBoxMayBeShown: function chatBoxMayBeShown(chatbox) { + return true; + }, + onChatBoxesFetched: function onChatBoxesFetched(collection) { + var _this = this; + + /* Show chat boxes upon receiving them from sessionStorage + * + * This method gets overridden entirely in src/converse-controlbox.js + * if the controlbox plugin is active. + */ + collection.each(function (chatbox) { + if (_this.chatBoxMayBeShown(chatbox)) { + chatbox.trigger('show'); + } + }); + + _converse.emit('chatBoxesFetched'); + }, + onConnected: function onConnected() { + this.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1("converse.chatboxes-".concat(_converse.bare_jid))); + this.registerMessageHandler(); + this.fetch({ + add: true, + success: this.onChatBoxesFetched.bind(this) + }); + }, + onErrorMessage: function onErrorMessage(message) { + /* Handler method for all incoming error message stanzas + */ + // TODO: we can likely just reuse "onMessage" below + var from_jid = Strophe.getBareJidFromJid(message.getAttribute('from')); + + if (utils.isSameBareJID(from_jid, _converse.bare_jid)) { + return true; + } // Get chat box, but only create a new one when the message has a body. + + + var chatbox = this.getChatBox(from_jid); + + if (!chatbox) { + return true; + } + + chatbox.createMessage(message, null, message); + return true; + }, + onMessage: function onMessage(message) { + /* Handler method for all incoming single-user chat "message" + * stanzas. + */ + var contact_jid, + delay, + resource, + from_jid = message.getAttribute('from'), + to_jid = message.getAttribute('to'); + var original_stanza = message, + to_resource = Strophe.getResourceFromJid(to_jid), + is_carbon = !_.isNull(message.querySelector("received[xmlns=\"".concat(Strophe.NS.CARBONS, "\"]"))); + + if (_converse.filter_by_resource && to_resource && to_resource !== _converse.resource) { + _converse.log("onMessage: Ignoring incoming message intended for a different resource: ".concat(to_jid), Strophe.LogLevel.INFO); + + return true; + } else if (utils.isHeadlineMessage(message)) { + // XXX: Ideally we wouldn't have to check for headline + // messages, but Prosody sends headline messages with the + // wrong type ('chat'), so we need to filter them out here. + _converse.log("onMessage: Ignoring incoming headline message sent with type 'chat' from JID: ".concat(from_jid), Strophe.LogLevel.INFO); + + return true; + } + + var forwarded = message.querySelector('forwarded'); + + if (!_.isNull(forwarded)) { + var forwarded_message = forwarded.querySelector('message'); + var forwarded_from = forwarded_message.getAttribute('from'); + + if (is_carbon && Strophe.getBareJidFromJid(forwarded_from) !== from_jid) { + // Prevent message forging via carbons + // + // https://xmpp.org/extensions/xep-0280.html#security + return true; + } + + message = forwarded_message; + delay = forwarded.querySelector('delay'); + from_jid = message.getAttribute('from'); + to_jid = message.getAttribute('to'); + } + + var from_bare_jid = Strophe.getBareJidFromJid(from_jid), + from_resource = Strophe.getResourceFromJid(from_jid), + is_me = from_bare_jid === _converse.bare_jid; + + if (is_me) { + // I am the sender, so this must be a forwarded message... + contact_jid = Strophe.getBareJidFromJid(to_jid); + resource = Strophe.getResourceFromJid(to_jid); + } else { + contact_jid = from_bare_jid; + resource = from_resource; + } // Get chat box, but only create a new one when the message has a body. + + + var chatbox = this.getChatBox(contact_jid, !_.isNull(message.querySelector('body'))), + msgid = message.getAttribute('id'); + + if (chatbox) { + var messages = msgid && chatbox.messages.findWhere({ + msgid: msgid + }) || []; + + if (_.isEmpty(messages)) { + // Only create the message when we're sure it's not a + // duplicate + chatbox.incrementUnreadMsgCounter(original_stanza); + chatbox.createMessage(message, delay, original_stanza); + } + } + + _converse.emit('message', { + 'stanza': original_stanza, + 'chatbox': chatbox + }); + + return true; + }, + createChatBox: function createChatBox(jid, attrs) { + /* Creates a chat box + * + * Parameters: + * (String) jid - The JID of the user for whom a chat box + * gets created. + * (Object) attrs - Optional chat box atributes. + */ + var bare_jid = Strophe.getBareJidFromJid(jid), + roster_item = _converse.roster.get(bare_jid); + + var roster_info = {}; + + if (!_.isUndefined(roster_item)) { + roster_info = { + 'fullname': _.isEmpty(roster_item.get('fullname')) ? jid : roster_item.get('fullname'), + 'image_type': roster_item.get('image_type'), + 'image': roster_item.get('image'), + 'url': roster_item.get('url') + }; + } else if (!_converse.allow_non_roster_messaging) { + _converse.log("Could not get roster item for JID ".concat(bare_jid) + ' and allow_non_roster_messaging is set to false', Strophe.LogLevel.ERROR); + + return; + } + + return this.create(_.assignIn({ + 'id': bare_jid, + 'jid': bare_jid, + 'fullname': jid, + 'image_type': _converse.DEFAULT_IMAGE_TYPE, + 'image': _converse.DEFAULT_IMAGE, + 'url': '' + }, roster_info, attrs || {})); + }, + getChatBox: function getChatBox(jid, create, attrs) { + /* Returns a chat box or optionally return a newly + * created one if one doesn't exist. + * + * Parameters: + * (String) jid - The JID of the user whose chat box we want + * (Boolean) create - Should a new chat box be created if none exists? + * (Object) attrs - Optional chat box atributes. + */ + jid = jid.toLowerCase(); + var chatbox = this.get(Strophe.getBareJidFromJid(jid)); + + if (!chatbox && create) { + chatbox = this.createChatBox(jid, attrs); + } + + return chatbox; + } + }); + _converse.ChatBoxViews = Backbone.Overview.extend({ + initialize: function initialize() { + this.model.on("add", this.onChatBoxAdded, this); + this.model.on("destroy", this.removeChat, this); + }, + _ensureElement: function _ensureElement() { + /* Override method from backbone.js + * If the #conversejs element doesn't exist, create it. + */ + if (!this.el) { + var el = document.querySelector('#conversejs'); + + if (_.isNull(el)) { + el = document.createElement('div'); + el.setAttribute('id', 'conversejs'); // Converse.js expects a tag to be present. + + document.querySelector('body').appendChild(el); + } + + el.innerHTML = ''; + this.setElement(el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + }, + onChatBoxAdded: function onChatBoxAdded(item) { + // Views aren't created here, since the core code doesn't + // contain any views. Instead, they're created in overrides in + // plugins, such as in converse-chatview.js and converse-muc.js + return this.get(item.get('id')); + }, + removeChat: function removeChat(item) { + this.remove(item.get('id')); + }, + closeAllChatBoxes: function closeAllChatBoxes() { + /* This method gets overridden in src/converse-controlbox.js if + * the controlbox plugin is active. + */ + this.each(function (view) { + view.close(); + }); + return this; + }, + chatBoxMayBeShown: function chatBoxMayBeShown(chatbox) { + return this.model.chatBoxMayBeShown(chatbox); + }, + getChatBox: function getChatBox(attrs, create) { + var chatbox = this.model.get(attrs.jid); + + if (!chatbox && create) { + chatbox = this.model.create(attrs, { + 'error': function error(model, response) { + _converse.log(response.responseText); + } + }); + } + + return chatbox; + }, + showChat: function showChat(attrs) { + /* Find the chat box and show it (if it may be shown). + * If it doesn't exist, create it. + */ + var chatbox = this.getChatBox(attrs, true); + + if (this.chatBoxMayBeShown(chatbox)) { + chatbox.trigger('show', true); + } + + return chatbox; + } + }); // BEGIN: Event handlers + + _converse.api.listen.on('pluginsInitialized', function () { + _converse.chatboxes = new _converse.ChatBoxes(); + _converse.chatboxviews = new _converse.ChatBoxViews({ + 'model': _converse.chatboxes + }); + + _converse.emit('chatBoxesInitialized'); + }); + + _converse.api.listen.on('beforeTearDown', function () { + _converse.chatboxes.remove(); // Don't call off(), events won't get re-registered upon reconnect. + + + delete _converse.chatboxes.browserStorage; + }); // END: Event handlers + + + _converse.getViewForChatBox = function (chatbox) { + if (!chatbox) { + return; + } + + return _converse.chatboxviews.get(chatbox.get('id')); + }; + /* We extend the default converse.js API */ + + + _.extend(_converse.api, { + 'chats': { + 'open': function open(jids, attrs) { + if (_.isUndefined(jids)) { + _converse.log("chats.open: You need to provide at least one JID", Strophe.LogLevel.ERROR); + + return null; + } else if (_.isString(jids)) { + var chatbox = _converse.chatboxes.getChatBox(jids, true, attrs); + + if (_.isNil(chatbox)) { + _converse.log("Could not open chatbox for JID: " + jids); + + return; + } + + return _converse.getViewForChatBox(chatbox.trigger('show')); + } + + return _.map(jids, function (jid) { + return _converse.getViewForChatBox(_converse.chatboxes.getChatBox(jid, true, attrs).trigger('show')); + }); + }, + 'get': function get(jids) { + if (_.isUndefined(jids)) { + var result = []; + + _converse.chatboxes.each(function (chatbox) { + // FIXME: Leaky abstraction from MUC. We need to add a + // base type for chat boxes, and check for that. + if (chatbox.get('type') !== 'chatroom') { + result.push(_converse.getViewForChatBox(chatbox)); + } + }); + + return result; + } else if (_.isString(jids)) { + return _converse.getViewForChatBox(_converse.chatboxes.getChatBox(jids)); + } + + return _.map(jids, _.partial(_.flow(_converse.chatboxes.getChatBox.bind(_converse.chatboxes), _converse.getViewForChatBox.bind(_converse)), _, true)); + } + } + }); + } + }); + return converse; }); //# sourceMappingURL=converse-chatboxes.js.map; /* jshint maxerr: 10000 */ @@ -9958,7 +10354,7 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol ns.imagePathPNG = 'https://cdn.jsdelivr.net/emojione/assets/' + ns.emojiVersion + '/png/'; ns.defaultPathPNG = ns.imagePathPNG; ns.imageTitleTag = true; // set to false to remove title attribute from img tag - ns.sprites = false; // if this is true then sprite markup will be used + ns.sprites = true; // if this is true then sprite markup will be used ns.spriteSize = '32'; ns.unicodeAlt = true; // use the unicode char as the alt attribute (makes copy and pasting the resulting text better) ns.ascii = false; // change to true to convert ascii smileys @@ -10370,6 +10766,7 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol }(this.emojione = this.emojione || {})); if(typeof module === "object") module.exports = this.emojione; + define("emojione", (function (global) { return function () { var ret, fn; @@ -12125,111 +12522,115 @@ define('tpl',['require','exports','module','lodash'],function(require, exports) })(typeof global === "object" ? global : this); -define('tpl!chatbox', ['lodash'], function(_) {return function(obj) { -obj || (obj = {}); +define('tpl!chatbox', ['lodash'], function(_) {return function(o) { var __t, __p = '', __e = _.escape, __j = Array.prototype.join; function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '
\n
\n \n
\n '; - if (url) { ; -__p += '\n \n '; - } ; -__p += '\n ' + -__e( title ) + -'\n '; - if (url) { ; -__p += '\n \n '; - } ; -__p += '\n

\n

\n
\n
\n
\n
\n
\n \n '; - if (show_textarea) { ; + if (o.show_textarea) { ; __p += '\n
\n '; - if (show_toolbar) { ; + if (o.show_toolbar) { ; __p += '\n
    \n '; } ; -__p += '\n \n\n '; - if (show_send_button) { ; -__p += '\n \n '; +__p += '"\n placeholder="' + +__e(o.label_personal_message) + +'">\n '; + if (o.show_send_button) { ; +__p += '\n \n '; } ; __p += '\n \n '; } ; __p += '\n
    \n
    \n'; - -} return __p };}); -define('tpl!new_day', ['lodash'], function(_) {return function(obj) { -obj || (obj = {}); -var __t, __p = '', __e = _.escape; -with (obj) { -__p += '\n'; - -} -return __p -};}); - - -define('tpl!action', ['lodash'], function(_) {return function(obj) { -obj || (obj = {}); -var __t, __p = '', __e = _.escape; -with (obj) { -__p += '
    \n ' + -__e(time) + -' **' + -__e(username) + -' \n \n
    \n'; - -} -return __p -};}); - - -define('tpl!emojis', ['lodash'], function(_) {return function(obj) { -obj || (obj = {}); +define('tpl!chatbox_head', ['lodash'], function(_) {return function(o) { var __t, __p = '', __e = _.escape, __j = Array.prototype.join; function print() { __p += __j.call(arguments, '') } -with (obj) { +__p += '
    \n \n User Avatar\n
    \n '; + if (o.url) { ; +__p += '\n \n '; + } ; +__p += '\n ' + +__e( o.fullname ) + +'\n '; + if (o.url) { ; +__p += '\n \n '; + } ; +__p += '\n

    ' + +__e( o.status ) + +'

    \n
    \n
    \n'; +return __p +};}); - _.forEach(emojis_by_category, function (obj, category) { ; + +define('tpl!new_day', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape; +__p += '\n'; +return __p +};}); + + +define('tpl!action', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape; +__p += '
    \n ' + +__e(o.time) + +' **' + +__e(o.username) + +' \n \n
    \n'; +return __p +};}); + + +define('tpl!emojis', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } + + _.forEach(o.emojis_by_category, function (obj, category) { ; __p += '\n \n'; }); ; __p += '\n\n'; - -} return __p };}); -define('tpl!message', ['lodash'], function(_) {return function(obj) { -obj || (obj = {}); +define('tpl!message', ['lodash'], function(_) {return function(o) { var __t, __p = '', __e = _.escape; -with (obj) { __p += '
    \n ' + -__e(time) + +__e(o.time) + ' ' + -__e(username) + +__e(o.username) + ': \n \n
    \n'; - -} return __p };}); -define('tpl!help_message', ['lodash'], function(_) {return function(obj) { -obj || (obj = {}); +define('tpl!help_message', ['lodash'], function(_) {return function(o) { var __t, __p = '', __e = _.escape; -with (obj) { __p += '
    ' + -__e(message) + +__e(o.message) + '
    \n'; - -} return __p };}); -define('tpl!toolbar', ['lodash'], function(_) {return function(obj) { -obj || (obj = {}); +define('tpl!toolbar', ['lodash'], function(_) {return function(o) { var __t, __p = '', __e = _.escape, __j = Array.prototype.join; function print() { __p += __j.call(arguments, '') } -with (obj) { - if (use_emoji) { ; + if (o.use_emoji) { ; __p += '\n
  • \n
      \n
    • \n'; } ; __p += '\n'; - if (show_call_button) { ; + if (o.show_call_button) { ; __p += '\n
    • \n'; } ; __p += '\n'; - if (show_clear_button) { ; + if (o.show_clear_button) { ; __p += '\n
    • \n'; } ; __p += '\n'; - -} return __p };}); -define('tpl!avatar', ['lodash'], function(_) {return function(obj) { -obj || (obj = {}); +define('tpl!spinner', ['lodash'], function(_) {return function(o) { var __t, __p = ''; -with (obj) { -__p += '\n'; - -} -return __p -};}); - - -define('tpl!spinner', ['lodash'], function(_) {return function(obj) { -obj || (obj = {}); -var __t, __p = ''; -with (obj) { __p += '\n'; +return __p +};}); -} +// 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) +// + +/*global define */ +(function (root, factory) { + define('converse-chatview',["jquery.noconflict", "converse-core", "converse-chatboxes", "emojione", "xss", "tpl!chatbox", "tpl!chatbox_head", "tpl!new_day", "tpl!action", "tpl!emojis", "tpl!message", "tpl!help_message", "tpl!toolbar", "tpl!spinner"], factory); +})(this, function ($, converse, dummy, emojione, xss, tpl_chatbox, tpl_chatbox_head, tpl_new_day, tpl_action, tpl_emojis, tpl_message, tpl_help_message, tpl_toolbar, tpl_spinner) { + "use strict"; + + var _converse$env = converse.env, + $msg = _converse$env.$msg, + Backbone = _converse$env.Backbone, + Strophe = _converse$env.Strophe, + _ = _converse$env._, + b64_sha1 = _converse$env.b64_sha1, + moment = _converse$env.moment, + utils = _converse$env.utils; + var KEY = { + ENTER: 13, + FORWARD_SLASH: 47 + }; + converse.plugins.add('converse-chatview', { + overrides: { + // Overrides mentioned here will be picked up by converse.js's + // plugin architecture they will replace existing methods on the + // relevant objects or classes. + // + // New functions which don't exist yet can also be added. + // + registerGlobalEventHandlers: function registerGlobalEventHandlers() { + this.__super__.registerGlobalEventHandlers(); + + document.addEventListener('click', function (ev) { + if (_.includes(ev.target.classList, 'toggle-toolbar-menu') || _.includes(ev.target.classList, 'insert-emoji')) { + return; + } + + utils.slideInAllElements(document.querySelectorAll('.toolbar-menu')); + }); + }, + ChatBoxViews: { + onChatBoxAdded: function onChatBoxAdded(item) { + var _converse = this.__super__._converse; + var view = this.get(item.get('id')); + + if (!view) { + view = new _converse.ChatBoxView({ + model: item + }); + this.add(item.get('id'), view); + return view; + } else { + return this.__super__.onChatBoxAdded.apply(this, arguments); + } + } + } + }, + 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, + __ = _converse.__; + + _converse.api.settings.update({ + 'use_emojione': true, + 'emojione_image_path': emojione.imagePathPNG, + 'chatview_avatar_height': 32, + 'chatview_avatar_width': 32, + 'show_toolbar': true, + 'time_format': 'HH:mm', + 'visible_toolbar_buttons': { + 'emoji': true, + 'call': false, + 'clear': true + } + }); + + emojione.imagePathPNG = _converse.emojione_image_path; + emojione.ascii = true; + + function onWindowStateChanged(data) { + _converse.chatboxviews.each(function (chatboxview) { + chatboxview.onWindowStateChanged(data.state); + }); + } + + _converse.api.listen.on('windowStateChanged', onWindowStateChanged); + + _converse.EmojiPicker = Backbone.Model.extend({ + defaults: { + 'current_category': 'people', + 'current_skintone': '', + 'scroll_position': 0 + }, + initialize: function initialize() { + var id = "converse.emoji-".concat(_converse.bare_jid); + this.id = id; + this.browserStorage = new Backbone.BrowserStorage[_converse.storage](id); + } + }); + _converse.EmojiPickerView = Backbone.View.extend({ + className: 'emoji-picker-container toolbar-menu collapsed', + events: { + 'click .emoji-category-picker li.emoji-category': 'chooseCategory', + 'click .emoji-skintone-picker li.emoji-skintone': 'chooseSkinTone' + }, + initialize: function initialize() { + this.model.on('change:current_skintone', this.render, this); + this.model.on('change:current_category', this.render, this); + this.setScrollPosition = _.debounce(this.setScrollPosition, 50); + }, + render: function render() { + var _this = this; + + var emojis_html = tpl_emojis(_.extend(this.model.toJSON(), { + 'transform': _converse.use_emojione ? emojione.shortnameToImage : emojione.shortnameToUnicode, + 'emojis_by_category': utils.getEmojisByCategory(_converse, emojione), + 'toned_emojis': utils.getTonedEmojis(_converse), + 'skintones': ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'], + 'shouldBeHidden': this.shouldBeHidden + })); + this.el.innerHTML = emojis_html; + + _.forEach(this.el.querySelectorAll('.emoji-picker'), function (el) { + el.addEventListener('scroll', _this.setScrollPosition.bind(_this)); + }); + + this.restoreScrollPosition(); + return this; + }, + shouldBeHidden: function shouldBeHidden(shortname, current_skintone, toned_emojis) { + /* Helper method for the template which decides whether an + * emoji should be hidden, based on which skin tone is + * currently being applied. + */ + if (_.includes(shortname, '_tone')) { + if (!current_skintone || !_.includes(shortname, current_skintone)) { + return true; + } + } else { + if (current_skintone && _.includes(toned_emojis, shortname)) { + return true; + } + } + + return false; + }, + restoreScrollPosition: function restoreScrollPosition() { + var current_picker = _.difference(this.el.querySelectorAll('.emoji-picker'), this.el.querySelectorAll('.emoji-picker.hidden')); + + if (current_picker.length === 1 && this.model.get('scroll_position')) { + current_picker[0].scrollTop = this.model.get('scroll_position'); + } + }, + setScrollPosition: function setScrollPosition(ev) { + this.model.save('scroll_position', ev.target.scrollTop); + }, + chooseSkinTone: function chooseSkinTone(ev) { + ev.preventDefault(); + ev.stopPropagation(); + var target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target; + var skintone = target.getAttribute("data-skintone").trim(); + + if (this.model.get('current_skintone') === skintone) { + this.model.save({ + 'current_skintone': '' + }); + } else { + this.model.save({ + 'current_skintone': skintone + }); + } + }, + chooseCategory: function chooseCategory(ev) { + ev.preventDefault(); + ev.stopPropagation(); + var target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target; + var category = target.getAttribute("data-category").trim(); + this.model.save({ + 'current_category': category, + 'scroll_position': 0 + }); + } + }); + _converse.ChatBoxHeading = Backbone.View.extend({ + initialize: function initialize() { + this.model.on('change:image', this.render, this); + this.model.on('change:status', this.onStatusMessageChanged, this); + this.model.on('change:fullname', this.render, this); + }, + render: function render() { + this.el.innerHTML = tpl_chatbox_head(_.extend(this.model.toJSON(), { + 'avatar_width': _converse.chatview_avatar_width, + 'avatar_height': _converse.chatview_avatar_height, + 'info_close': __('Close this chat box') + })); + return this; + }, + onStatusMessageChanged: function onStatusMessageChanged(item) { + this.render(); + + _converse.emit('contactStatusMessageChanged', { + 'contact': item.attributes, + 'message': item.get('status') + }); + } + }); + _converse.ChatBoxView = Backbone.View.extend({ + length: 200, + className: 'chatbox hidden', + is_chatroom: false, + // Leaky abstraction from MUC + events: { + 'click .close-chatbox-button': 'close', + 'keypress .chat-textarea': 'keyPressed', + 'click .send-button': 'onFormSubmitted', + 'click .toggle-smiley': 'toggleEmojiMenu', + 'click .toggle-smiley ul.emoji-picker li': 'insertEmoji', + 'click .toggle-clear': 'clearMessages', + 'click .toggle-call': 'toggleCall', + 'click .new-msgs-indicator': 'viewUnreadMessages' + }, + initialize: function initialize() { + this.markScrolled = _.debounce(this.markScrolled, 100); + this.createEmojiPicker(); + this.model.messages.on('add', this.onMessageAdded, this); + this.model.on('show', this.show, this); + this.model.on('destroy', this.remove, this); // TODO check for changed fullname as well + + this.model.on('change:chat_state', this.sendChatState, this); + this.model.on('change:chat_status', this.onChatStatusChanged, this); + this.model.on('showHelpMessages', this.showHelpMessages, this); + this.model.on('sendMessage', this.sendMessage, this); + this.render().renderToolbar().insertHeading().fetchMessages(); + utils.refreshWebkit(); + + _converse.emit('chatBoxOpened', this); + + _converse.emit('chatBoxInitialized', this); + }, + render: function render() { + this.el.setAttribute('id', this.model.get('box_id')); + this.el.innerHTML = tpl_chatbox(_.extend(this.model.toJSON(), { + label_personal_message: __('Personal message'), + label_send: __('Send'), + show_send_button: _converse.show_send_button, + show_textarea: true, + show_toolbar: _converse.show_toolbar, + unread_msgs: __('You have unread messages') + })); + this.content = this.el.querySelector('.chat-content'); + this.$content = $(this.content); + return this; + }, + insertHeading: function insertHeading() { + this.heading = new _converse.ChatBoxHeading({ + 'model': this.model + }); + this.heading.render(); + this.heading.chatview = this; + var flyout = this.el.querySelector('.flyout'); + flyout.insertBefore(this.heading.el, flyout.querySelector('.chat-body')); + return this; + }, + createEmojiPicker: function createEmojiPicker() { + if (_.isUndefined(_converse.emojipicker)) { + _converse.emojipicker = new _converse.EmojiPicker(); + + _converse.emojipicker.fetch(); + } + + this.emoji_picker_view = new _converse.EmojiPickerView({ + 'model': _converse.emojipicker + }); + }, + afterMessagesFetched: function afterMessagesFetched() { + this.insertIntoDOM(); + this.scrollDown(); // We only start listening for the scroll event after + // cached messages have been fetched + + this.$content.on('scroll', this.markScrolled.bind(this)); + + _converse.emit('afterMessagesFetched', this); + }, + fetchMessages: function fetchMessages() { + this.model.messages.fetch({ + 'add': true, + 'success': this.afterMessagesFetched.bind(this), + 'error': this.afterMessagesFetched.bind(this) + }); + return this; + }, + insertIntoDOM: function insertIntoDOM() { + /* This method gets overridden in src/converse-controlbox.js + * as well as src/converse-muc.js (if those plugins are + * enabled). + */ + var container = document.querySelector('#conversejs'); + + if (this.el.parentNode !== container) { + container.insertBefore(this.el, container.firstChild); + } + + return this; + }, + clearStatusNotification: function clearStatusNotification() { + this.$content.find('div.chat-event').remove(); + }, + showStatusNotification: function showStatusNotification(message, keep_old, permanent) { + if (!keep_old) { + this.clearStatusNotification(); + } + + var $el = $('
      ').text(message); + + if (!permanent) { + $el.addClass('chat-event'); + } + + this.content.insertAdjacentElement('beforeend', $el[0]); + this.scrollDown(); + }, + addSpinner: function addSpinner() { + var append = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; + + if (_.isNull(this.el.querySelector('.spinner'))) { + if (append) { + this.content.insertAdjacentHTML('beforeend', tpl_spinner()); + this.scrollDown(); + } else { + this.content.insertAdjacentHTML('afterbegin', tpl_spinner()); + } + } + }, + clearSpinner: function clearSpinner() { + _.each(this.content.querySelectorAll('span.spinner'), function (el) { + return el.parentNode.removeChild(el); + }); + }, + insertDayIndicator: function insertDayIndicator(date, prepend) { + /* Appends (or prepends if "prepend" is truthy) an indicator + * into the chat area, showing the day as given by the + * passed in date. + * + * Parameters: + * (String) date - An ISO8601 date string. + */ + var day_date = moment(date).startOf('day'); + var insert = prepend ? this.$content.prepend : this.$content.append; + insert.call(this.$content, tpl_new_day({ + isodate: day_date.format(), + datestring: day_date.format("dddd MMM Do YYYY") + })); + }, + insertMessage: function insertMessage(attrs, prepend) { + var _this2 = this; + + /* Helper method which appends a message (or prepends if the + * 2nd parameter is set to true) to the end of the chat box's + * content area. + * + * Parameters: + * (Object) attrs: An object containing the message attributes. + */ + var insert = prepend ? this.$content.prepend : this.$content.append; + + _.flow(function ($el) { + insert.call(_this2.$content, $el); + return $el; + }, this.scrollDown.bind(this))(this.renderMessage(attrs)); + }, + showMessage: function showMessage(attrs) { + /* Inserts a chat message into the content area of the chat box. + * Will also insert a new day indicator if the message is on a + * different day. + * + * The message to show may either be newer than the newest + * message, or older than the oldest message. + * + * Parameters: + * (Object) attrs: An object containing the message + * attributes. + */ + var current_msg_date = moment(attrs.time) || moment; + var $first_msg = this.$content.find('.chat-message:first'), + first_msg_date = $first_msg.data('isodate'); + + if (!first_msg_date) { + // This is the first received message, so we insert a + // date indicator before it. + this.insertDayIndicator(current_msg_date); + this.insertMessage(attrs); + return; + } + + var last_msg_date = this.$content.find('.chat-message:last').data('isodate'); + + if (current_msg_date.isAfter(last_msg_date) || current_msg_date.isSame(last_msg_date)) { + // The new message is after the last message + if (current_msg_date.isAfter(last_msg_date, 'day')) { + // Append a new day indicator + this.insertDayIndicator(current_msg_date); + } + + this.insertMessage(attrs); + return; + } + + if (current_msg_date.isBefore(first_msg_date) || current_msg_date.isSame(first_msg_date)) { + // The message is before the first, but on the same day. + // We need to prepend the message immediately before the + // first message (so that it'll still be after the day + // indicator). + this.insertMessage(attrs, 'prepend'); + + if (current_msg_date.isBefore(first_msg_date, 'day')) { + // This message is also on a different day, so + // we prepend a day indicator. + this.insertDayIndicator(current_msg_date, 'prepend'); + } + + return; + } // Find the correct place to position the message + + + current_msg_date = current_msg_date.format(); + + var msg_dates = _.map(this.$content.find('.chat-message'), function (el) { + return $(el).data('isodate'); + }); + + msg_dates.push(current_msg_date); + msg_dates.sort(); + var idx = msg_dates.indexOf(current_msg_date) - 1; + var $latest_message = this.$content.find(".chat-message[data-isodate=\"".concat(msg_dates[idx], "\"]:last")); + + _.flow(function ($el) { + $el.insertAfter($latest_message); + return $el; + }, this.scrollDown.bind(this))(this.renderMessage(attrs)); + }, + getExtraMessageTemplateAttributes: function getExtraMessageTemplateAttributes() { + /* Provides a hook for sending more attributes to the + * message template. + * + * Parameters: + * (Object) attrs: An object containing message attributes. + */ + return {}; + }, + getExtraMessageClasses: function getExtraMessageClasses(attrs) { + return attrs.delayed && 'delayed' || ''; + }, + renderMessage: function renderMessage(attrs) { + /* Renders a chat message based on the passed in attributes. + * + * Parameters: + * (Object) attrs: An object containing the message attributes. + * + * Returns: + * The DOM element representing the message. + */ + var text = attrs.message, + fullname = this.model.get('fullname') || attrs.fullname, + template, + username; + var match = text.match(/^\/(.*?)(?: (.*))?$/); + + if (match && match[1] === 'me') { + text = text.replace(/^\/me/, ''); + template = tpl_action; + + if (attrs.sender === 'me') { + fullname = _converse.xmppstatus.get('fullname') || attrs.fullname; + username = _.isNil(fullname) ? _converse.bare_jid : fullname; + } else { + username = attrs.fullname; + } + } else { + template = tpl_message; + username = attrs.sender === 'me' && __('me') || fullname; + } + + this.$content.find('div.chat-event').remove(); + + if (text.length > 8000) { + text = text.substring(0, 10) + '...'; + this.showStatusNotification(__("A very large message has been received. " + "This might be due to an attack meant to degrade the chat performance. " + "Output has been shortened."), true, true); + } + + var msg_time = moment(attrs.time) || moment; + var $msg = $(template(_.extend(this.getExtraMessageTemplateAttributes(attrs), { + 'msgid': attrs.msgid, + 'sender': attrs.sender, + 'time': msg_time.format(_converse.time_format), + 'isodate': msg_time.format(), + 'username': username, + 'extra_classes': this.getExtraMessageClasses(attrs) + }))); + var msg_content = $msg[0].querySelector('.chat-msg-content'); + msg_content.innerHTML = utils.addEmoji(_converse, emojione, utils.addHyperlinks(xss.filterXSS(text, { + 'whiteList': {} + }))); + utils.renderImageURLs(msg_content); + return $msg; + }, + showHelpMessages: function showHelpMessages(msgs, type, spinner) { + var _this3 = this; + + _.each(msgs, function (msg) { + _this3.$content.append($(tpl_help_message({ + 'type': type || 'info', + 'message': msgs + }))); + }); + + if (spinner === true) { + this.$content.append(tpl_spinner); + } else if (spinner === false) { + this.$content.find('span.spinner').remove(); + } + + return this.scrollDown(); + }, + handleChatStateMessage: function handleChatStateMessage(message) { + if (message.get('chat_state') === _converse.COMPOSING) { + if (message.get('sender') === 'me') { + this.showStatusNotification(__('Typing from another device')); + } else { + this.showStatusNotification(message.get('fullname') + ' ' + __('is typing')); + } + + this.clear_status_timeout = window.setTimeout(this.clearStatusNotification.bind(this), 30000); + } else if (message.get('chat_state') === _converse.PAUSED) { + if (message.get('sender') === 'me') { + this.showStatusNotification(__('Stopped typing on the other device')); + } else { + this.showStatusNotification(message.get('fullname') + ' ' + __('has stopped typing')); + } + } else if (_.includes([_converse.INACTIVE, _converse.ACTIVE], message.get('chat_state'))) { + this.$content.find('div.chat-event').remove(); + } else if (message.get('chat_state') === _converse.GONE) { + this.showStatusNotification(message.get('fullname') + ' ' + __('has gone away')); + } + }, + shouldShowOnTextMessage: function shouldShowOnTextMessage() { + return !this.$el.is(':visible'); + }, + handleTextMessage: function handleTextMessage(message) { + this.showMessage(_.clone(message.attributes)); + + if (utils.isNewMessage(message) && message.get('sender') === 'me') { + // We remove the "scrolled" flag so that the chat area + // gets scrolled down. We always want to scroll down + // when the user writes a message as opposed to when a + // message is received. + this.model.set('scrolled', false); + } else { + if (utils.isNewMessage(message) && this.model.get('scrolled', true)) { + this.$el.find('.new-msgs-indicator').removeClass('hidden'); + } + } + + if (this.shouldShowOnTextMessage()) { + this.show(); + } else { + this.scrollDown(); + } + }, + handleErrorMessage: function handleErrorMessage(message) { + var $message = $("[data-msgid=".concat(message.get('msgid'), "]")); + + if ($message.length) { + $message.after($('
      ').text(message.get('message'))); + this.scrollDown(); + } + }, + onMessageAdded: function onMessageAdded(message) { + /* Handler that gets called when a new message object is created. + * + * Parameters: + * (Object) message - The message Backbone object that was added. + */ + if (!_.isUndefined(this.clear_status_timeout)) { + window.clearTimeout(this.clear_status_timeout); + delete this.clear_status_timeout; + } + + if (message.get('type') === 'error') { + this.handleErrorMessage(message); + } else if (!message.get('message')) { + this.handleChatStateMessage(message); + } else { + this.handleTextMessage(message); + } + + _converse.emit('messageAdded', { + 'message': message, + 'chatbox': this.model + }); + }, + createMessageStanza: function createMessageStanza(message) { + return $msg({ + from: _converse.connection.jid, + to: this.model.get('jid'), + type: 'chat', + id: message.get('msgid') + }).c('body').t(message.get('message')).up().c(_converse.ACTIVE, { + 'xmlns': Strophe.NS.CHATSTATES + }).up(); + }, + sendMessage: function sendMessage(message) { + /* Responsible for sending off a text message. + * + * Parameters: + * (Message) message - The chat message + */ + // TODO: We might want to send to specfic resources. + // Especially in the OTR case. + var messageStanza = this.createMessageStanza(message); + + _converse.connection.send(messageStanza); + + if (_converse.forward_messages) { + // Forward the message, so that other connected resources are also aware of it. + _converse.connection.send($msg({ + to: _converse.bare_jid, + type: 'chat', + id: message.get('msgid') + }).c('forwarded', { + xmlns: 'urn:xmpp:forward:0' + }).c('delay', { + xmns: 'urn:xmpp:delay', + stamp: new Date().getTime() + }).up().cnode(messageStanza.tree())); + } + }, + onMessageSubmitted: function onMessageSubmitted(text) { + /* This method gets called once the user has typed a message + * and then pressed enter in a chat box. + * + * Parameters: + * (string) text - The chat message text. + */ + if (!_converse.connection.authenticated) { + return this.showHelpMessages(['Sorry, the connection has been lost, ' + 'and your message could not be sent'], 'error'); + } + + var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/); + + if (match) { + if (match[1] === "clear") { + return this.clearMessages(); + } else if (match[1] === "help") { + var msgs = ["/help:".concat(__('Show this menu')), "/me:".concat(__('Write in the third person')), "/clear:".concat(__('Remove messages'))]; + this.showHelpMessages(msgs); + return; + } + } + + var fullname = _converse.xmppstatus.get('fullname'); + + fullname = _.isEmpty(fullname) ? _converse.bare_jid : fullname; + var message = this.model.messages.create({ + fullname: fullname, + sender: 'me', + time: moment().format(), + message: text + }); + this.sendMessage(message); + }, + sendChatState: function sendChatState() { + /* Sends a message with the status of the user in this chat session + * as taken from the 'chat_state' attribute of the chat box. + * See XEP-0085 Chat State Notifications. + */ + _converse.connection.send($msg({ + 'to': this.model.get('jid'), + 'type': 'chat' + }).c(this.model.get('chat_state'), { + 'xmlns': Strophe.NS.CHATSTATES + }).up().c('no-store', { + 'xmlns': Strophe.NS.HINTS + }).up().c('no-permanent-store', { + 'xmlns': Strophe.NS.HINTS + })); + }, + setChatState: function setChatState(state, no_save) { + /* Mutator for setting the chat state of this chat session. + * Handles clearing of any chat state notification timeouts and + * setting new ones if necessary. + * Timeouts are set when the state being set is COMPOSING or PAUSED. + * After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE. + * See XEP-0085 Chat State Notifications. + * + * Parameters: + * (string) state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE) + * (Boolean) no_save - Just do the cleanup or setup but don't actually save the state. + */ + if (!_.isUndefined(this.chat_state_timeout)) { + window.clearTimeout(this.chat_state_timeout); + delete this.chat_state_timeout; + } + + if (state === _converse.COMPOSING) { + this.chat_state_timeout = window.setTimeout(this.setChatState.bind(this), _converse.TIMEOUTS.PAUSED, _converse.PAUSED); + } else if (state === _converse.PAUSED) { + this.chat_state_timeout = window.setTimeout(this.setChatState.bind(this), _converse.TIMEOUTS.INACTIVE, _converse.INACTIVE); + } + + if (!no_save && this.model.get('chat_state') !== state) { + this.model.set('chat_state', state); + } + + return this; + }, + onFormSubmitted: function onFormSubmitted(ev) { + ev.preventDefault(); + var textarea = this.el.querySelector('.chat-textarea'), + message = textarea.value; + textarea.value = ''; + textarea.focus(); + + if (message !== '') { + this.onMessageSubmitted(message); + + _converse.emit('messageSend', message); + } + + this.setChatState(_converse.ACTIVE); + }, + keyPressed: function keyPressed(ev) { + /* Event handler for when a key is pressed in a chat box textarea. + */ + if (ev.keyCode === KEY.ENTER) { + this.onFormSubmitted(ev); + } else { + // Set chat state to composing if keyCode is not a forward-slash + // (which would imply an internal command and not a message). + this.setChatState(_converse.COMPOSING, ev.keyCode === KEY.FORWARD_SLASH); + } + }, + clearMessages: function clearMessages(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + var result = confirm(__("Are you sure you want to clear the messages from this chat box?")); + + if (result === true) { + this.$content.empty(); + this.model.messages.reset(); + + this.model.messages.browserStorage._clear(); + } + + return this; + }, + insertIntoTextArea: function insertIntoTextArea(value) { + var $textbox = this.$el.find('textarea.chat-textarea'); + var existing = $textbox.val(); + + if (existing && existing[existing.length - 1] !== ' ') { + existing = existing + ' '; + } + + $textbox.focus().val(existing + value + ' '); + }, + insertEmoji: function insertEmoji(ev) { + ev.stopPropagation(); + var target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target; + this.insertIntoTextArea(target.getAttribute('data-emoji')); + }, + toggleEmojiMenu: function toggleEmojiMenu(ev) { + if (!_.isUndefined(ev)) { + ev.stopPropagation(); + + if (ev.target.classList.contains('emoji-category-picker') || ev.target.classList.contains('emoji-skintone-picker') || ev.target.classList.contains('emoji-category')) { + return; + } + } + + var elements = _.difference(document.querySelectorAll('.toolbar-menu'), [this.emoji_picker_view.el]); + + utils.slideInAllElements(elements).then(_.partial(utils.slideToggleElement, this.emoji_picker_view.el)).then(this.focus.bind(this)); + }, + toggleCall: function toggleCall(ev) { + ev.stopPropagation(); + + _converse.emit('callButtonClicked', { + connection: _converse.connection, + model: this.model + }); + }, + onChatStatusChanged: function onChatStatusChanged(item) { + var chat_status = item.get('chat_status'); + var fullname = item.get('fullname'); + fullname = _.isEmpty(fullname) ? item.get('jid') : fullname; + + if (this.$el.is(':visible')) { + if (chat_status === 'offline') { + this.showStatusNotification(fullname + ' ' + __('has gone offline')); + } else if (chat_status === 'away') { + this.showStatusNotification(fullname + ' ' + __('has gone away')); + } else if (chat_status === 'dnd') { + this.showStatusNotification(fullname + ' ' + __('is busy')); + } else if (chat_status === 'online') { + this.$el.find('div.chat-event').remove(); + } + } + }, + close: function close(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + if (Backbone.history.getFragment() === "converse/chat?jid=" + this.model.get('jid')) { + _converse.router.navigate(''); + } + + if (_converse.connection.connected) { + // Immediately sending the chat state, because the + // model is going to be destroyed afterwards. + this.model.set('chat_state', _converse.INACTIVE); + this.sendChatState(); + } + + try { + this.model.destroy(); + } catch (e) { + _converse.log(e, Strophe.LogLevel.ERROR); + } + + this.remove(); + + _converse.emit('chatBoxClosed', this); + + return this; + }, + getToolbarOptions: function getToolbarOptions(options) { + return _.extend(options || {}, { + 'label_clear': __('Clear all messages'), + 'label_insert_smiley': __('Insert a smiley'), + 'label_start_call': __('Start a call'), + 'show_call_button': _converse.visible_toolbar_buttons.call, + 'show_clear_button': _converse.visible_toolbar_buttons.clear, + 'use_emoji': _converse.visible_toolbar_buttons.emoji + }); + }, + renderToolbar: function renderToolbar(toolbar, options) { + if (!_converse.show_toolbar) { + return; + } + + toolbar = toolbar || tpl_toolbar; + options = _.assign(this.model.toJSON(), this.getToolbarOptions(options || {})); + this.el.querySelector('.chat-toolbar').innerHTML = toolbar(options); + var toggle = this.el.querySelector('.toggle-smiley'); + toggle.innerHTML = ''; + toggle.appendChild(this.emoji_picker_view.render().el); + return this; + }, + focus: function focus() { + this.el.querySelector('.chat-textarea').focus(); + + _converse.emit('chatBoxFocused', this); + + return this; + }, + hide: function hide() { + this.el.classList.add('hidden'); + utils.refreshWebkit(); + return this; + }, + afterShown: function afterShown(focus) { + if (utils.isPersistableModel(this.model)) { + this.model.save(); + } + + this.setChatState(_converse.ACTIVE); + this.scrollDown(); + + if (focus) { + this.focus(); + } + }, + _show: function _show(focus) { + /* Inner show method that gets debounced */ + if (this.$el.is(':visible') && this.$el.css('opacity') === "1") { + if (focus) { + this.focus(); + } + + return; + } + + utils.fadeIn(this.el, _.bind(this.afterShown, this, focus)); + }, + show: function show(focus) { + if (_.isUndefined(this.debouncedShow)) { + /* We wrap the method in a debouncer and set it on the + * instance, so that we have it debounced per instance. + * Debouncing it on the class-level is too broad. + */ + this.debouncedShow = _.debounce(this._show, 250, { + 'leading': true + }); + } + + this.debouncedShow.apply(this, arguments); + return this; + }, + hideNewMessagesIndicator: function hideNewMessagesIndicator() { + var new_msgs_indicator = this.el.querySelector('.new-msgs-indicator'); + + if (!_.isNull(new_msgs_indicator)) { + new_msgs_indicator.classList.add('hidden'); + } + }, + markScrolled: function markScrolled(ev) { + /* Called when the chat content is scrolled up or down. + * We want to record when the user has scrolled away from + * the bottom, so that we don't automatically scroll away + * from what the user is reading when new messages are + * received. + */ + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + if (this.model.get('auto_scrolled')) { + this.model.set({ + 'scrolled': false, + 'auto_scrolled': false + }); + return; + } + + var scrolled = true; + var is_at_bottom = this.$content.scrollTop() + this.$content.innerHeight() >= this.$content[0].scrollHeight - 10; + + if (is_at_bottom) { + scrolled = false; + this.onScrolledDown(); + } + + utils.safeSave(this.model, { + 'scrolled': scrolled + }); + }, + viewUnreadMessages: function viewUnreadMessages() { + this.model.save('scrolled', false); + this.scrollDown(); + }, + _scrollDown: function _scrollDown() { + /* Inner method that gets debounced */ + if (this.$content.is(':visible') && !this.model.get('scrolled')) { + this.$content.scrollTop(this.$content[0].scrollHeight); + this.onScrolledDown(); + this.model.save({ + 'auto_scrolled': true + }); + } + }, + onScrolledDown: function onScrolledDown() { + this.hideNewMessagesIndicator(); + + if (_converse.windowState !== 'hidden') { + this.model.clearUnreadMsgCounter(); + } + + _converse.emit('chatBoxScrolledDown', { + 'chatbox': this.model + }); + }, + scrollDown: function scrollDown() { + if (_.isUndefined(this.debouncedScrollDown)) { + /* We wrap the method in a debouncer and set it on the + * instance, so that we have it debounced per instance. + * Debouncing it on the class-level is too broad. + */ + this.debouncedScrollDown = _.debounce(this._scrollDown, 250); + } + + this.debouncedScrollDown.apply(this, arguments); + return this; + }, + onWindowStateChanged: function onWindowStateChanged(state) { + if (this.model.get('num_unread', 0) && !this.model.newMessageWillBeHidden()) { + this.model.clearUnreadMsgCounter(); + } + } + }); + } + }); + return converse; +}); +//# sourceMappingURL=converse-chatview.js.map; +define('lodash.fp',['lodash', 'lodash.converter', 'converse-core'], function (_, lodashConverter, converse) { + var fp = lodashConverter(_.runInContext()); + converse.env.fp = fp; + return fp; +}); + + +define('tpl!add_contact_dropdown', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape; +__p += '\n'; return __p };}); +define('tpl!add_contact_form', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +__p += '
      \n '; + if (o.error_message) { ; +__p += '\n ' + +__e(o.error_message) + +'\n '; + } ; +__p += '\n \n \n
      \n'; +return __p +};}); + +define('tpl!converse_brand_heading', ['lodash'], function(_) {return function(o) { +var __t, __p = ''; +__p += '\n
      \n \n converse\n
      \n
      \n'; +return __p +};}); + + +define('tpl!contacts_panel', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +__p += '
      \n \n
      \n'; +return __p +};}); + + +define('tpl!contacts_tab', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +__p += '\n ' + +__e(o.label_contacts) + +'\n '; + if (o.num_unread) { ; +__p += '\n ' + +__e( o.num_unread ) + +'\n '; + } ; +__p += '\n\n'; +return __p +};}); + + +define('tpl!controlbox', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +__p += '
      \n
      \n
        \n '; + if (!o.sticky_controlbox) { ; +__p += '\n \n '; + } ; +__p += '\n
        \n
        \n
        \n'; +return __p +};}); + + +define('tpl!controlbox_toggle', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape; +__p += '' + +__e(o.label_toggle) + +'\n'; +return __p +};}); + + +define('tpl!login_panel', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +__p += '
        \n
        \n ' + +__e(o.__("Login")) + +'\n \n '; + if (o.auto_login || o._converse.CONNECTION_STATUS[o.connection_status] === 'CONNECTING') { ; +__p += '\n \n '; + } else { ; +__p += '\n '; + if (o.authentication == o.LOGIN || o.authentication == o.EXTERNAL) { ; +__p += '\n \n \n '; + if (o.authentication !== o.EXTERNAL) { ; +__p += '\n \n \n '; + } ; +__p += '\n \n '; + } ; +__p += '\n '; + if (o.authentication == o.ANONYMOUS) { ; +__p += '\n \n '; + } ; +__p += '\n '; + if (o.authentication == o.PREBIND) { ; +__p += '\n

        Disconnected.

        \n '; + } ; +__p += '\n '; + } ; +__p += '\n \n
        \n'; +return __p +};}); + + +define('tpl!search_contact', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape; +__p += '
      • \n
        \n \n \n
        \n
      • \n'; +return __p +};}); + + +define('tpl!group_header', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape; +__p += '' + +__e(o.label_group) + +'\n'; +return __p +};}); + + +define('tpl!pending_contact', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } + + if (o.allow_chat_pending_contacts) { ; +__p += '\n\n'; + } ; +__p += '\n' + +__e(o.fullname) + +' \n'; + if (o.allow_chat_pending_contacts) { ; +__p += '\n\n'; + } ; +__p += '\n\n'; +return __p +};}); + + +define('tpl!requesting_contact', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } + + if (o.allow_chat_pending_contacts) { ; +__p += '\n\n'; + } ; +__p += '\n' + +__e(o.fullname) + +'\n'; + if (o.allow_chat_pending_contacts) { ; +__p += '\n\n'; + } ; +__p += '\n\n \n \n\n'; +return __p +};}); + + +define('tpl!roster', ['lodash'], function(_) {return function(o) { +var __t, __p = ''; +__p += '
        \n'; +return __p +};}); + + +define('tpl!roster_filter', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +__p += '\n \n \n\n\n'; +return __p +};}); + + +define('tpl!roster_item', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +__p += '\n
        \n \n
        \n '; + if (o.num_unread) { ; +__p += '\n ' + +__e( o.num_unread ) + +'\n '; + } ; +__p += '\n ' + +__e(o.fullname) + +'\n
        \n'; + if (o.allow_contact_removal) { ; +__p += '\n\n'; + } ; +__p += '\n\n\n'; +return __p +};}); + +// 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) +// + +/*global define */ +(function (root, factory) { + define('converse-rosterview',["jquery.noconflict", "converse-core", "tpl!group_header", "tpl!pending_contact", "tpl!requesting_contact", "tpl!roster", "tpl!roster_filter", "tpl!roster_item", "converse-chatboxes"], factory); +})(this, function ($, converse, tpl_group_header, tpl_pending_contact, tpl_requesting_contact, tpl_roster, tpl_roster_filter, tpl_roster_item) { + "use strict"; + + var _converse$env = converse.env, + Backbone = _converse$env.Backbone, + utils = _converse$env.utils, + Strophe = _converse$env.Strophe, + $iq = _converse$env.$iq, + b64_sha1 = _converse$env.b64_sha1, + sizzle = _converse$env.sizzle, + _ = _converse$env._; + converse.plugins.add('converse-rosterview', { + overrides: { + // Overrides mentioned here will be picked up by converse.js's + // plugin architecture they will replace existing methods on the + // relevant objects or classes. + // + // New functions which don't exist yet can also be added. + afterReconnected: function afterReconnected() { + this.__super__.afterReconnected.apply(this, arguments); + }, + _tearDown: function _tearDown() { + /* Remove the rosterview when tearing down. It gets created + * anew when reconnecting or logging in. + */ + this.__super__._tearDown.apply(this, arguments); + + if (!_.isUndefined(this.rosterview)) { + this.rosterview.remove(); + } + }, + RosterGroups: { + comparator: function comparator() { + // RosterGroupsComparator only gets set later (once i18n is + // set up), so we need to wrap it in this nameless function. + var _converse = this.__super__._converse; + return _converse.RosterGroupsComparator.apply(this, arguments); + } + } + }, + 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, + __ = _converse.__; + + _converse.api.settings.update({ + allow_chat_pending_contacts: true, + allow_contact_removal: true, + show_toolbar: true + }); + + _converse.api.promises.add('rosterViewInitialized'); + + var STATUSES = { + 'dnd': __('This contact is busy'), + 'online': __('This contact is online'), + 'offline': __('This contact is offline'), + 'unavailable': __('This contact is unavailable'), + 'xa': __('This contact is away for an extended period'), + 'away': __('This contact is away') + }; + + var LABEL_CONTACTS = __('Contacts'); + + var LABEL_GROUPS = __('Groups'); + + var HEADER_CURRENT_CONTACTS = __('My contacts'); + + var HEADER_PENDING_CONTACTS = __('Pending contacts'); + + var HEADER_REQUESTING_CONTACTS = __('Contact requests'); + + var HEADER_UNGROUPED = __('Ungrouped'); + + var HEADER_WEIGHTS = {}; + HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 0; + HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 1; + HEADER_WEIGHTS[HEADER_UNGROUPED] = 2; + HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3; + + _converse.RosterGroupsComparator = function (a, b) { + /* Groups are sorted alphabetically, ignoring case. + * However, Ungrouped, Requesting Contacts and Pending Contacts + * appear last and in that order. + */ + a = a.get('name'); + b = b.get('name'); + + var special_groups = _.keys(HEADER_WEIGHTS); + + var a_is_special = _.includes(special_groups, a); + + var b_is_special = _.includes(special_groups, b); + + if (!a_is_special && !b_is_special) { + return a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0; + } else if (a_is_special && b_is_special) { + return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0; + } else if (!a_is_special && b_is_special) { + return b === HEADER_REQUESTING_CONTACTS ? 1 : -1; + } else if (a_is_special && !b_is_special) { + return a === HEADER_REQUESTING_CONTACTS ? -1 : 1; + } + }; + + _converse.RosterFilter = Backbone.Model.extend({ + initialize: function initialize() { + this.set({ + 'filter_text': '', + 'filter_type': 'contacts', + 'chat_state': '' + }); + } + }); + _converse.RosterFilterView = Backbone.VDOMView.extend({ + tagName: 'span', + events: { + "keydown .roster-filter": "liveFilter", + "submit form.roster-filter-form": "submitFilter", + "click .onX": "clearFilter", + "mousemove .x": "toggleX", + "change .filter-type": "changeTypeFilter", + "change .state-type": "changeChatStateFilter" + }, + initialize: function initialize() { + this.model.on('change:filter_type', this.render, this); + this.model.on('change:filter_text', this.renderClearButton, this); + }, + renderHTML: function renderHTML() { + return tpl_roster_filter(_.extend(this.model.toJSON(), { + visible: this.shouldBeVisible(), + placeholder: __('Filter'), + label_contacts: LABEL_CONTACTS, + label_groups: LABEL_GROUPS, + label_state: __('State'), + label_any: __('Any'), + label_unread_messages: __('Unread'), + label_online: __('Online'), + label_chatty: __('Chatty'), + label_busy: __('Busy'), + label_away: __('Away'), + label_xa: __('Extended Away'), + label_offline: __('Offline') + })); + }, + afterRender: function afterRender() { + this.renderClearButton(); + }, + renderClearButton: function renderClearButton() { + var roster_filter = this.el.querySelector('.roster-filter'); + + if (_.isNull(roster_filter)) { + return; + } + + roster_filter.classList[this.tog(roster_filter.value)]('x'); + }, + tog: function tog(v) { + return v ? 'add' : 'remove'; + }, + toggleX: function toggleX(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + var el = ev.target; + el.classList[this.tog(el.offsetWidth - 18 < ev.clientX - el.getBoundingClientRect().left)]('onX'); + }, + changeChatStateFilter: function changeChatStateFilter(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + this.model.save({ + 'chat_state': this.el.querySelector('.state-type').value + }); + }, + changeTypeFilter: function changeTypeFilter(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + var type = ev.target.value; + + if (type === 'state') { + this.model.save({ + 'filter_type': type, + 'chat_state': this.el.querySelector('.state-type').value + }); + } else { + this.model.save({ + 'filter_type': type, + 'filter_text': this.el.querySelector('.roster-filter').value + }); + } + }, + liveFilter: _.debounce(function (ev) { + this.model.save({ + 'filter_type': this.el.querySelector('.filter-type').value, + 'filter_text': this.el.querySelector('.roster-filter').value + }); + }, 250), + submitFilter: function submitFilter(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + this.liveFilter(); + this.render(); + }, + isActive: function isActive() { + /* Returns true if the filter is enabled (i.e. if the user + * has added values to the filter). + */ + if (this.model.get('filter_type') === 'state' || this.model.get('filter_text')) { + return true; + } + + return false; + }, + shouldBeVisible: function shouldBeVisible() { + return _converse.roster.length >= 7 || this.isActive(); + }, + showOrHide: function showOrHide() { + if (this.shouldBeVisible) { + this.show(); + } else { + this.hide(); + } + }, + show: function show() { + if (utils.isVisible(this.el)) { + return this; + } + + this.el.classList.add('fade-in'); + this.el.classList.remove('hidden'); + return this; + }, + hide: function hide() { + if (!utils.isVisible(this.el)) { + return this; + } + + this.model.save({ + 'filter_text': '', + 'chat_state': '' + }); + this.el.classList.add('hidden'); + return this; + }, + clearFilter: function clearFilter(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + ev.target.classList.remove('x'); + ev.target.classList.remove('onX'); + ev.target.value = ''; + } + + this.model.save({ + 'filter_text': '' + }); + } + }); + _converse.RosterView = Backbone.Overview.extend({ + tagName: 'div', + id: 'converse-roster', + initialize: function initialize() { + _converse.roster.on("add", this.onContactAdd, this); + + _converse.roster.on('change', this.onContactChange, this); + + _converse.roster.on("destroy", this.update, this); + + _converse.roster.on("remove", this.update, this); + + this.model.on("add", this.onGroupAdd, this); + this.model.on("reset", this.reset, this); + + _converse.on('rosterGroupsFetched', this.positionFetchedGroups, this); + + _converse.on('rosterContactsFetched', this.update, this); + + this.createRosterFilter(); + }, + render: function render() { + this.renderRoster(); + this.el.innerHTML = ""; + this.el.appendChild(this.filter_view.render().el); + + if (!_converse.allow_contact_requests) { + // XXX: if we ever support live editing of config then + // we'll need to be able to remove this class on the fly. + this.el.classList.add('no-contact-requests'); + } + + return this; + }, + renderRoster: function renderRoster() { + this.$roster = $(tpl_roster()); + this.roster = this.$roster[0]; + }, + createRosterFilter: function createRosterFilter() { + // Create a model on which we can store filter properties + var model = new _converse.RosterFilter(); + model.id = b64_sha1("_converse.rosterfilter".concat(_converse.bare_jid)); + model.browserStorage = new Backbone.BrowserStorage.local(this.filter.id); + this.filter_view = new _converse.RosterFilterView({ + 'model': model + }); + this.filter_view.model.on('change', this.updateFilter, this); + this.filter_view.model.fetch(); + }, + updateFilter: _.debounce(function () { + /* Filter the roster again. + * Called whenever the filter settings have been changed or + * when contacts have been added, removed or changed. + * + * Debounced so that it doesn't get called for every + * contact fetched from browser storage. + */ + var type = this.filter_view.model.get('filter_type'); + + if (type === 'state') { + this.filter(this.filter_view.model.get('chat_state'), type); + } else { + this.filter(this.filter_view.model.get('filter_text'), type); + } + }, 100), + update: _.debounce(function () { + if (_.isNull(this.roster.parentElement)) { + this.$el.append(this.$roster.show()); + } + + return this.showHideFilter(); + }, _converse.animate ? 100 : 0), + showHideFilter: function showHideFilter() { + if (!utils.isVisible(this.el)) { + return; + } + + this.filter_view.showOrHide(); + return this; + }, + filter: function filter(query, type) { + // First we make sure the filter is restored to its + // original state + _.each(this.getAll(), function (view) { + if (view.model.contacts.length > 0) { + view.show().filter(''); + } + }); // Now we can filter + + + query = query.toLowerCase(); + + if (type === 'groups') { + _.each(this.getAll(), function (view, idx) { + if (!_.includes(view.model.get('name').toLowerCase(), query.toLowerCase())) { + view.hide(); + } else if (view.model.contacts.length > 0) { + view.show(); + } + }); + } else { + _.each(this.getAll(), function (view) { + view.filter(query, type); + }); + } + }, + reset: function reset() { + _converse.roster.reset(); + + this.removeAll(); + this.renderRoster(); + this.render().update(); + return this; + }, + onGroupAdd: function onGroupAdd(group) { + var view = new _converse.RosterGroupView({ + model: group + }); + this.add(group.get('name'), view.render()); + this.positionGroup(view); + }, + onContactAdd: function onContactAdd(contact) { + this.addRosterContact(contact).update(); + this.updateFilter(); + }, + onContactChange: function onContactChange(contact) { + this.updateChatBox(contact).update(); + + if (_.has(contact.changed, 'subscription')) { + if (contact.changed.subscription === 'from') { + this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); + } else if (_.includes(['both', 'to'], contact.get('subscription'))) { + this.addExistingContact(contact); + } + } + + if (_.has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') { + this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); + } + + if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') { + this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS); + } + + this.updateFilter(); + }, + updateChatBox: function updateChatBox(contact) { + var chatbox = _converse.chatboxes.get(contact.get('jid')), + changes = {}; + + if (!chatbox) { + return this; + } + + if (_.has(contact.changed, 'chat_status')) { + changes.chat_status = contact.get('chat_status'); + } + + if (_.has(contact.changed, 'status')) { + changes.status = contact.get('status'); + } + + chatbox.save(changes); + return this; + }, + positionFetchedGroups: function positionFetchedGroups() { + /* Instead of throwing an add event for each group + * fetched, we wait until they're all fetched and then + * we position them. + * Works around the problem of positionGroup not + * working when all groups besides the one being + * positioned aren't already in inserted into the + * roster DOM element. + */ + var that = this; + this.model.sort(); + this.model.each(function (group, idx) { + var view = that.get(group.get('name')); + + if (!view) { + view = new _converse.RosterGroupView({ + model: group + }); + that.add(group.get('name'), view.render()); + } + + if (idx === 0) { + that.$roster.append(view.$el); + } else { + that.appendGroup(view); + } + }); + }, + positionGroup: function positionGroup(view) { + /* Place the group's DOM element in the correct alphabetical + * position amongst the other groups in the roster. + */ + var $groups = this.$roster.find('.roster-group'), + index = $groups.length ? this.model.indexOf(view.model) : 0; + + if (index === 0) { + this.$roster.prepend(view.$el); + } else if (index === this.model.length - 1) { + this.appendGroup(view); + } else { + $($groups.eq(index)).before(view.$el); + } + + return this; + }, + appendGroup: function appendGroup(view) { + /* Add the group at the bottom of the roster + */ + var $last = this.$roster.find('.roster-group').last(); + var $siblings = $last.siblings('dd'); + + if ($siblings.length > 0) { + $siblings.last().after(view.$el); + } else { + $last.after(view.$el); + } + + return this; + }, + getGroup: function getGroup(name) { + /* Returns the group as specified by name. + * Creates the group if it doesn't exist. + */ + var view = this.get(name); + + if (view) { + return view.model; + } + + return this.model.create({ + name: name, + id: b64_sha1(name) + }); + }, + addContactToGroup: function addContactToGroup(contact, name) { + this.getGroup(name).contacts.add(contact); + }, + addExistingContact: function addExistingContact(contact) { + var groups; + + if (_converse.roster_groups) { + groups = contact.get('groups'); + + if (groups.length === 0) { + groups = [HEADER_UNGROUPED]; + } + } else { + groups = [HEADER_CURRENT_CONTACTS]; + } + + _.each(groups, _.bind(this.addContactToGroup, this, contact)); + }, + addRosterContact: function addRosterContact(contact) { + if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') { + this.addExistingContact(contact); + } else { + if (contact.get('ask') === 'subscribe' || contact.get('subscription') === 'from') { + this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); + } else if (contact.get('requesting') === true) { + this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS); + } + } + + return this; + } + }); + _converse.RosterContactView = Backbone.View.extend({ + tagName: 'dd', + events: { + "click .accept-xmpp-request": "acceptRequest", + "click .decline-xmpp-request": "declineRequest", + "click .open-chat": "openChat", + "click .remove-xmpp-contact": "removeContact" + }, + initialize: function initialize() { + this.model.on("change", this.render, this); + this.model.on("remove", this.remove, this); + this.model.on("destroy", this.remove, this); + this.model.on("open", this.openChat, this); + }, + render: function render() { + var that = this; + + if (!this.mayBeShown()) { + this.$el.hide(); + return this; + } + + var item = this.model, + ask = item.get('ask'), + chat_status = item.get('chat_status'), + requesting = item.get('requesting'), + subscription = item.get('subscription'); + var classes_to_remove = ['current-xmpp-contact', 'pending-xmpp-contact', 'requesting-xmpp-contact'].concat(_.keys(STATUSES)); + + _.each(classes_to_remove, function (cls) { + if (_.includes(that.el.className, cls)) { + that.el.classList.remove(cls); + } + }); + + this.$el.addClass(chat_status).data('status', chat_status); + + if (ask === 'subscribe' || subscription === 'from') { + /* ask === 'subscribe' + * Means we have asked to subscribe to them. + * + * subscription === 'from' + * They are subscribed to use, but not vice versa. + * We assume that there is a pending subscription + * from us to them (otherwise we're in a state not + * supported by converse.js). + * + * So in both cases the user is a "pending" contact. + */ + this.el.classList.add('pending-xmpp-contact'); + this.$el.html(tpl_pending_contact(_.extend(item.toJSON(), { + 'desc_remove': __('Click to remove %1$s as a contact', item.get('fullname')), + 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts + }))); + } else if (requesting === true) { + this.el.classList.add('requesting-xmpp-contact'); + this.$el.html(tpl_requesting_contact(_.extend(item.toJSON(), { + 'desc_accept': __("Click to accept the contact request from %1$s", item.get('fullname')), + 'desc_decline': __("Click to decline the contact request from %1$s", item.get('fullname')), + 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts + }))); + } else if (subscription === 'both' || subscription === 'to') { + this.el.classList.add('current-xmpp-contact'); + this.el.classList.remove(_.without(['both', 'to'], subscription)[0]); + this.el.classList.add(subscription); + this.renderRosterItem(item); + } + + return this; + }, + renderRosterItem: function renderRosterItem(item) { + var chat_status = item.get('chat_status'); + this.$el.html(tpl_roster_item(_.extend(item.toJSON(), { + 'desc_status': STATUSES[chat_status || 'offline'], + 'desc_chat': __('Click to chat with this contact'), + 'desc_remove': __('Click to remove %1$s as a contact', item.get('fullname')), + 'title_fullname': __('Name'), + 'allow_contact_removal': _converse.allow_contact_removal, + 'num_unread': item.get('num_unread') || 0 + }))); + return this; + }, + isGroupCollapsed: function isGroupCollapsed() { + /* Check whether the group in which this contact appears is + * collapsed. + */ + // XXX: this sucks and is fragile. + // It's because I tried to do the "right thing" + // and use definition lists to represent roster groups. + // If roster group items were inside the group elements, we + // would simplify things by not having to check whether the + // group is collapsed or not. + var name = this.$el.prevAll('dt:first').data('group'); + + var group = _.head(_converse.rosterview.model.where({ + 'name': name.toString() + })); + + if (group.get('state') === _converse.CLOSED) { + return true; + } + + return false; + }, + mayBeShown: function mayBeShown() { + /* Return a boolean indicating whether this contact should + * generally be visible in the roster. + * + * It doesn't check for the more specific case of whether + * the group it's in is collapsed (see isGroupCollapsed). + */ + var chatStatus = this.model.get('chat_status'); + + if (_converse.show_only_online_users && chatStatus !== 'online' || _converse.hide_offline_users && chatStatus === 'offline') { + // If pending or requesting, show + if (this.model.get('ask') === 'subscribe' || this.model.get('subscription') === 'from' || this.model.get('requesting') === true) { + return true; + } + + return false; + } + + return true; + }, + openChat: function openChat(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + return _converse.chatboxviews.showChat(this.model.attributes, true); + }, + removeContact: function removeContact(ev) { + var _this = this; + + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + if (!_converse.allow_contact_removal) { + return; + } + + var result = confirm(__("Are you sure you want to remove this contact?")); + + if (result === true) { + var iq = $iq({ + type: 'set' + }).c('query', { + xmlns: Strophe.NS.ROSTER + }).c('item', { + jid: this.model.get('jid'), + subscription: "remove" + }); + + _converse.connection.sendIQ(iq, function (iq) { + _this.model.destroy(); + + _this.remove(); + }, function (err) { + alert(__('Sorry, there was an error while trying to remove %1$s as a contact.', name)); + + _converse.log(err, Strophe.LogLevel.ERROR); + }); + } + }, + acceptRequest: function acceptRequest(ev) { + var _this2 = this; + + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + _converse.roster.sendContactAddIQ(this.model.get('jid'), this.model.get('fullname'), [], function () { + _this2.model.authorize().subscribe(); + }); + }, + declineRequest: function declineRequest(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + var result = confirm(__("Are you sure you want to decline this contact request?")); + + if (result === true) { + this.model.unauthorize().destroy(); + } + + return this; + } + }); + _converse.RosterGroupView = Backbone.Overview.extend({ + tagName: 'dt', + className: 'roster-group', + events: { + "click a.group-toggle": "toggle" + }, + initialize: function initialize() { + this.model.contacts.on("add", this.addContact, this); + this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this); + this.model.contacts.on("change:requesting", this.onContactRequestChange, this); + this.model.contacts.on("change:chat_status", function (contact) { + // This might be optimized by instead of first sorting, + // finding the correct position in positionContact + this.model.contacts.sort(); + this.positionContact(contact).render(); + }, this); + this.model.contacts.on("destroy", this.onRemove, this); + this.model.contacts.on("remove", this.onRemove, this); + + _converse.roster.on('change:groups', this.onContactGroupChange, this); + }, + render: function render() { + this.el.setAttribute('data-group', this.model.get('name')); + var html = tpl_group_header({ + label_group: this.model.get('name'), + desc_group_toggle: this.model.get('description'), + toggle_state: this.model.get('state') + }); + this.el.innerHTML = html; + return this; + }, + addContact: function addContact(contact) { + var view = new _converse.RosterContactView({ + model: contact + }); + this.add(contact.get('id'), view); + view = this.positionContact(contact).render(); + + if (view.mayBeShown()) { + if (this.model.get('state') === _converse.CLOSED) { + if (view.$el[0].style.display !== "none") { + view.$el.hide(); + } + + if (!this.$el.is(':visible')) { + this.$el.show(); + } + } else { + if (this.$el[0].style.display !== "block") { + this.show(); + } + } + } + }, + positionContact: function positionContact(contact) { + /* Place the contact's DOM element in the correct alphabetical + * position amongst the other contacts in this group. + */ + var view = this.get(contact.get('id')); + var index = this.model.contacts.indexOf(contact); + view.$el.detach(); + + if (index === 0) { + this.$el.after(view.$el); + } else if (index === this.model.contacts.length - 1) { + this.$el.nextUntil('dt').last().after(view.$el); + } else { + this.$el.nextUntil('dt').eq(index).before(view.$el); + } + + return view; + }, + show: function show() { + this.$el.show(); + + _.each(this.getAll(), function (view) { + if (view.mayBeShown() && !view.isGroupCollapsed()) { + view.$el.show(); + } + }); + + return this; + }, + hide: function hide() { + this.$el.nextUntil('dt').addBack().hide(); + }, + filter: function filter(q, type) { + var _this3 = this; + + /* Filter the group's contacts based on the query "q". + * The query is matched against the contact's full name. + * If all contacts are filtered out (i.e. hidden), then the + * group must be filtered out as well. + */ + var matches; + + if (q.length === 0) { + if (this.model.get('state') === _converse.OPENED) { + this.model.contacts.each(function (item) { + var view = _this3.get(item.get('id')); + + if (view.mayBeShown() && !view.isGroupCollapsed()) { + view.$el.show(); + } + }); + } + + this.showIfNecessary(); + } else { + q = q.toLowerCase(); + + if (type === 'state') { + if (this.model.get('name') === HEADER_REQUESTING_CONTACTS) { + // When filtering by chat state, we still want to + // show requesting contacts, even though they don't + // have the state in question. + matches = this.model.contacts.filter(function (contact) { + return utils.contains.not('chat_status', q)(contact) && !contact.get('requesting'); + }); + } else if (q === 'unread_messages') { + matches = this.model.contacts.filter({ + 'num_unread': 0 + }); + } else { + matches = this.model.contacts.filter(utils.contains.not('chat_status', q)); + } + } else { + matches = this.model.contacts.filter(utils.contains.not('fullname', q)); + } + + if (matches.length === this.model.contacts.length) { + // hide the whole group + this.hide(); + } else { + _.each(matches, function (item) { + _this3.get(item.get('id')).$el.hide(); + }); + + if (this.model.get('state') === _converse.OPENED) { + _.each(this.model.contacts.reject(utils.contains.not('fullname', q)), function (item) { + _this3.get(item.get('id')).$el.show(); + }); + } + + this.showIfNecessary(); + } + } + }, + showIfNecessary: function showIfNecessary() { + if (!this.$el.is(':visible') && this.model.contacts.length > 0) { + this.$el.show(); + } + }, + toggle: function toggle(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + var $el = $(ev.target); + + if ($el.hasClass("icon-opened")) { + this.$el.nextUntil('dt').slideUp(); + this.model.save({ + state: _converse.CLOSED + }); + $el.removeClass("icon-opened").addClass("icon-closed"); + } else { + $el.removeClass("icon-closed").addClass("icon-opened"); + this.model.save({ + state: _converse.OPENED + }); + this.filter(_converse.rosterview.$('.roster-filter').val() || '', _converse.rosterview.$('.filter-type').val()); + } + }, + onContactGroupChange: function onContactGroupChange(contact) { + var in_this_group = _.includes(contact.get('groups'), this.model.get('name')); + + var cid = contact.get('id'); + var in_this_overview = !this.get(cid); + + if (in_this_group && !in_this_overview) { + this.model.contacts.remove(cid); + } else if (!in_this_group && in_this_overview) { + this.addContact(contact); + } + }, + onContactSubscriptionChange: function onContactSubscriptionChange(contact) { + if (this.model.get('name') === HEADER_PENDING_CONTACTS && contact.get('subscription') !== 'from') { + this.model.contacts.remove(contact.get('id')); + } + }, + onContactRequestChange: function onContactRequestChange(contact) { + if (this.model.get('name') === HEADER_REQUESTING_CONTACTS && !contact.get('requesting')) { + /* We suppress events, otherwise the remove event will + * also cause the contact's view to be removed from the + * "Pending Contacts" group. + */ + this.model.contacts.remove(contact.get('id'), { + 'silent': true + }); // Since we suppress events, we make sure the view and + // contact are removed from this group. + + this.get(contact.get('id')).remove(); + this.onRemove(contact); + } + }, + onRemove: function onRemove(contact) { + this.remove(contact.get('id')); + + if (this.model.contacts.length === 0) { + this.$el.hide(); + } + } + }); + /* -------- Event Handlers ----------- */ + + var onChatBoxMaximized = function onChatBoxMaximized(chatboxview) { + /* When a chat box gets maximized, the num_unread counter needs + * to be cleared, but if chatbox is scrolled up, then num_unread should not be cleared. + */ + var chatbox = chatboxview.model; + + if (chatbox.get('type') !== 'chatroom') { + var contact = _.head(_converse.roster.where({ + 'jid': chatbox.get('jid') + })); + + if (!_.isUndefined(contact) && !chatbox.isScrolledUp()) { + contact.save({ + 'num_unread': 0 + }); + } + } + }; + + var onMessageReceived = function onMessageReceived(data) { + /* Given a newly received message, update the unread counter on + * the relevant roster contact. + */ + var chatbox = data.chatbox; + + if (_.isUndefined(chatbox)) { + return; + } + + if (_.isNull(data.stanza.querySelector('body'))) { + return; // The message has no text + } + + if (chatbox.get('type') !== 'chatroom' && utils.isNewMessage(data.stanza) && chatbox.newMessageWillBeHidden()) { + var contact = _.head(_converse.roster.where({ + 'jid': chatbox.get('jid') + })); + + if (!_.isUndefined(contact)) { + contact.save({ + 'num_unread': contact.get('num_unread') + 1 + }); + } + } + }; + + var onChatBoxScrolledDown = function onChatBoxScrolledDown(data) { + var chatbox = data.chatbox; + + if (_.isUndefined(chatbox)) { + return; + } + + var contact = _.head(_converse.roster.where({ + 'jid': chatbox.get('jid') + })); + + if (!_.isUndefined(contact)) { + contact.save({ + 'num_unread': 0 + }); + } + }; + + var initRoster = function initRoster() { + /* Create an instance of RosterView once the RosterGroups + * collection has been created (in converse-core.js) + */ + _converse.rosterview = new _converse.RosterView({ + 'model': _converse.rostergroups + }); + + _converse.rosterview.render(); + + _converse.emit('rosterViewInitialized'); + }; + + _converse.api.listen.on('rosterInitialized', initRoster); + + _converse.api.listen.on('rosterReadyAfterReconnection', initRoster); + + _converse.api.listen.on('message', onMessageReceived); + + _converse.api.listen.on('chatBoxMaximized', onChatBoxMaximized); + + _converse.api.listen.on('chatBoxScrolledDown', onChatBoxScrolledDown); + } + }); +}); +//# sourceMappingURL=converse-rosterview.js.map; + +define('tpl!change_status_message', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape; +__p += '
        \n \n \n \n \n
        \n'; +return __p +};}); + + +define('tpl!chat_status', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape; +__p += '\n'; +return __p +};}); + + +define('tpl!choose_status', ['lodash'], function(_) {return function(o) { +var __t, __p = ''; +__p += '\n'; +return __p +};}); + + +define('tpl!status_option', ['lodash'], function(_) {return function(o) { +var __t, __p = '', __e = _.escape; +__p += '
      • \n \n \n ' + +__e( o.text ) + +'\n \n
      • \n'; +return __p +};}); + +// 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) +// + +/*global define */ +(function (root, factory) { + define('converse-vcard',["converse-core", "strophe.vcard"], factory); +})(this, function (converse) { + "use strict"; + + var _converse$env = converse.env, + Promise = _converse$env.Promise, + Strophe = _converse$env.Strophe, + _ = _converse$env._, + moment = _converse$env.moment, + sizzle = _converse$env.sizzle; + + function onVCardData(_converse, jid, iq, callback) { + var vcard = iq.querySelector('vCard'), + img_type = _.get(vcard.querySelector('TYPE'), 'textContent'), + img = _.get(vcard.querySelector('BINVAL'), 'textContent'), + url = _.get(vcard.querySelector('URL'), 'textContent'), + fullname = _.get(vcard.querySelector('FN'), 'textContent'); + + if (jid) { + var contact = _converse.roster.get(jid); + + if (contact) { + contact.save({ + 'fullname': fullname || _.get(contact, 'fullname', jid), + 'image_type': img_type, + 'image': img, + 'url': url, + 'vcard_updated': moment().format() + }); + } + } + + if (callback) { + callback({ + 'stanza': iq, + 'jid': jid, + 'fullname': fullname || jid, + 'image': img, + 'image_type': img_type, + 'url': url + }); + } + } + + function onVCardError(_converse, jid, iq, errback) { + var contact = _converse.roster.get(jid); + + if (contact) { + contact.save({ + 'vcard_updated': moment().format() + }); + } + + if (errback) { + errback({ + 'stanza': iq, + 'jid': jid + }); + } + } + + function getVCard(_converse, jid) { + /* Request the VCard of another user. Returns a promise. + * + * Parameters: + * (String) jid - The Jabber ID of the user whose VCard + * is being requested. + */ + if (Strophe.getBareJidFromJid(jid) === _converse.bare_jid) { + jid = null; // No 'to' attr when getting one's own vCard + } + + return new Promise(function (resolve, reject) { + if (!_converse.use_vcards) { + if (resolve) { + resolve({ + 'jid': jid + }); + } + } else { + _converse.connection.vcard.get(_.partial(onVCardData, _converse, jid, _, resolve), jid, _.partial(onVCardError, _converse, jid, _, resolve)); + } + }); + } + + function updateChatBoxFromVCard(_converse, jid) { + _converse.api.vcard.get(jid).then(function (vcard) { + var chatbox = _converse.chatboxes.getChatBox(vcard.jid); + + if (!_.isUndefined(chatbox)) { + chatbox.save(_.pick(vcard, ['fullname', 'url', 'image_type', 'image', 'vcard_updated'])); + } + }).catch(function () { + _converse.log("updateChatBoxFromVCard: Error occured while attempting to update chatbox with VCard data", Strophe.LogLevel.ERROR); + }); + } + + converse.plugins.add('converse-vcard', { + overrides: { + // Overrides mentioned here will be picked up by converse.js's + // plugin architecture they will replace existing methods on the + // relevant objects or classes. + // + // New functions which don't exist yet can also be added. + RosterContacts: { + createRequestingContact: function createRequestingContact(presence) { + var _converse = this.__super__._converse; + var bare_jid = Strophe.getBareJidFromJid(presence.getAttribute('from')); + + _converse.api.vcard.get(bare_jid).then(_.partial(_converse.createRequestingContactFromVCard, presence)).catch(function (vcard) { + _converse.log("Error while retrieving vcard for ".concat(vcard.jid), Strophe.LogLevel.WARN); + + _converse.createRequestingContactFromVCard(presence, vcard.stanza, vcard.jid); + }); + } + } + }, + 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; + + _converse.api.settings.update({ + 'use_vcards': true + }); + + _converse.createRequestingContactFromVCard = function (presence, vcard) { + var bare_jid = Strophe.getBareJidFromJid(vcard.jid); + var fullname = vcard.fullname; + + if (!fullname) { + var nick_el = sizzle("nick[xmlns=\"".concat(Strophe.NS.NICK, "\"]"), presence); + fullname = nick_el.length ? nick_el[0].textContent : bare_jid; + } + + var user_data = { + 'jid': bare_jid, + 'subscription': 'none', + 'ask': null, + 'requesting': true, + 'fullname': fullname, + 'image': vcard.image, + 'image_type': vcard.image_type, + 'url': vcard.url, + 'vcard_updated': moment().format() + }; + + _converse.roster.create(user_data); + + _converse.emit('contactRequest', user_data); + }; + /* Event handlers */ + + + _converse.on('addClientFeatures', function () { + if (_converse.use_vcards) { + _converse.connection.disco.addFeature(Strophe.NS.VCARD); + } + }); + + _converse.on('chatBoxInitialized', function (chatbox) { + if (!_converse.use_vcards || chatbox.model.get('type') === 'headline') { + return; + } + + _converse.api.waitUntil('rosterInitialized').then(function () { + var jid = chatbox.model.get('jid'), + contact = _converse.roster.get(jid); + + if (contact && !contact.get('vcard_updated') || _.isUndefined(contact) && _converse.allow_non_roster_messaging) { + updateChatBoxFromVCard(_converse, jid); + } + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }); + + _converse.on('initialized', function () { + _converse.roster.on("add", function (contact) { + if (!contact.get('vcard_updated')) { + _converse.api.vcard.get(contact.get('jid')); + } + }); + }); + + _converse.on('statusInitialized', function fetchOwnVCard() { + if (_.isNil(_converse.xmppstatus.get('fullname'))) { + _converse.api.disco.supports(Strophe.NS.VCARD, _converse.domain).then(function (result) { + if (result.supported) { + _converse.api.vcard.get(_converse.bare_jid).then(function (vcard) { + _converse.xmppstatus.save({ + 'fullname': vcard.fullname || '' + }); + }); + } + }).catch(function (msg) { + _converse.log(msg, Strophe.LogLevel.FATAL); + }); + } + }); + + _.extend(_converse.api, { + 'vcard': { + 'get': function get(jid) { + return getVCard(_converse, jid); + } + } + }); + } + }); +}); +//# sourceMappingURL=converse-vcard.js.map; // Converse.js (A browser based XMPP chat client) // http://conversejs.org // @@ -12381,913 +15455,2077 @@ return __p /*global define */ (function (root, factory) { - define('converse-chatview',["jquery.noconflict", "converse-core", "converse-chatboxes", "emojione", "xss", "tpl!chatbox", "tpl!new_day", "tpl!action", "tpl!emojis", "tpl!message", "tpl!help_message", "tpl!toolbar", "tpl!avatar", "tpl!spinner"], factory); -})(undefined, function ($, converse, dummy, emojione, xss, tpl_chatbox, tpl_new_day, tpl_action, tpl_emojis, tpl_message, tpl_help_message, tpl_toolbar, tpl_avatar, tpl_spinner) { + define('converse-profile',["converse-core", + "tpl!change_status_message", + "tpl!chat_status", + "tpl!choose_status", + "tpl!status_option", + "converse-vcard" + ], factory); +}(this, function ( + converse, + tpl_change_status_message, + tpl_chat_status, + tpl_choose_status, + tpl_status_option + ) { "use strict"; - var _converse$env = converse.env, - $msg = _converse$env.$msg, - Backbone = _converse$env.Backbone, - Strophe = _converse$env.Strophe, - _ = _converse$env._, - b64_sha1 = _converse$env.b64_sha1, - moment = _converse$env.moment, - utils = _converse$env.utils; + const { Strophe, Backbone, Promise, utils, _, moment } = converse.env; - var KEY = { - ENTER: 13, - FORWARD_SLASH: 47 - }; + converse.plugins.add('converse-profile', { - converse.plugins.add('converse-chatview', { - - overrides: { - // Overrides mentioned here will be picked up by converse.js's - // plugin architecture they will replace existing methods on the - // relevant objects or classes. - // - // New functions which don't exist yet can also be added. - // - registerGlobalEventHandlers: function registerGlobalEventHandlers() { - this.__super__.registerGlobalEventHandlers(); - document.addEventListener('click', function (ev) { - if (_.includes(ev.target.classList, 'toggle-toolbar-menu') || _.includes(ev.target.classList, 'insert-emoji')) { - return; - } - utils.slideInAllElements(document.querySelectorAll('.toolbar-menu')); - }); - }, - - ChatBoxViews: { - onChatBoxAdded: function onChatBoxAdded(item) { - var _converse = this.__super__._converse; - - var view = this.get(item.get('id')); - if (!view) { - view = new _converse.ChatBoxView({ model: item }); - this.add(item.get('id'), view); - return view; - } else { - return this.__super__.onChatBoxAdded.apply(this, arguments); - } - } - } - }, - - initialize: function initialize() { + initialize () { /* The initialize function gets called as soon as the plugin is * loaded by converse.js's plugin machinery. */ - var _converse = this._converse, - __ = _converse.__; + const { _converse } = this, + { __ } = _converse; - - _converse.api.settings.update({ - 'use_emojione': true, - 'emojione_image_path': emojione.imagePathPNG, - 'chatview_avatar_height': 32, - 'chatview_avatar_width': 32, - 'show_toolbar': true, - 'time_format': 'HH:mm', - 'visible_toolbar_buttons': { - 'emoji': true, - 'call': false, - 'clear': true - } - }); - emojione.imagePathPNG = _converse.emojione_image_path; - emojione.ascii = true; - - function onWindowStateChanged(data) { - _converse.chatboxviews.each(function (chatboxview) { - chatboxview.onWindowStateChanged(data.state); - }); - } - _converse.api.listen.on('windowStateChanged', onWindowStateChanged); - - _converse.EmojiPicker = Backbone.Model.extend({ - defaults: { - 'current_category': 'people', - 'current_skintone': '', - 'scroll_position': 0 - }, - initialize: function initialize() { - var id = "converse.emoji-" + _converse.bare_jid; - this.id = id; - this.browserStorage = new Backbone.BrowserStorage[_converse.storage](id); - } - }); - - _converse.EmojiPickerView = Backbone.View.extend({ - className: 'emoji-picker-container toolbar-menu collapsed', + _converse.XMPPStatusView = Backbone.View.extend({ + el: "form#set-xmpp-status", events: { - 'click .emoji-category-picker li.emoji-category': 'chooseCategory', - 'click .emoji-skintone-picker li.emoji-skintone': 'chooseSkinTone' + "click a.choose-xmpp-status": "toggleOptions", + "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm", + "submit": "setStatusMessage", + "click .dropdown dd ul li a": "setStatus" }, - initialize: function initialize() { - this.model.on('change:current_skintone', this.render, this); - this.model.on('change:current_category', this.render, this); - this.setScrollPosition = _.debounce(this.setScrollPosition, 50); - }, - render: function render() { - var _this = this; - - var emojis_html = tpl_emojis(_.extend(this.model.toJSON(), { - 'transform': _converse.use_emojione ? emojione.shortnameToImage : emojione.shortnameToUnicode, - 'emojis_by_category': utils.getEmojisByCategory(_converse, emojione), - 'toned_emojis': utils.getTonedEmojis(_converse), - 'skintones': ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'], - 'shouldBeHidden': this.shouldBeHidden - })); - this.el.innerHTML = emojis_html; - _.forEach(this.el.querySelectorAll('.emoji-picker'), function (el) { - el.addEventListener('scroll', _this.setScrollPosition.bind(_this)); - }); - this.restoreScrollPosition(); - return this; - }, - shouldBeHidden: function shouldBeHidden(shortname, current_skintone, toned_emojis) { - /* Helper method for the template which decides whether an - * emoji should be hidden, based on which skin tone is - * currently being applied. - */ - if (_.includes(shortname, '_tone')) { - if (!current_skintone || !_.includes(shortname, current_skintone)) { - return true; - } - } else { - if (current_skintone && _.includes(toned_emojis, shortname)) { - return true; - } - } - return false; - }, - restoreScrollPosition: function restoreScrollPosition() { - var current_picker = _.difference(this.el.querySelectorAll('.emoji-picker'), this.el.querySelectorAll('.emoji-picker.hidden')); - if (current_picker.length === 1 && this.model.get('scroll_position')) { - current_picker[0].scrollTop = this.model.get('scroll_position'); - } - }, - setScrollPosition: function setScrollPosition(ev) { - this.model.save('scroll_position', ev.target.scrollTop); - }, - chooseSkinTone: function chooseSkinTone(ev) { - ev.preventDefault(); - ev.stopPropagation(); - var target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target; - var skintone = target.getAttribute("data-skintone").trim(); - if (this.model.get('current_skintone') === skintone) { - this.model.save({ 'current_skintone': '' }); - } else { - this.model.save({ 'current_skintone': skintone }); - } - }, - chooseCategory: function chooseCategory(ev) { - ev.preventDefault(); - ev.stopPropagation(); - var target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target; - var category = target.getAttribute("data-category").trim(); - this.model.save({ - 'current_category': category, - 'scroll_position': 0 - }); - } - }); - - _converse.ChatBoxView = Backbone.View.extend({ - length: 200, - tagName: 'div', - className: 'chatbox hidden', - is_chatroom: false, // Leaky abstraction from MUC - - events: { - 'click .close-chatbox-button': 'close', - 'keypress .chat-textarea': 'keyPressed', - 'click .send-button': 'onFormSubmitted', - 'click .toggle-smiley': 'toggleEmojiMenu', - 'click .toggle-smiley ul.emoji-picker li': 'insertEmoji', - 'click .toggle-clear': 'clearMessages', - 'click .toggle-call': 'toggleCall', - 'click .new-msgs-indicator': 'viewUnreadMessages' + initialize () { + this.model.on("change:status", this.updateStatusUI, this); + this.model.on("change:status_message", this.updateStatusUI, this); + this.model.on("update-status-ui", this.updateStatusUI, this); }, - initialize: function initialize() { - this.markScrolled = _.debounce(this.markScrolled, 100); + render () { + // Replace the default dropdown with something nicer + const select = this.el.querySelector('select#select-xmpp-status') + const chat_status = this.model.get('status') || 'offline'; + this.el.innerHTML = tpl_choose_status(); - this.createEmojiPicker(); - this.model.messages.on('add', this.onMessageAdded, this); - this.model.on('show', this.show, this); - this.model.on('destroy', this.hide, this); - // TODO check for changed fullname as well - this.model.on('change:chat_state', this.sendChatState, this); - this.model.on('change:chat_status', this.onChatStatusChanged, this); - this.model.on('change:image', this.renderAvatar, this); - this.model.on('change:status', this.onStatusChanged, this); - this.model.on('showHelpMessages', this.showHelpMessages, this); - this.model.on('sendMessage', this.sendMessage, this); - this.render().fetchMessages(); - _converse.emit('chatBoxInitialized', this); - }, - render: function render() { - this.$el.attr('id', this.model.get('box_id')).html(tpl_chatbox(_.extend(this.model.toJSON(), { - show_toolbar: _converse.show_toolbar, - show_textarea: true, - show_send_button: _converse.show_send_button, - title: this.model.get('fullname'), - unread_msgs: __('You have unread messages'), - info_close: __('Close this chat box'), - label_personal_message: __('Personal message'), - label_send: __('Send') - }))); - this.$content = this.$el.find('.chat-content'); - this.renderToolbar().renderAvatar(); - _converse.emit('chatBoxOpened', this); - utils.refreshWebkit(); - return this.showStatusMessage(); - }, - createEmojiPicker: function createEmojiPicker() { - if (_.isUndefined(_converse.emojipicker)) { - _converse.emojipicker = new _converse.EmojiPicker(); - _converse.emojipicker.fetch(); - } - this.emoji_picker_view = new _converse.EmojiPickerView({ - 'model': _converse.emojipicker - }); - }, - afterMessagesFetched: function afterMessagesFetched() { - this.insertIntoDOM(); - this.scrollDown(); - // We only start listening for the scroll event after - // cached messages have been fetched - this.$content.on('scroll', this.markScrolled.bind(this)); - _converse.emit('afterMessagesFetched', this); - }, - fetchMessages: function fetchMessages() { - this.model.messages.fetch({ - 'add': true, - 'success': this.afterMessagesFetched.bind(this), - 'error': this.afterMessagesFetched.bind(this) - }); - return this; - }, - insertIntoDOM: function insertIntoDOM() { - /* This method gets overridden in src/converse-controlbox.js - * as well as src/converse-muc.js (if those plugins are - * enabled). - */ - var container = document.querySelector('#conversejs'); - if (this.el.parentNode !== container) { - container.insertBefore(this.el, container.firstChild); - } - return this; - }, - clearStatusNotification: function clearStatusNotification() { - this.$content.find('div.chat-event').remove(); - }, - showStatusNotification: function showStatusNotification(message, keep_old, permanent) { - if (!keep_old) { - this.clearStatusNotification(); - } - var $el = $('
        ').text(message); - if (!permanent) { - $el.addClass('chat-event'); - } - this.$content.append($el); - this.scrollDown(); - }, - addSpinner: function addSpinner() { - if (_.isNull(this.el.querySelector('.spinner'))) { - this.$content.prepend(tpl_spinner); - } - }, - clearSpinner: function clearSpinner() { - if (this.$content.children(':first').is('span.spinner')) { - this.$content.children(':first').remove(); - } - }, - insertDayIndicator: function insertDayIndicator(date, prepend) { - /* Appends (or prepends if "prepend" is truthy) an indicator - * into the chat area, showing the day as given by the - * passed in date. - * - * Parameters: - * (String) date - An ISO8601 date string. - */ - var day_date = moment(date).startOf('day'); - var insert = prepend ? this.$content.prepend : this.$content.append; - insert.call(this.$content, tpl_new_day({ - isodate: day_date.format(), - datestring: day_date.format("dddd MMM Do YYYY") - })); - }, - insertMessage: function insertMessage(attrs, prepend) { - var _this2 = this; - - /* Helper method which appends a message (or prepends if the - * 2nd parameter is set to true) to the end of the chat box's - * content area. - * - * Parameters: - * (Object) attrs: An object containing the message attributes. - */ - var insert = prepend ? this.$content.prepend : this.$content.append; - _.flow(function ($el) { - insert.call(_this2.$content, $el); - return $el; - }, this.scrollDown.bind(this))(this.renderMessage(attrs)); - }, - showMessage: function showMessage(attrs) { - /* Inserts a chat message into the content area of the chat box. - * Will also insert a new day indicator if the message is on a - * different day. - * - * The message to show may either be newer than the newest - * message, or older than the oldest message. - * - * Parameters: - * (Object) attrs: An object containing the message - * attributes. - */ - var current_msg_date = moment(attrs.time) || moment; - var $first_msg = this.$content.find('.chat-message:first'), - first_msg_date = $first_msg.data('isodate'); - - if (!first_msg_date) { - // This is the first received message, so we insert a - // date indicator before it. - this.insertDayIndicator(current_msg_date); - this.insertMessage(attrs); - return; - } - - var last_msg_date = this.$content.find('.chat-message:last').data('isodate'); - if (current_msg_date.isAfter(last_msg_date) || current_msg_date.isSame(last_msg_date)) { - // The new message is after the last message - if (current_msg_date.isAfter(last_msg_date, 'day')) { - // Append a new day indicator - this.insertDayIndicator(current_msg_date); - } - this.insertMessage(attrs); - return; - } - if (current_msg_date.isBefore(first_msg_date) || current_msg_date.isSame(first_msg_date)) { - // The message is before the first, but on the same day. - // We need to prepend the message immediately before the - // first message (so that it'll still be after the day - // indicator). - this.insertMessage(attrs, 'prepend'); - if (current_msg_date.isBefore(first_msg_date, 'day')) { - // This message is also on a different day, so - // we prepend a day indicator. - this.insertDayIndicator(current_msg_date, 'prepend'); - } - return; - } - // Find the correct place to position the message - current_msg_date = current_msg_date.format(); - var msg_dates = _.map(this.$content.find('.chat-message'), function (el) { - return $(el).data('isodate'); - }); - msg_dates.push(current_msg_date); - msg_dates.sort(); - - var idx = msg_dates.indexOf(current_msg_date) - 1; - var $latest_message = this.$content.find(".chat-message[data-isodate=\"" + msg_dates[idx] + "\"]:last"); - _.flow(function ($el) { - $el.insertAfter($latest_message); - return $el; - }, this.scrollDown.bind(this))(this.renderMessage(attrs)); - }, - getExtraMessageTemplateAttributes: function getExtraMessageTemplateAttributes() { - /* Provides a hook for sending more attributes to the - * message template. - * - * Parameters: - * (Object) attrs: An object containing message attributes. - */ - return {}; - }, - getExtraMessageClasses: function getExtraMessageClasses(attrs) { - return attrs.delayed && 'delayed' || ''; - }, - renderMessage: function renderMessage(attrs) { - /* Renders a chat message based on the passed in attributes. - * - * Parameters: - * (Object) attrs: An object containing the message attributes. - * - * Returns: - * The DOM element representing the message. - */ - var text = attrs.message, - fullname = this.model.get('fullname') || attrs.fullname, - template = void 0, - username = void 0; - - var match = text.match(/^\/(.*?)(?: (.*))?$/); - if (match && match[1] === 'me') { - text = text.replace(/^\/me/, ''); - template = tpl_action; - if (attrs.sender === 'me') { - fullname = _converse.xmppstatus.get('fullname') || attrs.fullname; - username = _.isNil(fullname) ? _converse.bare_jid : fullname; - } else { - username = attrs.fullname; - } - } else { - template = tpl_message; - username = attrs.sender === 'me' && __('me') || fullname; - } - this.$content.find('div.chat-event').remove(); - - if (text.length > 8000) { - text = text.substring(0, 10) + '...'; - this.showStatusNotification(__("A very large message has been received." + "This might be due to an attack meant to degrade the chat performance." + "Output has been shortened."), true, true); - } - var msg_time = moment(attrs.time) || moment; - var $msg = $(template(_.extend(this.getExtraMessageTemplateAttributes(attrs), { - 'msgid': attrs.msgid, - 'sender': attrs.sender, - 'time': msg_time.format(_converse.time_format), - 'isodate': msg_time.format(), - 'username': username, - 'extra_classes': this.getExtraMessageClasses(attrs) - }))); - var msg_content = $msg[0].querySelector('.chat-msg-content'); - msg_content.innerHTML = utils.addEmoji(_converse, emojione, utils.addHyperlinks(xss.filterXSS(text, { 'whiteList': {} }))); - utils.renderImageURLs(msg_content); - return $msg; - }, - showHelpMessages: function showHelpMessages(msgs, type, spinner) { - var _this3 = this; - - _.each(msgs, function (msg) { - _this3.$content.append($(tpl_help_message({ - 'type': type || 'info', - 'message': msgs - }))); - }); - if (spinner === true) { - this.$content.append(tpl_spinner); - } else if (spinner === false) { - this.$content.find('span.spinner').remove(); - } - return this.scrollDown(); - }, - handleChatStateMessage: function handleChatStateMessage(message) { - if (message.get('chat_state') === _converse.COMPOSING) { - if (message.get('sender') === 'me') { - this.showStatusNotification(__('Typing from another device')); - } else { - this.showStatusNotification(message.get('fullname') + ' ' + __('is typing')); - } - this.clear_status_timeout = window.setTimeout(this.clearStatusNotification.bind(this), 30000); - } else if (message.get('chat_state') === _converse.PAUSED) { - if (message.get('sender') === 'me') { - this.showStatusNotification(__('Stopped typing on the other device')); - } else { - this.showStatusNotification(message.get('fullname') + ' ' + __('has stopped typing')); - } - } else if (_.includes([_converse.INACTIVE, _converse.ACTIVE], message.get('chat_state'))) { - this.$content.find('div.chat-event').remove(); - } else if (message.get('chat_state') === _converse.GONE) { - this.showStatusNotification(message.get('fullname') + ' ' + __('has gone away')); - } - }, - shouldShowOnTextMessage: function shouldShowOnTextMessage() { - return !this.$el.is(':visible'); - }, - handleTextMessage: function handleTextMessage(message) { - this.showMessage(_.clone(message.attributes)); - if (utils.isNewMessage(message) && message.get('sender') === 'me') { - // We remove the "scrolled" flag so that the chat area - // gets scrolled down. We always want to scroll down - // when the user writes a message as opposed to when a - // message is received. - this.model.set('scrolled', false); - } else { - if (utils.isNewMessage(message) && this.model.get('scrolled', true)) { - this.$el.find('.new-msgs-indicator').removeClass('hidden'); - } - } - if (this.shouldShowOnTextMessage()) { - this.show(); - } else { - this.scrollDown(); - } - }, - handleErrorMessage: function handleErrorMessage(message) { - var $message = $("[data-msgid=" + message.get('msgid') + "]"); - if ($message.length) { - $message.after($('
        ').text(message.get('message'))); - this.scrollDown(); - } - }, - onMessageAdded: function onMessageAdded(message) { - /* Handler that gets called when a new message object is created. - * - * Parameters: - * (Object) message - The message Backbone object that was added. - */ - if (!_.isUndefined(this.clear_status_timeout)) { - window.clearTimeout(this.clear_status_timeout); - delete this.clear_status_timeout; - } - if (message.get('type') === 'error') { - this.handleErrorMessage(message); - } else if (!message.get('message')) { - this.handleChatStateMessage(message); - } else { - this.handleTextMessage(message); - } - _converse.emit('messageAdded', { - 'message': message, - 'chatbox': this.model - }); - }, - createMessageStanza: function createMessageStanza(message) { - return $msg({ - from: _converse.connection.jid, - to: this.model.get('jid'), - type: 'chat', - id: message.get('msgid') - }).c('body').t(message.get('message')).up().c(_converse.ACTIVE, { 'xmlns': Strophe.NS.CHATSTATES }).up(); - }, - sendMessage: function sendMessage(message) { - /* Responsible for sending off a text message. - * - * Parameters: - * (Message) message - The chat message - */ - // TODO: We might want to send to specfic resources. - // Especially in the OTR case. - var messageStanza = this.createMessageStanza(message); - _converse.connection.send(messageStanza); - if (_converse.forward_messages) { - // Forward the message, so that other connected resources are also aware of it. - _converse.connection.send($msg({ to: _converse.bare_jid, type: 'chat', id: message.get('msgid') }).c('forwarded', { xmlns: 'urn:xmpp:forward:0' }).c('delay', { xmns: 'urn:xmpp:delay', stamp: new Date().getTime() }).up().cnode(messageStanza.tree())); - } - }, - onMessageSubmitted: function onMessageSubmitted(text) { - /* This method gets called once the user has typed a message - * and then pressed enter in a chat box. - * - * Parameters: - * (string) text - The chat message text. - */ - if (!_converse.connection.authenticated) { - return this.showHelpMessages(['Sorry, the connection has been lost, ' + 'and your message could not be sent'], 'error'); - } - var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/); - if (match) { - if (match[1] === "clear") { - return this.clearMessages(); - } else if (match[1] === "help") { - var msgs = ["/help:" + __('Show this menu'), "/me:" + __('Write in the third person'), "/clear:" + __('Remove messages')]; - this.showHelpMessages(msgs); - return; - } - } - var fullname = _converse.xmppstatus.get('fullname'); - fullname = _.isEmpty(fullname) ? _converse.bare_jid : fullname; - - var message = this.model.messages.create({ - fullname: fullname, - sender: 'me', - time: moment().format(), - message: text - }); - this.sendMessage(message); - }, - sendChatState: function sendChatState() { - /* Sends a message with the status of the user in this chat session - * as taken from the 'chat_state' attribute of the chat box. - * See XEP-0085 Chat State Notifications. - */ - _converse.connection.send($msg({ 'to': this.model.get('jid'), 'type': 'chat' }).c(this.model.get('chat_state'), { 'xmlns': Strophe.NS.CHATSTATES }).up().c('no-store', { 'xmlns': Strophe.NS.HINTS }).up().c('no-permanent-store', { 'xmlns': Strophe.NS.HINTS })); - }, - setChatState: function setChatState(state, no_save) { - /* Mutator for setting the chat state of this chat session. - * Handles clearing of any chat state notification timeouts and - * setting new ones if necessary. - * Timeouts are set when the state being set is COMPOSING or PAUSED. - * After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE. - * See XEP-0085 Chat State Notifications. - * - * Parameters: - * (string) state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE) - * (Boolean) no_save - Just do the cleanup or setup but don't actually save the state. - */ - if (!_.isUndefined(this.chat_state_timeout)) { - window.clearTimeout(this.chat_state_timeout); - delete this.chat_state_timeout; - } - if (state === _converse.COMPOSING) { - this.chat_state_timeout = window.setTimeout(this.setChatState.bind(this), _converse.TIMEOUTS.PAUSED, _converse.PAUSED); - } else if (state === _converse.PAUSED) { - this.chat_state_timeout = window.setTimeout(this.setChatState.bind(this), _converse.TIMEOUTS.INACTIVE, _converse.INACTIVE); - } - if (!no_save && this.model.get('chat_state') !== state) { - this.model.set('chat_state', state); - } - return this; - }, - onFormSubmitted: function onFormSubmitted(ev) { - ev.preventDefault(); - var textarea = this.el.querySelector('.chat-textarea'), - message = textarea.value; - textarea.value = ''; - textarea.focus(); - if (message !== '') { - this.onMessageSubmitted(message); - _converse.emit('messageSend', message); - } - this.setChatState(_converse.ACTIVE); - }, - keyPressed: function keyPressed(ev) { - /* Event handler for when a key is pressed in a chat box textarea. - */ - if (ev.keyCode === KEY.ENTER) { - this.onFormSubmitted(ev); - } else { - // Set chat state to composing if keyCode is not a forward-slash - // (which would imply an internal command and not a message). - this.setChatState(_converse.COMPOSING, ev.keyCode === KEY.FORWARD_SLASH); - } - }, - clearMessages: function clearMessages(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - var result = confirm(__("Are you sure you want to clear the messages from this chat box?")); - if (result === true) { - this.$content.empty(); - this.model.messages.reset(); - this.model.messages.browserStorage._clear(); - } - return this; - }, - insertIntoTextArea: function insertIntoTextArea(value) { - var $textbox = this.$el.find('textarea.chat-textarea'); - var existing = $textbox.val(); - if (existing && existing[existing.length - 1] !== ' ') { - existing = existing + ' '; - } - $textbox.focus().val(existing + value + ' '); - }, - insertEmoji: function insertEmoji(ev) { - ev.stopPropagation(); - this.toggleEmojiMenu(); - var target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target; - this.insertIntoTextArea(target.getAttribute('data-emoji')); - }, - toggleEmojiMenu: function toggleEmojiMenu(ev) { - if (!_.isUndefined(ev)) { - ev.stopPropagation(); - if (ev.target.classList.contains('emoji-category-picker') || ev.target.classList.contains('emoji-skintone-picker') || ev.target.classList.contains('emoji-category')) { - return; - } - } - var elements = _.difference(document.querySelectorAll('.toolbar-menu'), [this.emoji_picker_view.el]); - utils.slideInAllElements(elements).then(_.partial(utils.slideToggleElement, this.emoji_picker_view.el)); - }, - toggleCall: function toggleCall(ev) { - ev.stopPropagation(); - _converse.emit('callButtonClicked', { - connection: _converse.connection, - model: this.model - }); - }, - onChatStatusChanged: function onChatStatusChanged(item) { - var chat_status = item.get('chat_status'); - var fullname = item.get('fullname'); - fullname = _.isEmpty(fullname) ? item.get('jid') : fullname; - if (this.$el.is(':visible')) { - if (chat_status === 'offline') { - this.showStatusNotification(fullname + ' ' + __('has gone offline')); - } else if (chat_status === 'away') { - this.showStatusNotification(fullname + ' ' + __('has gone away')); - } else if (chat_status === 'dnd') { - this.showStatusNotification(fullname + ' ' + __('is busy')); - } else if (chat_status === 'online') { - this.$el.find('div.chat-event').remove(); - } - } - }, - onStatusChanged: function onStatusChanged(item) { - this.showStatusMessage(); - _converse.emit('contactStatusMessageChanged', { - 'contact': item.attributes, - 'message': item.get('status') - }); - }, - showStatusMessage: function showStatusMessage(msg) { - msg = msg || this.model.get('status'); - if (_.isString(msg)) { - this.$el.find('p.user-custom-message').text(msg).attr('title', msg); - } - return this; - }, - close: function close(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - if (Backbone.history.getFragment() === "converse/chat?jid=" + this.model.get('jid')) { - _converse.router.navigate(''); - } - if (_converse.connection.connected) { - // Immediately sending the chat state, because the - // model is going to be destroyed afterwards. - this.model.set('chat_state', _converse.INACTIVE); - this.sendChatState(); - } - try { - this.model.destroy(); - } catch (e) { - _converse.log(e, Strophe.LogLevel.ERROR); - } - this.remove(); - _converse.emit('chatBoxClosed', this); - return this; - }, - getToolbarOptions: function getToolbarOptions(options) { - return _.extend(options || {}, { - 'label_clear': __('Clear all messages'), - 'label_insert_smiley': __('Insert a smiley'), - 'label_start_call': __('Start a call'), - 'show_call_button': _converse.visible_toolbar_buttons.call, - 'show_clear_button': _converse.visible_toolbar_buttons.clear, - 'use_emoji': _converse.visible_toolbar_buttons.emoji - }); - }, - renderToolbar: function renderToolbar(toolbar, options) { - if (!_converse.show_toolbar) { - return; - } - toolbar = toolbar || tpl_toolbar; - options = _.assign(this.model.toJSON(), this.getToolbarOptions(options || {})); - this.el.querySelector('.chat-toolbar').innerHTML = toolbar(options); - - var toggle = this.el.querySelector('.toggle-smiley'); - toggle.innerHTML = ''; - toggle.appendChild(this.emoji_picker_view.render().el); - return this; - }, - renderAvatar: function renderAvatar() { - if (!this.model.get('image')) { - return; - } - var width = _converse.chatview_avatar_width; - var height = _converse.chatview_avatar_height; - var img_src = "data:" + this.model.get('image_type') + ";base64," + this.model.get('image'), - canvas = $(tpl_avatar({ - 'width': width, - 'height': height - })).get(0); - - if (!(canvas.getContext && canvas.getContext('2d'))) { - return this; - } - var ctx = canvas.getContext('2d'); - var img = new Image(); // Create new Image object - img.onload = function () { - var ratio = img.width / img.height; - if (ratio < 1) { - ctx.drawImage(img, 0, 0, width, height * (1 / ratio)); - } else { - ctx.drawImage(img, 0, 0, width, height * ratio); - } - }; - img.src = img_src; - this.$el.find('.chat-title').before(canvas); - return this; - }, - focus: function focus() { - this.$el.find('.chat-textarea').focus(); - _converse.emit('chatBoxFocused', this); - return this; - }, - hide: function hide() { - this.el.classList.add('hidden'); - utils.refreshWebkit(); - return this; - }, - afterShown: function afterShown(focus) { - if (utils.isPersistableModel(this.model)) { - this.model.save(); - } - this.setChatState(_converse.ACTIVE); - this.scrollDown(); - if (focus) { - this.focus(); - } - }, - _show: function _show(focus) { - /* Inner show method that gets debounced */ - if (this.$el.is(':visible') && this.$el.css('opacity') === "1") { - if (focus) { - this.focus(); - } - return; - } - utils.fadeIn(this.el, _.bind(this.afterShown, this, focus)); - }, - show: function show(focus) { - if (_.isUndefined(this.debouncedShow)) { - /* We wrap the method in a debouncer and set it on the - * instance, so that we have it debounced per instance. - * Debouncing it on the class-level is too broad. - */ - this.debouncedShow = _.debounce(this._show, 250, { 'leading': true }); - } - this.debouncedShow.apply(this, arguments); - return this; - }, - hideNewMessagesIndicator: function hideNewMessagesIndicator() { - var new_msgs_indicator = this.el.querySelector('.new-msgs-indicator'); - if (!_.isNull(new_msgs_indicator)) { - new_msgs_indicator.classList.add('hidden'); - } - }, - - - markScrolled: function markScrolled(ev) { - /* Called when the chat content is scrolled up or down. - * We want to record when the user has scrolled away from - * the bottom, so that we don't automatically scroll away - * from what the user is reading when new messages are - * received. - */ - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - if (this.model.get('auto_scrolled')) { - this.model.set({ - 'scrolled': false, - 'auto_scrolled': false + this.el.querySelector('#fancy-xmpp-status-select') + .innerHTML = tpl_chat_status({ + 'status_message': this.model.get('status_message') || + __("I am %1$s", this.getPrettyStatus(chat_status)), + 'chat_status': chat_status, + 'desc_custom_status': __('Click here to write a custom status message'), + 'desc_change_status': __('Click to change your chat status') }); - return; - } - var scrolled = true; - var is_at_bottom = this.$content.scrollTop() + this.$content.innerHeight() >= this.$content[0].scrollHeight - 10; - if (is_at_bottom) { - scrolled = false; - this.onScrolledDown(); - } - utils.safeSave(this.model, { 'scrolled': scrolled }); - }, - - viewUnreadMessages: function viewUnreadMessages() { - this.model.save('scrolled', false); - this.scrollDown(); - }, - _scrollDown: function _scrollDown() { - /* Inner method that gets debounced */ - if (this.$content.is(':visible') && !this.model.get('scrolled')) { - this.$content.scrollTop(this.$content[0].scrollHeight); - this.onScrolledDown(); - this.model.save({ 'auto_scrolled': true }); - } - }, - onScrolledDown: function onScrolledDown() { - this.hideNewMessagesIndicator(); - if (_converse.windowState !== 'hidden') { - this.model.clearUnreadMsgCounter(); - } - _converse.emit('chatBoxScrolledDown', { 'chatbox': this.model }); - }, - scrollDown: function scrollDown() { - if (_.isUndefined(this.debouncedScrollDown)) { - /* We wrap the method in a debouncer and set it on the - * instance, so that we have it debounced per instance. - * Debouncing it on the class-level is too broad. - */ - this.debouncedScrollDown = _.debounce(this._scrollDown, 250); - } - this.debouncedScrollDown.apply(this, arguments); + // iterate through all the