diff --git a/Makefile b/Makefile index c0b404c91..2c94d0fa9 100644 --- a/Makefile +++ b/Makefile @@ -149,6 +149,8 @@ watch: stamp-bundler BUILDS = dist/converse.js \ dist/converse.min.js \ + dist/inverse.js \ + dist/inverse.min.js \ dist/converse-mobile.js \ dist/converse-mobile.min.js \ dist/converse-muc-embedded.js \ @@ -162,6 +164,10 @@ dist/converse.min.js: src locale node_modules *.js $(RJS) -o src/build.js include=converse out=dist/converse.min.js dist/converse.js: src locale node_modules *.js $(RJS) -o src/build.js include=converse out=dist/converse.js optimize=none +dist/inverse.js: src locale node_modules *.js + $(RJS) -o src/build-inverse.js include=inverse out=dist/inverse.js optimize=none +dist/inverse.min.js: src locale node_modules *.js + $(RJS) -o src/build-inverse.js include=inverse out=dist/inverse.min.js dist/converse-no-jquery.min.js: src locale node_modules *.js $(RJS) -o src/build.js include=converse wrap.endFile=end-no-jquery.frag exclude=jquery exclude=jquery.noconflict out=dist/converse-no-jquery.min.js dist/converse-no-jquery.js: src locale node_modules *.js diff --git a/dist/converse.js b/dist/converse.js index ddd5172d9..58e4bdef1 100644 --- a/dist/converse.js +++ b/dist/converse.js @@ -29536,9 +29536,1062 @@ define('jquery.noconflict',['jquery'], function (jq) { /*global define */ define('lodash.noconflict',['lodash'], function (_) { + if (!_.isUndefined(require) && !_.isUndefined(require.s)) { + /* XXX: This is a hack to make sure that the compiled templates have + * access to the _ object. + * + * Otherwise we sometimes get errors like this: + * + * TypeError: Cannot read property 'escape' of undefined + * at eval (./src/templates/chatroom_sidebar.html:6) + */ + var lodashLoader = require.s.contexts._.config.lodashLoader; + lodashLoader.templateSettings.imports = { '_': _ }; + require.config({'lodashLoader': lodashLoader}); + } return _.noConflict(); }); +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define('lodash.converter',[], factory); + else if(typeof exports === 'object') + exports["fp"] = factory(); + else + root["fp"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + var baseConvert = __webpack_require__(1); + + /** + * Converts `lodash` to an immutable auto-curried iteratee-first data-last + * version with conversion `options` applied. + * + * @param {Function} lodash The lodash function to convert. + * @param {Object} [options] The options object. See `baseConvert` for more details. + * @returns {Function} Returns the converted `lodash`. + */ + function browserConvert(lodash, options) { + return baseConvert(lodash, lodash, options); + } + + if (typeof _ == 'function' && typeof _.runInContext == 'function') { + _ = browserConvert(_.runInContext()); + } + module.exports = browserConvert; + + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + var mapping = __webpack_require__(2), + fallbackHolder = __webpack_require__(3); + + /** Built-in value reference. */ + var push = Array.prototype.push; + + /** + * Creates a function, with an arity of `n`, that invokes `func` with the + * arguments it receives. + * + * @private + * @param {Function} func The function to wrap. + * @param {number} n The arity of the new function. + * @returns {Function} Returns the new function. + */ + function baseArity(func, n) { + return n == 2 + ? function(a, b) { return func.apply(undefined, arguments); } + : function(a) { return func.apply(undefined, arguments); }; + } + + /** + * Creates a function that invokes `func`, with up to `n` arguments, ignoring + * any additional arguments. + * + * @private + * @param {Function} func The function to cap arguments for. + * @param {number} n The arity cap. + * @returns {Function} Returns the new function. + */ + function baseAry(func, n) { + return n == 2 + ? function(a, b) { return func(a, b); } + : function(a) { return func(a); }; + } + + /** + * Creates a clone of `array`. + * + * @private + * @param {Array} array The array to clone. + * @returns {Array} Returns the cloned array. + */ + function cloneArray(array) { + var length = array ? array.length : 0, + result = Array(length); + + while (length--) { + result[length] = array[length]; + } + return result; + } + + /** + * Creates a function that clones a given object using the assignment `func`. + * + * @private + * @param {Function} func The assignment function. + * @returns {Function} Returns the new cloner function. + */ + function createCloner(func) { + return function(object) { + return func({}, object); + }; + } + + /** + * A specialized version of `_.spread` which flattens the spread array into + * the arguments of the invoked `func`. + * + * @private + * @param {Function} func The function to spread arguments over. + * @param {number} start The start position of the spread. + * @returns {Function} Returns the new function. + */ + function flatSpread(func, start) { + return function() { + var length = arguments.length, + lastIndex = length - 1, + args = Array(length); + + while (length--) { + args[length] = arguments[length]; + } + var array = args[start], + otherArgs = args.slice(0, start); + + if (array) { + push.apply(otherArgs, array); + } + if (start != lastIndex) { + push.apply(otherArgs, args.slice(start + 1)); + } + return func.apply(this, otherArgs); + }; + } + + /** + * Creates a function that wraps `func` and uses `cloner` to clone the first + * argument it receives. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} cloner The function to clone arguments. + * @returns {Function} Returns the new immutable function. + */ + function wrapImmutable(func, cloner) { + return function() { + var length = arguments.length; + if (!length) { + return; + } + var args = Array(length); + while (length--) { + args[length] = arguments[length]; + } + var result = args[0] = cloner.apply(undefined, args); + func.apply(undefined, args); + return result; + }; + } + + /** + * The base implementation of `convert` which accepts a `util` object of methods + * required to perform conversions. + * + * @param {Object} util The util object. + * @param {string} name The name of the function to convert. + * @param {Function} func The function to convert. + * @param {Object} [options] The options object. + * @param {boolean} [options.cap=true] Specify capping iteratee arguments. + * @param {boolean} [options.curry=true] Specify currying. + * @param {boolean} [options.fixed=true] Specify fixed arity. + * @param {boolean} [options.immutable=true] Specify immutable operations. + * @param {boolean} [options.rearg=true] Specify rearranging arguments. + * @returns {Function|Object} Returns the converted function or object. + */ + function baseConvert(util, name, func, options) { + var setPlaceholder, + isLib = typeof name == 'function', + isObj = name === Object(name); + + if (isObj) { + options = func; + func = name; + name = undefined; + } + if (func == null) { + throw new TypeError; + } + options || (options = {}); + + var config = { + 'cap': 'cap' in options ? options.cap : true, + 'curry': 'curry' in options ? options.curry : true, + 'fixed': 'fixed' in options ? options.fixed : true, + 'immutable': 'immutable' in options ? options.immutable : true, + 'rearg': 'rearg' in options ? options.rearg : true + }; + + var forceCurry = ('curry' in options) && options.curry, + forceFixed = ('fixed' in options) && options.fixed, + forceRearg = ('rearg' in options) && options.rearg, + placeholder = isLib ? func : fallbackHolder, + pristine = isLib ? func.runInContext() : undefined; + + var helpers = isLib ? func : { + 'ary': util.ary, + 'assign': util.assign, + 'clone': util.clone, + 'curry': util.curry, + 'forEach': util.forEach, + 'isArray': util.isArray, + 'isFunction': util.isFunction, + 'iteratee': util.iteratee, + 'keys': util.keys, + 'rearg': util.rearg, + 'toInteger': util.toInteger, + 'toPath': util.toPath + }; + + var ary = helpers.ary, + assign = helpers.assign, + clone = helpers.clone, + curry = helpers.curry, + each = helpers.forEach, + isArray = helpers.isArray, + isFunction = helpers.isFunction, + keys = helpers.keys, + rearg = helpers.rearg, + toInteger = helpers.toInteger, + toPath = helpers.toPath; + + var aryMethodKeys = keys(mapping.aryMethod); + + var wrappers = { + 'castArray': function(castArray) { + return function() { + var value = arguments[0]; + return isArray(value) + ? castArray(cloneArray(value)) + : castArray.apply(undefined, arguments); + }; + }, + 'iteratee': function(iteratee) { + return function() { + var func = arguments[0], + arity = arguments[1], + result = iteratee(func, arity), + length = result.length; + + if (config.cap && typeof arity == 'number') { + arity = arity > 2 ? (arity - 2) : 1; + return (length && length <= arity) ? result : baseAry(result, arity); + } + return result; + }; + }, + 'mixin': function(mixin) { + return function(source) { + var func = this; + if (!isFunction(func)) { + return mixin(func, Object(source)); + } + var pairs = []; + each(keys(source), function(key) { + if (isFunction(source[key])) { + pairs.push([key, func.prototype[key]]); + } + }); + + mixin(func, Object(source)); + + each(pairs, function(pair) { + var value = pair[1]; + if (isFunction(value)) { + func.prototype[pair[0]] = value; + } else { + delete func.prototype[pair[0]]; + } + }); + return func; + }; + }, + 'nthArg': function(nthArg) { + return function(n) { + var arity = n < 0 ? 1 : (toInteger(n) + 1); + return curry(nthArg(n), arity); + }; + }, + 'rearg': function(rearg) { + return function(func, indexes) { + var arity = indexes ? indexes.length : 0; + return curry(rearg(func, indexes), arity); + }; + }, + 'runInContext': function(runInContext) { + return function(context) { + return baseConvert(util, runInContext(context), options); + }; + } + }; + + /*--------------------------------------------------------------------------*/ + + /** + * Casts `func` to a function with an arity capped iteratee if needed. + * + * @private + * @param {string} name The name of the function to inspect. + * @param {Function} func The function to inspect. + * @returns {Function} Returns the cast function. + */ + function castCap(name, func) { + if (config.cap) { + var indexes = mapping.iterateeRearg[name]; + if (indexes) { + return iterateeRearg(func, indexes); + } + var n = !isLib && mapping.iterateeAry[name]; + if (n) { + return iterateeAry(func, n); + } + } + return func; + } + + /** + * Casts `func` to a curried function if needed. + * + * @private + * @param {string} name The name of the function to inspect. + * @param {Function} func The function to inspect. + * @param {number} n The arity of `func`. + * @returns {Function} Returns the cast function. + */ + function castCurry(name, func, n) { + return (forceCurry || (config.curry && n > 1)) + ? curry(func, n) + : func; + } + + /** + * Casts `func` to a fixed arity function if needed. + * + * @private + * @param {string} name The name of the function to inspect. + * @param {Function} func The function to inspect. + * @param {number} n The arity cap. + * @returns {Function} Returns the cast function. + */ + function castFixed(name, func, n) { + if (config.fixed && (forceFixed || !mapping.skipFixed[name])) { + var data = mapping.methodSpread[name], + start = data && data.start; + + return start === undefined ? ary(func, n) : flatSpread(func, start); + } + return func; + } + + /** + * Casts `func` to an rearged function if needed. + * + * @private + * @param {string} name The name of the function to inspect. + * @param {Function} func The function to inspect. + * @param {number} n The arity of `func`. + * @returns {Function} Returns the cast function. + */ + function castRearg(name, func, n) { + return (config.rearg && n > 1 && (forceRearg || !mapping.skipRearg[name])) + ? rearg(func, mapping.methodRearg[name] || mapping.aryRearg[n]) + : func; + } + + /** + * Creates a clone of `object` by `path`. + * + * @private + * @param {Object} object The object to clone. + * @param {Array|string} path The path to clone by. + * @returns {Object} Returns the cloned object. + */ + function cloneByPath(object, path) { + path = toPath(path); + + var index = -1, + length = path.length, + lastIndex = length - 1, + result = clone(Object(object)), + nested = result; + + while (nested != null && ++index < length) { + var key = path[index], + value = nested[key]; + + if (value != null) { + nested[path[index]] = clone(index == lastIndex ? value : Object(value)); + } + nested = nested[key]; + } + return result; + } + + /** + * Converts `lodash` to an immutable auto-curried iteratee-first data-last + * version with conversion `options` applied. + * + * @param {Object} [options] The options object. See `baseConvert` for more details. + * @returns {Function} Returns the converted `lodash`. + */ + function convertLib(options) { + return _.runInContext.convert(options)(undefined); + } + + /** + * Create a converter function for `func` of `name`. + * + * @param {string} name The name of the function to convert. + * @param {Function} func The function to convert. + * @returns {Function} Returns the new converter function. + */ + function createConverter(name, func) { + var realName = mapping.aliasToReal[name] || name, + methodName = mapping.remap[realName] || realName, + oldOptions = options; + + return function(options) { + var newUtil = isLib ? pristine : helpers, + newFunc = isLib ? pristine[methodName] : func, + newOptions = assign(assign({}, oldOptions), options); + + return baseConvert(newUtil, realName, newFunc, newOptions); + }; + } + + /** + * Creates a function that wraps `func` to invoke its iteratee, with up to `n` + * arguments, ignoring any additional arguments. + * + * @private + * @param {Function} func The function to cap iteratee arguments for. + * @param {number} n The arity cap. + * @returns {Function} Returns the new function. + */ + function iterateeAry(func, n) { + return overArg(func, function(func) { + return typeof func == 'function' ? baseAry(func, n) : func; + }); + } + + /** + * Creates a function that wraps `func` to invoke its iteratee with arguments + * arranged according to the specified `indexes` where the argument value at + * the first index is provided as the first argument, the argument value at + * the second index is provided as the second argument, and so on. + * + * @private + * @param {Function} func The function to rearrange iteratee arguments for. + * @param {number[]} indexes The arranged argument indexes. + * @returns {Function} Returns the new function. + */ + function iterateeRearg(func, indexes) { + return overArg(func, function(func) { + var n = indexes.length; + return baseArity(rearg(baseAry(func, n), indexes), n); + }); + } + + /** + * Creates a function that invokes `func` with its first argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ + function overArg(func, transform) { + return function() { + var length = arguments.length; + if (!length) { + return func(); + } + var args = Array(length); + while (length--) { + args[length] = arguments[length]; + } + var index = config.rearg ? 0 : (length - 1); + args[index] = transform(args[index]); + return func.apply(undefined, args); + }; + } + + /** + * Creates a function that wraps `func` and applys the conversions + * rules by `name`. + * + * @private + * @param {string} name The name of the function to wrap. + * @param {Function} func The function to wrap. + * @returns {Function} Returns the converted function. + */ + function wrap(name, func) { + var result, + realName = mapping.aliasToReal[name] || name, + wrapped = func, + wrapper = wrappers[realName]; + + if (wrapper) { + wrapped = wrapper(func); + } + else if (config.immutable) { + if (mapping.mutate.array[realName]) { + wrapped = wrapImmutable(func, cloneArray); + } + else if (mapping.mutate.object[realName]) { + wrapped = wrapImmutable(func, createCloner(func)); + } + else if (mapping.mutate.set[realName]) { + wrapped = wrapImmutable(func, cloneByPath); + } + } + each(aryMethodKeys, function(aryKey) { + each(mapping.aryMethod[aryKey], function(otherName) { + if (realName == otherName) { + var data = mapping.methodSpread[realName], + afterRearg = data && data.afterRearg; + + result = afterRearg + ? castFixed(realName, castRearg(realName, wrapped, aryKey), aryKey) + : castRearg(realName, castFixed(realName, wrapped, aryKey), aryKey); + + result = castCap(realName, result); + result = castCurry(realName, result, aryKey); + return false; + } + }); + return !result; + }); + + result || (result = wrapped); + if (result == func) { + result = forceCurry ? curry(result, 1) : function() { + return func.apply(this, arguments); + }; + } + result.convert = createConverter(realName, func); + if (mapping.placeholder[realName]) { + setPlaceholder = true; + result.placeholder = func.placeholder = placeholder; + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + if (!isObj) { + return wrap(name, func); + } + var _ = func; + + // Convert methods by ary cap. + var pairs = []; + each(aryMethodKeys, function(aryKey) { + each(mapping.aryMethod[aryKey], function(key) { + var func = _[mapping.remap[key] || key]; + if (func) { + pairs.push([key, wrap(key, func)]); + } + }); + }); + + // Convert remaining methods. + each(keys(_), function(key) { + var func = _[key]; + if (typeof func == 'function') { + var length = pairs.length; + while (length--) { + if (pairs[length][0] == key) { + return; + } + } + func.convert = createConverter(key, func); + pairs.push([key, func]); + } + }); + + // Assign to `_` leaving `_.prototype` unchanged to allow chaining. + each(pairs, function(pair) { + _[pair[0]] = pair[1]; + }); + + _.convert = convertLib; + if (setPlaceholder) { + _.placeholder = placeholder; + } + // Assign aliases. + each(keys(_), function(key) { + each(mapping.realToAlias[key] || [], function(alias) { + _[alias] = _[key]; + }); + }); + + return _; + } + + module.exports = baseConvert; + + +/***/ }, +/* 2 */ +/***/ function(module, exports) { + + /** Used to map aliases to their real names. */ + exports.aliasToReal = { + + // Lodash aliases. + 'each': 'forEach', + 'eachRight': 'forEachRight', + 'entries': 'toPairs', + 'entriesIn': 'toPairsIn', + 'extend': 'assignIn', + 'extendAll': 'assignInAll', + 'extendAllWith': 'assignInAllWith', + 'extendWith': 'assignInWith', + 'first': 'head', + + // Methods that are curried variants of others. + 'conforms': 'conformsTo', + 'matches': 'isMatch', + 'property': 'get', + + // Ramda aliases. + '__': 'placeholder', + 'F': 'stubFalse', + 'T': 'stubTrue', + 'all': 'every', + 'allPass': 'overEvery', + 'always': 'constant', + 'any': 'some', + 'anyPass': 'overSome', + 'apply': 'spread', + 'assoc': 'set', + 'assocPath': 'set', + 'complement': 'negate', + 'compose': 'flowRight', + 'contains': 'includes', + 'dissoc': 'unset', + 'dissocPath': 'unset', + 'dropLast': 'dropRight', + 'dropLastWhile': 'dropRightWhile', + 'equals': 'isEqual', + 'identical': 'eq', + 'indexBy': 'keyBy', + 'init': 'initial', + 'invertObj': 'invert', + 'juxt': 'over', + 'omitAll': 'omit', + 'nAry': 'ary', + 'path': 'get', + 'pathEq': 'matchesProperty', + 'pathOr': 'getOr', + 'paths': 'at', + 'pickAll': 'pick', + 'pipe': 'flow', + 'pluck': 'map', + 'prop': 'get', + 'propEq': 'matchesProperty', + 'propOr': 'getOr', + 'props': 'at', + 'symmetricDifference': 'xor', + 'symmetricDifferenceBy': 'xorBy', + 'symmetricDifferenceWith': 'xorWith', + 'takeLast': 'takeRight', + 'takeLastWhile': 'takeRightWhile', + 'unapply': 'rest', + 'unnest': 'flatten', + 'useWith': 'overArgs', + 'where': 'conformsTo', + 'whereEq': 'isMatch', + 'zipObj': 'zipObject' + }; + + /** Used to map ary to method names. */ + exports.aryMethod = { + '1': [ + 'assignAll', 'assignInAll', 'attempt', 'castArray', 'ceil', 'create', + 'curry', 'curryRight', 'defaultsAll', 'defaultsDeepAll', 'floor', 'flow', + 'flowRight', 'fromPairs', 'invert', 'iteratee', 'memoize', 'method', 'mergeAll', + 'methodOf', 'mixin', 'nthArg', 'over', 'overEvery', 'overSome','rest', 'reverse', + 'round', 'runInContext', 'spread', 'template', 'trim', 'trimEnd', 'trimStart', + 'uniqueId', 'words', 'zipAll' + ], + '2': [ + 'add', 'after', 'ary', 'assign', 'assignAllWith', 'assignIn', 'assignInAllWith', + 'at', 'before', 'bind', 'bindAll', 'bindKey', 'chunk', 'cloneDeepWith', + 'cloneWith', 'concat', 'conformsTo', 'countBy', 'curryN', 'curryRightN', + 'debounce', 'defaults', 'defaultsDeep', 'defaultTo', 'delay', 'difference', + 'divide', 'drop', 'dropRight', 'dropRightWhile', 'dropWhile', 'endsWith', 'eq', + 'every', 'filter', 'find', 'findIndex', 'findKey', 'findLast', 'findLastIndex', + 'findLastKey', 'flatMap', 'flatMapDeep', 'flattenDepth', 'forEach', + 'forEachRight', 'forIn', 'forInRight', 'forOwn', 'forOwnRight', 'get', + 'groupBy', 'gt', 'gte', 'has', 'hasIn', 'includes', 'indexOf', 'intersection', + 'invertBy', 'invoke', 'invokeMap', 'isEqual', 'isMatch', 'join', 'keyBy', + 'lastIndexOf', 'lt', 'lte', 'map', 'mapKeys', 'mapValues', 'matchesProperty', + 'maxBy', 'meanBy', 'merge', 'mergeAllWith', 'minBy', 'multiply', 'nth', 'omit', + 'omitBy', 'overArgs', 'pad', 'padEnd', 'padStart', 'parseInt', 'partial', + 'partialRight', 'partition', 'pick', 'pickBy', 'propertyOf', 'pull', 'pullAll', + 'pullAt', 'random', 'range', 'rangeRight', 'rearg', 'reject', 'remove', + 'repeat', 'restFrom', 'result', 'sampleSize', 'some', 'sortBy', 'sortedIndex', + 'sortedIndexOf', 'sortedLastIndex', 'sortedLastIndexOf', 'sortedUniqBy', + 'split', 'spreadFrom', 'startsWith', 'subtract', 'sumBy', 'take', 'takeRight', + 'takeRightWhile', 'takeWhile', 'tap', 'throttle', 'thru', 'times', 'trimChars', + 'trimCharsEnd', 'trimCharsStart', 'truncate', 'union', 'uniqBy', 'uniqWith', + 'unset', 'unzipWith', 'without', 'wrap', 'xor', 'zip', 'zipObject', + 'zipObjectDeep' + ], + '3': [ + 'assignInWith', 'assignWith', 'clamp', 'differenceBy', 'differenceWith', + 'findFrom', 'findIndexFrom', 'findLastFrom', 'findLastIndexFrom', 'getOr', + 'includesFrom', 'indexOfFrom', 'inRange', 'intersectionBy', 'intersectionWith', + 'invokeArgs', 'invokeArgsMap', 'isEqualWith', 'isMatchWith', 'flatMapDepth', + 'lastIndexOfFrom', 'mergeWith', 'orderBy', 'padChars', 'padCharsEnd', + 'padCharsStart', 'pullAllBy', 'pullAllWith', 'rangeStep', 'rangeStepRight', + 'reduce', 'reduceRight', 'replace', 'set', 'slice', 'sortedIndexBy', + 'sortedLastIndexBy', 'transform', 'unionBy', 'unionWith', 'update', 'xorBy', + 'xorWith', 'zipWith' + ], + '4': [ + 'fill', 'setWith', 'updateWith' + ] + }; + + /** Used to map ary to rearg configs. */ + exports.aryRearg = { + '2': [1, 0], + '3': [2, 0, 1], + '4': [3, 2, 0, 1] + }; + + /** Used to map method names to their iteratee ary. */ + exports.iterateeAry = { + 'dropRightWhile': 1, + 'dropWhile': 1, + 'every': 1, + 'filter': 1, + 'find': 1, + 'findFrom': 1, + 'findIndex': 1, + 'findIndexFrom': 1, + 'findKey': 1, + 'findLast': 1, + 'findLastFrom': 1, + 'findLastIndex': 1, + 'findLastIndexFrom': 1, + 'findLastKey': 1, + 'flatMap': 1, + 'flatMapDeep': 1, + 'flatMapDepth': 1, + 'forEach': 1, + 'forEachRight': 1, + 'forIn': 1, + 'forInRight': 1, + 'forOwn': 1, + 'forOwnRight': 1, + 'map': 1, + 'mapKeys': 1, + 'mapValues': 1, + 'partition': 1, + 'reduce': 2, + 'reduceRight': 2, + 'reject': 1, + 'remove': 1, + 'some': 1, + 'takeRightWhile': 1, + 'takeWhile': 1, + 'times': 1, + 'transform': 2 + }; + + /** Used to map method names to iteratee rearg configs. */ + exports.iterateeRearg = { + 'mapKeys': [1], + 'reduceRight': [1, 0] + }; + + /** Used to map method names to rearg configs. */ + exports.methodRearg = { + 'assignInAllWith': [1, 0], + 'assignInWith': [1, 2, 0], + 'assignAllWith': [1, 0], + 'assignWith': [1, 2, 0], + 'differenceBy': [1, 2, 0], + 'differenceWith': [1, 2, 0], + 'getOr': [2, 1, 0], + 'intersectionBy': [1, 2, 0], + 'intersectionWith': [1, 2, 0], + 'isEqualWith': [1, 2, 0], + 'isMatchWith': [2, 1, 0], + 'mergeAllWith': [1, 0], + 'mergeWith': [1, 2, 0], + 'padChars': [2, 1, 0], + 'padCharsEnd': [2, 1, 0], + 'padCharsStart': [2, 1, 0], + 'pullAllBy': [2, 1, 0], + 'pullAllWith': [2, 1, 0], + 'rangeStep': [1, 2, 0], + 'rangeStepRight': [1, 2, 0], + 'setWith': [3, 1, 2, 0], + 'sortedIndexBy': [2, 1, 0], + 'sortedLastIndexBy': [2, 1, 0], + 'unionBy': [1, 2, 0], + 'unionWith': [1, 2, 0], + 'updateWith': [3, 1, 2, 0], + 'xorBy': [1, 2, 0], + 'xorWith': [1, 2, 0], + 'zipWith': [1, 2, 0] + }; + + /** Used to map method names to spread configs. */ + exports.methodSpread = { + 'assignAll': { 'start': 0 }, + 'assignAllWith': { 'start': 0 }, + 'assignInAll': { 'start': 0 }, + 'assignInAllWith': { 'start': 0 }, + 'defaultsAll': { 'start': 0 }, + 'defaultsDeepAll': { 'start': 0 }, + 'invokeArgs': { 'start': 2 }, + 'invokeArgsMap': { 'start': 2 }, + 'mergeAll': { 'start': 0 }, + 'mergeAllWith': { 'start': 0 }, + 'partial': { 'start': 1 }, + 'partialRight': { 'start': 1 }, + 'without': { 'start': 1 }, + 'zipAll': { 'start': 0 } + }; + + /** Used to identify methods which mutate arrays or objects. */ + exports.mutate = { + 'array': { + 'fill': true, + 'pull': true, + 'pullAll': true, + 'pullAllBy': true, + 'pullAllWith': true, + 'pullAt': true, + 'remove': true, + 'reverse': true + }, + 'object': { + 'assign': true, + 'assignAll': true, + 'assignAllWith': true, + 'assignIn': true, + 'assignInAll': true, + 'assignInAllWith': true, + 'assignInWith': true, + 'assignWith': true, + 'defaults': true, + 'defaultsAll': true, + 'defaultsDeep': true, + 'defaultsDeepAll': true, + 'merge': true, + 'mergeAll': true, + 'mergeAllWith': true, + 'mergeWith': true, + }, + 'set': { + 'set': true, + 'setWith': true, + 'unset': true, + 'update': true, + 'updateWith': true + } + }; + + /** Used to track methods with placeholder support */ + exports.placeholder = { + 'bind': true, + 'bindKey': true, + 'curry': true, + 'curryRight': true, + 'partial': true, + 'partialRight': true + }; + + /** Used to map real names to their aliases. */ + exports.realToAlias = (function() { + var hasOwnProperty = Object.prototype.hasOwnProperty, + object = exports.aliasToReal, + result = {}; + + for (var key in object) { + var value = object[key]; + if (hasOwnProperty.call(result, value)) { + result[value].push(key); + } else { + result[value] = [key]; + } + } + return result; + }()); + + /** Used to map method names to other names. */ + exports.remap = { + 'assignAll': 'assign', + 'assignAllWith': 'assignWith', + 'assignInAll': 'assignIn', + 'assignInAllWith': 'assignInWith', + 'curryN': 'curry', + 'curryRightN': 'curryRight', + 'defaultsAll': 'defaults', + 'defaultsDeepAll': 'defaultsDeep', + 'findFrom': 'find', + 'findIndexFrom': 'findIndex', + 'findLastFrom': 'findLast', + 'findLastIndexFrom': 'findLastIndex', + 'getOr': 'get', + 'includesFrom': 'includes', + 'indexOfFrom': 'indexOf', + 'invokeArgs': 'invoke', + 'invokeArgsMap': 'invokeMap', + 'lastIndexOfFrom': 'lastIndexOf', + 'mergeAll': 'merge', + 'mergeAllWith': 'mergeWith', + 'padChars': 'pad', + 'padCharsEnd': 'padEnd', + 'padCharsStart': 'padStart', + 'propertyOf': 'get', + 'rangeStep': 'range', + 'rangeStepRight': 'rangeRight', + 'restFrom': 'rest', + 'spreadFrom': 'spread', + 'trimChars': 'trim', + 'trimCharsEnd': 'trimEnd', + 'trimCharsStart': 'trimStart', + 'zipAll': 'zip' + }; + + /** Used to track methods that skip fixing their arity. */ + exports.skipFixed = { + 'castArray': true, + 'flow': true, + 'flowRight': true, + 'iteratee': true, + 'mixin': true, + 'rearg': true, + 'runInContext': true + }; + + /** Used to track methods that skip rearranging arguments. */ + exports.skipRearg = { + 'add': true, + 'assign': true, + 'assignIn': true, + 'bind': true, + 'bindKey': true, + 'concat': true, + 'difference': true, + 'divide': true, + 'eq': true, + 'gt': true, + 'gte': true, + 'isEqual': true, + 'lt': true, + 'lte': true, + 'matchesProperty': true, + 'merge': true, + 'multiply': true, + 'overArgs': true, + 'partial': true, + 'partialRight': true, + 'propertyOf': true, + 'random': true, + 'range': true, + 'rangeRight': true, + 'subtract': true, + 'zip': true, + 'zipObject': true, + 'zipObjectDeep': true + }; + + +/***/ }, +/* 3 */ +/***/ function(module, exports) { + + /** + * The default argument placeholder value for methods. + * + * @type {Object} + */ + module.exports = {}; + + +/***/ } +/******/ ]) +}); +; if (!String.prototype.endsWith) { String.prototype.endsWith = function (searchString, position) { var subjectString = this.toString(); @@ -31246,7 +32299,7 @@ define('text!zh',[],function () { return '{\n "domain": "converse",\n "local */ /*global define */ (function (root, factory) { - define("locales", ['jed', + define('locales',['jed', 'text!af', 'text!ca', 'text!de', @@ -38048,6 +39101,18 @@ return __p return utils.detectLocale(isSupportedByLibrary) || 'en'; }; + utils.isOfType = function (type, item) { + return item.get('type') == type; + } + + utils.isInstance = function (type, item) { + return item instanceof type; + }; + + utils.getAttribute = function (key, item) { + return item.get(key); + }; + utils.contains.not = function (attr, query) { return function (item) { return !(utils.contains(attr, query)(item)); @@ -44456,19 +45521,20 @@ require(["strophe-polyfill"]); // overriding method is called. This is done to enable // chaining of plugin methods, all the way up to the // original method. - wrappedOverride: function wrappedOverride(key, value, super_method) { + wrappedOverride: function wrappedOverride(key, value, super_method, default_super) { if (typeof super_method === "function") { if (typeof this.__super__ === "undefined") { /* We're not on the context of the plugged object. - * This can happen when the overridden method is called via - * an event handler. In this case, we simply tack on the - * __super__ obj. - */ - this.__super__ = {}; + * This can happen when the overridden method is called via + * an event handler or when it's a constructor. + * + * In this case, we simply tack on the __super__ obj. + */ + this.__super__ = default_super; } this.__super__[key] = super_method.bind(this); } - return value.apply(this, _.drop(arguments, 3)); + return value.apply(this, _.drop(arguments, 4)); }, // `_overrideAttribute` overrides an attribute on the original object @@ -44489,7 +45555,10 @@ require(["strophe-polyfill"]); _overrideAttribute: function _overrideAttribute(key, plugin) { var value = plugin.overrides[key]; if (typeof value === "function") { - var wrapped_function = _.partial(this.wrappedOverride, key, value, this.plugged[key]); + var default_super = {}; + default_super[this.name] = this.plugged; + + var wrapped_function = _.partial(this.wrappedOverride, key, value, this.plugged[key], default_super); this.plugged[key] = wrapped_function; } else { this.plugged[key] = value; @@ -44511,7 +45580,10 @@ require(["strophe-polyfill"]); // overriding method is called. This is done to enable // chaining of plugin methods, all the way up to the // original method. - var wrapped_function = _.partial(that.wrappedOverride, key, value, obj.prototype[key]); + var default_super = {}; + default_super[that.name] = that.plugged; + + var wrapped_function = _.partial(that.wrappedOverride, key, value, obj.prototype[key], default_super); obj.prototype[key] = wrapped_function; } else { obj.prototype[key] = value; @@ -47224,6 +48296,7 @@ return Backbone.BrowserStorage; define('converse-core',["sizzle", "jquery.noconflict", "lodash.noconflict", + "lodash.converter", "polyfill", "utils", "moment_with_locales", @@ -47234,12 +48307,18 @@ return Backbone.BrowserStorage; "backbone.browserStorage", "backbone.overview", ], factory); -}(this, function (sizzle, $, _, polyfill, utils, moment, Strophe, pluggable, Backbone) { +}(this, function ( + sizzle, $, _, lodashConverter, polyfill, + utils, moment, Strophe, pluggable, Backbone) { + /* Cannot use this due to Safari bug. * See https://github.com/jcbrand/converse.js/issues/196 */ // "use strict"; + // Create the FP (functional programming) version of lodash + var fp = lodashConverter(_.runInContext()); + // Strophe globals var $build = Strophe.$build; var $iq = Strophe.$iq; @@ -47253,8 +48332,9 @@ return Backbone.BrowserStorage; * config of requirejs-tpl in main.js). This one is for normal inline templates. */ _.templateSettings = { - evaluate : /\{\[([\s\S]+?)\]\}/g, - interpolate : /\{\{([\s\S]+?)\}\}/g + 'escape': /\{\{\{([\s\S]+?)\}\}\}/g, + 'evaluate': /\{\[([\s\S]+?)\]\}/g, + 'interpolate': /\{\{([\s\S]+?)\}\}/g }; var _converse = {}; @@ -47293,6 +48373,7 @@ return Backbone.BrowserStorage; 'converse-otr', 'converse-ping', 'converse-register', + 'converse-roomslist', 'converse-rosterview', 'converse-vcard' ]; @@ -47398,9 +48479,11 @@ return Backbone.BrowserStorage; 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: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'); // Instance level constants @@ -47732,26 +48815,21 @@ return Backbone.BrowserStorage; } }; - this.updateMsgCounter = function () { - if (this.msg_counter > 0) { - if (document.title.search(/^Messages \(\d+\) /) === -1) { - document.title = "Messages (" + this.msg_counter + ") " + document.title; - } else { - document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") "); - } - } else if (document.title.search(/^Messages \(\d+\) /) !== -1) { - document.title = document.title.replace(/^Messages \(\d+\) /, ""); - } - }; - this.incrementMsgCounter = function () { this.msg_counter += 1; - this.updateMsgCounter(); + 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; - this.updateMsgCounter(); + if (document.title.search(/^Messages \(\d+\) /) !== -1) { + document.title = document.title.replace(/^Messages \(\d+\) /, ""); + } }; this.initStatus = function () { @@ -47818,6 +48896,7 @@ return Backbone.BrowserStorage; _converse.clearMsgCounter(); } _converse.windowState = state; + _converse.emit('windowStateChanged', {state: state}) }; this.registerGlobalEventHandlers = function () { @@ -47986,6 +49065,17 @@ return Backbone.BrowserStorage; this.RosterContact = Backbone.Model.extend({ + defaults: { + 'bookmarked': false, + 'chat_state': undefined, + 'chat_status': 'offline', + 'groups': [], + 'image': DEFAULT_IMAGE, + 'image_type': DEFAULT_IMAGE_TYPE, + 'num_unread': 0, + 'status': '', + }, + initialize: function (attributes) { var jid = attributes.jid; var bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(); @@ -47995,13 +49085,8 @@ return Backbone.BrowserStorage; 'id': bare_jid, 'jid': bare_jid, 'fullname': bare_jid, - 'chat_status': 'offline', 'user_id': Strophe.getNodeFromJid(jid), - 'resources': resource ? {'resource':0} : {}, - 'groups': [], - 'image_type': DEFAULT_IMAGE_TYPE, - 'image': DEFAULT_IMAGE, - 'status': '' + 'resources': resource ? {resource :0} : {}, }, attributes)); this.on('destroy', function () { this.removeFromRoster(); }.bind(this)); @@ -48561,6 +49646,14 @@ return Backbone.BrowserStorage; this.ChatBox = Backbone.Model.extend({ + defaults: { + 'type': 'chatbox', + 'bookmarked': false, + 'chat_state': undefined, + 'num_unread': 0, + 'url': '' + }, + initialize: function () { this.messages = new _converse.Messages(); this.messages.browserStorage = new Backbone.BrowserStorage[_converse.message_storage]( @@ -48569,10 +49662,7 @@ return Backbone.BrowserStorage; // 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')), - 'chat_state': undefined, - 'num_unread': this.get('num_unread') || 0, 'time_opened': this.get('time_opened') || moment().valueOf(), - 'url': '', 'user_id' : Strophe.getNodeFromJid(this.get('jid')) }); }, @@ -48627,13 +49717,54 @@ return Backbone.BrowserStorage; createMessage: function (message, delay, original_stanza) { return this.messages.create(this.getMessageAttributes.apply(this, arguments)); + }, + + isNewMessage: function (stanza) { + /* Given a message stanza, determine whether it's a new + * message, i.e. not an archived one. + */ + return !(sizzle('result[xmlns="'+Strophe.NS.MAM+'"]', stanza).length); + }, + + newMessageWillBeHidden: function () { + /* 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 (stanza) { + /* Given a newly received message, update the unread counter if + * necessary. + */ + if (_.isNull(stanza.querySelector('body'))) { + return; // The message has no text + } + if (this.isNewMessage(stanza) && this.newMessageWillBeHidden()) { + this.save({'num_unread': this.get('num_unread') + 1}); + _converse.incrementMsgCounter(); + } + }, + + clearUnreadMsgCounter: function() { + this.save({'num_unread': 0}); + }, + + isScrolledUp: function () { + return this.get('scrolled', true); } }); this.ChatBoxes = Backbone.Collection.extend({ - model: _converse.ChatBox, comparator: 'time_opened', + model: function (attrs, options) { + return new _converse.ChatBox(attrs, options); + }, + registerMessageHandler: function () { _converse.connection.addHandler(this.onMessage.bind(this), null, 'message', 'chat'); _converse.connection.addHandler(this.onErrorMessage.bind(this), null, 'message', 'error'); @@ -48690,8 +49821,8 @@ return Backbone.BrowserStorage; * stanzas. */ var original_stanza = message, - contact_jid, forwarded, delay, from_bare_jid, - from_resource, is_me, msgid, + contact_jid, delay, from_bare_jid, + from_resource, is_me, msgid, messages, chatbox, resource, from_jid = message.getAttribute('from'), to_jid = message.getAttribute('to'), @@ -48714,7 +49845,7 @@ return Backbone.BrowserStorage; ); return true; } - forwarded = message.querySelector('forwarded'); + var forwarded = message.querySelector('forwarded'); if (!_.isNull(forwarded)) { var forwarded_message = forwarded.querySelector('message'); var forwarded_from = forwarded_message.getAttribute('from'); @@ -48732,7 +49863,6 @@ return Backbone.BrowserStorage; from_bare_jid = Strophe.getBareJidFromJid(from_jid); from_resource = Strophe.getResourceFromJid(from_jid); is_me = from_bare_jid === _converse.bare_jid; - msgid = message.getAttribute('id'); if (is_me) { // I am the sender, so this must be a forwarded message... contact_jid = Strophe.getBareJidFromJid(to_jid); @@ -48741,19 +49871,55 @@ return Backbone.BrowserStorage; contact_jid = from_bare_jid; resource = from_resource; } - _converse.emit('message', original_stanza); // Get chat box, but only create a new one when the message has a body. chatbox = this.getChatBox(contact_jid, !_.isNull(message.querySelector('body'))); - if (!chatbox) { - return true; + msgid = message.getAttribute('id'); + if (chatbox) { + 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); + } } - if (msgid && chatbox.messages.findWhere({msgid: msgid})) { - return true; // We already have this message stored. - } - chatbox.createMessage(message, delay, original_stanza); + _converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox}); return true; }, + createChatBox: function (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); + var roster_info = {}; + var roster_item = _converse.roster.get(bare_jid); + 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', 'error'); + return; + } + return this.create(_.assignIn({ + 'id': bare_jid, + 'jid': bare_jid, + 'fullname': jid, + 'image_type': DEFAULT_IMAGE_TYPE, + 'image': DEFAULT_IMAGE, + 'url': '', + }, roster_info, attrs || {})); + }, + getChatBox: function (jid, create, attrs) { /* Returns a chat box or optionally return a newly * created one if one doesn't exist. @@ -48764,31 +49930,9 @@ return Backbone.BrowserStorage; * (Object) attrs - Optional chat box atributes. */ jid = jid.toLowerCase(); - var bare_jid = Strophe.getBareJidFromJid(jid); - var chatbox = this.get(bare_jid); + var chatbox = this.get(Strophe.getBareJidFromJid(jid)); if (!chatbox && create) { - var roster_info = {}; - var roster_item = _converse.roster.get(bare_jid); - 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', 'error'); - return; - } - chatbox = this.create(_.assignIn({ - 'id': bare_jid, - 'jid': bare_jid, - 'fullname': jid, - 'image_type': DEFAULT_IMAGE_TYPE, - 'image': DEFAULT_IMAGE, - 'url': '', - }, roster_info, attrs || {})); + chatbox = this.createChatBox(jid, attrs); } return chatbox; } @@ -49080,39 +50224,92 @@ return Backbone.BrowserStorage; xhr.send(); }; - this.attemptPreboundSession = function (reconnecting) { - /* Handle session resumption or initialization when prebind is being used. - */ - if (!reconnecting && this.keepalive) { - if (!this.jid) { - throw new Error("attemptPreboundSession: when using 'keepalive' with 'prebind, "+ - "you must supply the JID of the current user."); - } - try { - return this.connection.restore(this.jid, this.onConnectStatusChanged); - } catch (e) { - this.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) + 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) { + this.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; + } + }; - // No keepalive, or session resumption has failed. - if (!reconnecting && this.jid && this.sid && this.rid) { - return this.connection.attach(this.jid, this.sid, this.rid, this.onConnectStatusChanged); - } else if (this.prebind_url) { + 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, "+ + 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 (this.auto_login) { + if (credentials) { + // When credentials are passed in, they override prebinding + // or credentials fetching via HTTP + this.autoLogin(credentials); + } else if (this.credentials_url) { + this.fetchLoginCredentials().done(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, then they come from credentials_url, so we - // set them on the _converse object. + // If passed in, the credentials come from credentials_url, + // so we set them on the converse object. this.jid = credentials.jid; - this.password = credentials.password; } if (this.authentication === _converse.ANONYMOUS) { if (!this.jid) { @@ -49124,9 +50321,9 @@ return Backbone.BrowserStorage; this.connection.reset(); this.connection.connect(this.jid.toLowerCase(), null, this.onConnectStatusChanged); } else if (this.authentication === _converse.LOGIN) { - var password = _converse.connection.pass || this.password; + var password = _.isNil(credentials) ? (_converse.connection.pass || this.password) : credentials.password; if (!password) { - if (this.auto_login && !this.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."); } @@ -49145,44 +50342,6 @@ return Backbone.BrowserStorage; } }; - 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 (this.keepalive && !reconnecting) { - try { - return this.connection.restore(this.jid, this.onConnectStatusChanged); - } catch (e) { - this.log("Could not restore session. Error message: "+e.message); - this.clearSession(); // If there's a roster, we want to clear it (see #555) - } - } - if (this.auto_login) { - if (credentials) { - // When credentials are passed in, they override prebinding - // or credentials fetching via HTTP - this.autoLogin(credentials); - } else if (this.credentials_url) { - this.fetchLoginCredentials().done(this.autoLogin.bind(this)); - } else if (!this.jid) { - throw new Error( - "initConnection: 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 { - // Probably ANONYMOUS login - this.autoLogin(); - } - } else if (reconnecting) { - this.autoLogin(); - } - }; - 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. @@ -49487,6 +50646,7 @@ return Backbone.BrowserStorage; 'Backbone': Backbone, 'Strophe': Strophe, '_': _, + 'fp': fp, 'b64_sha1': b64_sha1, 'jQuery': $, 'moment': moment, @@ -49502,7 +50662,7 @@ obj || (obj = {}); var __t, __p = '', __e = _.escape, __j = Array.prototype.join; function print() { __p += __j.call(arguments, '') } with (obj) { -__p += '
\n
\n
\n
\n
\n \n
\n \n
\n '; if (url) { ; @@ -49642,7 +50802,7 @@ __e(label_start_call) + } ; __p += '\n'; if (show_clear_button) { ; -__p += '\n
  • \n'; } ; @@ -49657,7 +50817,11 @@ define('tpl!avatar', ['lodash'], function(_) {return function(obj) { obj || (obj = {}); var __t, __p = ''; with (obj) { -__p += '\n'; +__p += '\n'; } return __p @@ -49751,6 +50915,15 @@ return __p }, }); + var onWindowStateChanged = function (data) { + var state = data.state; + _converse.chatboxviews.each(function (chatboxview) { + chatboxview.onWindowStateChanged(state); + }) + }; + + _converse.api.listen.on('windowStateChanged', onWindowStateChanged); + _converse.ChatBoxView = Backbone.View.extend({ length: 200, tagName: 'div', @@ -50074,29 +51247,12 @@ return __p return !this.$el.is(':visible'); }, - updateNewMessageIndicators: function (message) { - /* We have two indicators of new messages. The unread messages - * counter, which shows the number of unread messages in - * the document.title, and the "new messages" indicator in - * a chat area, if it's scrolled up so that new messages - * aren't visible. - * - * In both cases we ignore MAM messages. - */ - if (!message.get('archive_id')) { - if (this.model.get('scrolled', true)) { - this.$el.find('.new-msgs-indicator').removeClass('hidden'); - } - if (_converse.windowState === 'hidden' || this.model.get('scrolled', true)) { - _converse.incrementMsgCounter(); - } - } - }, - handleTextMessage: function (message) { this.showMessage(_.clone(message.attributes)); if (message.get('sender') !== 'me') { - this.updateNewMessageIndicators(message); + if (!message.get('archive_id') && this.model.get('scrolled', true)) { + this.$el.find('.new-msgs-indicator').removeClass('hidden'); + } } else { // We remove the "scrolled" flag so that the chat area // gets scrolled down. We always want to scroll down @@ -50136,6 +51292,10 @@ return __p } else { this.handleTextMessage(message); } + _converse.emit('messageAdded', { + 'message': message, + 'chatbox': this.model + }); }, createMessageStanza: function (message) { @@ -50446,7 +51606,7 @@ return __p }, afterShown: function (focus) { - if (_converse.connection.connected) { + if (this.model.collection.browserStorage) { // Without a connection, we haven't yet initialized // localstorage this.model.save(); @@ -50505,8 +51665,8 @@ return __p (this.$content.scrollTop() + this.$content.innerHeight()) >= this.$content[0].scrollHeight-10; if (is_at_bottom) { - this.hideNewMessagesIndicator(); this.model.save('scrolled', false); + this.onScrolledDown(); } else { // We're not at the bottom of the chat area, so we mark // that the box is in a scrolled-up state. @@ -50523,11 +51683,19 @@ return __p /* Inner method that gets debounced */ if (this.$content.is(':visible') && !this.model.get('scrolled')) { this.$content.scrollTop(this.$content[0].scrollHeight); - this.hideNewMessagesIndicator(); + this.onScrolledDown(); this.model.save({'auto_scrolled': true}); } }, + onScrolledDown: function() { + this.hideNewMessagesIndicator(); + if (_converse.windowState !== 'hidden') { + this.model.clearUnreadMsgCounter(); + } + _converse.emit('chatBoxScrolledDown', {'chatbox': this.model}); + }, + scrollDown: function () { if (_.isUndefined(this.debouncedScrollDown)) { /* We wrap the method in a debouncer and set it on the @@ -50538,6 +51706,12 @@ return __p } this.debouncedScrollDown.apply(this, arguments); return this; + }, + + onWindowStateChanged: function (state) { + if (this.model.get('num_unread', 0) && !this.model.newMessageWillBeHidden()) { + this.model.clearUnreadMsgCounter(); + } } }); } @@ -50581,13 +51755,13 @@ define('tpl!change_status_message', ['lodash'], function(_) {return function(obj obj || (obj = {}); var __t, __p = '', __e = _.escape; with (obj) { -__p += '
    \n
    \n \n \n \n \n \n
    \n
    \n'; +'"/>\n \n\n'; } return __p @@ -50633,25 +51807,25 @@ obj || (obj = {}); var __t, __p = '', __e = _.escape, __j = Array.prototype.join; function print() { __p += __j.call(arguments, '') } with (obj) { -__p += '
    \n \n \n \n \n \n \n \n '; +'\n '; if (include_offline_state) { ; -__p += '\n \n '; +'\n '; } ; -__p += '\n '; +__p += '\n '; if (allow_logout) { ; -__p += '\n \n '; +'\n '; } ; -__p += '\n \n \n
    \n'; +__p += '\n \n\n'; } return __p @@ -50663,13 +51837,23 @@ obj || (obj = {}); var __t, __p = '', __e = _.escape, __j = Array.prototype.join; function print() { __p += __j.call(arguments, '') } with (obj) { -__p += '
  • \n'; +'\n '; + if (num_unread) { ; +__p += '\n ' + +__e( num_unread ) + +'\n '; + } ; +__p += '\n\n'; } return __p @@ -50681,7 +51865,7 @@ obj || (obj = {}); var __t, __p = '', __j = Array.prototype.join; function print() { __p += __j.call(arguments, '') } with (obj) { -__p += '
    \n
    \n
    \n
    \n
    \n
      \n '; +__p += '
      \n
      \n
        \n '; if (!sticky_controlbox) { ; __p += '\n \n '; } ; @@ -50883,7 +52067,7 @@ define('tpl!roster', ['lodash'], function(_) {return function(obj) { obj || (obj = {}); var __t, __p = ''; with (obj) { -__p += '\n'; +__p += '
        \n'; } return __p @@ -50897,18 +52081,20 @@ function print() { __p += __j.call(arguments, '') } with (obj) { __p += '
        \n \n \n \n
        \n
        \n'; +'"/>\n \n\n
        \n
        \n
        \n'; } return __p @@ -53789,8 +55085,10 @@ define("awesomplete", (function (global) { tpl_room_panel, Awesomplete ) { + "use strict"; var ROOMS_PANEL_ID = 'chatrooms'; + var CHATROOMS_TYPE = 'chatroom'; // Strophe methods for building stanzas var Strophe = converse.env.Strophe, @@ -53805,6 +55103,7 @@ define("awesomplete", (function (global) { // Other necessary globals var $ = converse.env.jQuery, _ = converse.env._, + fp = converse.env.fp, moment = converse.env.moment; // Add Strophe Namespaces @@ -53878,25 +55177,42 @@ define("awesomplete", (function (global) { } }, + ChatBoxes: { + model: function (attrs, options) { + var _converse = this.__super__._converse; + if (attrs.type == CHATROOMS_TYPE) { + return new _converse.ChatRoom(attrs, options); + } else { + return this.__super__.model.apply(this, arguments); + } + }, + }, + ControlBoxView: { + renderRoomsPanel: function () { + var _converse = this.__super__._converse; + this.roomspanel = new _converse.RoomsPanel({ + '$parent': this.$el.find('.controlbox-panes'), + 'model': new (Backbone.Model.extend({ + id: b64_sha1('converse.roomspanel'+_converse.bare_jid), // Required by sessionStorage + browserStorage: new Backbone.BrowserStorage[_converse.storage]( + b64_sha1('converse.roomspanel'+_converse.bare_jid)) + }))() + }); + this.roomspanel.insertIntoDOM().model.fetch(); + if (!this.roomspanel.model.get('nick')) { + this.roomspanel.model.save({ + nick: Strophe.getNodeFromJid(_converse.bare_jid) + }); + } + _converse.emit('roomsPanelRendered'); + }, + renderContactsPanel: function () { var _converse = this.__super__._converse; this.__super__.renderContactsPanel.apply(this, arguments); if (_converse.allow_muc) { - this.roomspanel = new _converse.RoomsPanel({ - '$parent': this.$el.find('.controlbox-panes'), - 'model': new (Backbone.Model.extend({ - id: b64_sha1('converse.roomspanel'+_converse.bare_jid), // Required by sessionStorage - browserStorage: new Backbone.BrowserStorage[_converse.storage]( - b64_sha1('converse.roomspanel'+_converse.bare_jid)) - }))() - }); - this.roomspanel.render().model.fetch(); - if (!this.roomspanel.model.get('nick')) { - this.roomspanel.model.save({ - nick: Strophe.getNodeFromJid(_converse.bare_jid) - }); - } + this.renderRoomsPanel(); } }, @@ -53942,7 +55258,7 @@ define("awesomplete", (function (global) { onChatBoxAdded: function (item) { var _converse = this.__super__._converse; var view = this.get(item.get('id')); - if (!view && item.get('type') === 'chatroom') { + if (!view && item.get('type') === CHATROOMS_TYPE) { view = new _converse.ChatRoomView({'model': item}); return this.add(item.get('id'), view); } else { @@ -54054,7 +55370,11 @@ define("awesomplete", (function (global) { }, }); - _converse.createChatRoom = function (settings) { + _.extend(_converse.promises, { + 'roomsPanelRendered': new $.Deferred() + }); + + _converse.openChatRoom = function (settings) { /* Creates a new chat room, making sure that certain attributes * are correct, for example that the "type" is set to * "chatroom". @@ -54070,11 +55390,64 @@ define("awesomplete", (function (global) { 'description': '', 'features_fetched': false, 'roomconfig': {}, - 'type': 'chatroom', + 'type': CHATROOMS_TYPE, }, settings) ); }; + _converse.ChatRoom = _converse.ChatBox.extend({ + + defaults: function () { + return _.extend(_.clone(_converse.ChatBox.prototype.defaults), { + 'type': CHATROOMS_TYPE, + // For group chats, we distinguish between generally unread + // messages and those ones that specifically mention the + // user. + // + // To keep things simple, we reuse `num_unread` from + // _converse.ChatBox to indicate unread messages which + // mention the user and `num_unread_general` to indicate + // generally unread messages (which *includes* mentions!). + 'num_unread_general': 0 + }); + }, + + isUserMentioned: function (message) { + /* Returns a boolean to indicate whether the current user + * was mentioned in a message. + * + * Parameters: + * (String): The text message + */ + return (new RegExp("\\b"+this.get('nick')+"\\b")).test(message); + }, + + incrementUnreadMsgCounter: function (stanza) { + /* Given a newly received message, update the unread counter if + * necessary. + * + * Parameters: + * (XMLElement): The stanza + */ + var body = stanza.querySelector('body') + if (_.isNull(body)) { + return; // The message has no text + } + if (this.isNewMessage(stanza) && this.newMessageWillBeHidden()) { + this.save({'num_unread_general': this.get('num_unread_general') + 1}); + if (this.isUserMentioned(body.textContent)) { + this.save({'num_unread': this.get('num_unread') + 1}); + _converse.incrementMsgCounter(); + } + } + }, + + clearUnreadMsgCounter: function() { + this.save({'num_unread': 0}); + this.save({'num_unread_general': 0}); + } + }); + _converse.ChatRoomView = _converse.ChatBoxView.extend({ /* Backbone View which renders a chat room, based upon the view * for normal one-on-one chat boxes. @@ -54120,7 +55493,7 @@ define("awesomplete", (function (global) { }); } else { this.fetchMessages(); - _converse.emit('chatRoomOpened', that); + _converse.emit('chatRoomOpened', this); } }, @@ -54205,7 +55578,7 @@ define("awesomplete", (function (global) { * * This is instead done in `afterConnected` below. */ - if (_converse.connection.connected) { + if (this.model.collection.browserStorage) { // Without a connection, we haven't yet initialized // localstorage this.model.save(); @@ -54226,8 +55599,7 @@ define("awesomplete", (function (global) { .getExtraMessageClasses.apply(this, arguments); if (this.is_chatroom && attrs.sender === 'them' && - (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(attrs.message) - ) { + this.model.isUserMentioned(attrs.message)) { // Add special class to mark groupchat messages // in which we are mentioned. extra_classes += ' mentioned'; @@ -54885,7 +56257,7 @@ define("awesomplete", (function (global) { }, cleanup: function () { - if (_converse.connection.connected) { + if (this.model.collection.browserStorage) { this.model.save('connection_status', ROOMSTATUS.DISCONNECTED); } else { this.model.set('connection_status', ROOMSTATUS.DISCONNECTED); @@ -55187,14 +56559,14 @@ define("awesomplete", (function (global) { * chat room with it. */ ev.preventDefault(); - var $nick = this.$el.find('input[name=nick]'); - var nick = $nick.val(); + var nick_el = ev.target.nick; + var nick = nick_el.value; if (!nick) { - $nick.addClass('error'); + nick_el.classList.add('error'); return; } else { - $nick.removeClass('error'); + nick_el.classList.remove('error'); } this.$el.find('.chatroom-form-container') .replaceWith(''); @@ -55602,7 +56974,7 @@ define("awesomplete", (function (global) { } } else if (!this.model.get('features_fetched')) { // The features for this room weren't fetched. - // That must mean it's a new room without locking + // That must mean it's a new room without locking // (in which case Prosody doesn't send a 201 status), // otherwise the features would have been fetched in // the "initialize" method already. @@ -55685,10 +57057,14 @@ define("awesomplete", (function (global) { if (sender === '') { return true; } + this.model.incrementUnreadMsgCounter(original_stanza); this.model.createMessage(message, delay, original_stanza); if (sender !== this.model.get('nick')) { // We only emit an event if it's not our own message - _converse.emit('message', original_stanza); + _converse.emit( + 'message', + {'stanza': original_stanza, 'chatbox': this.model} + ); } return true; } @@ -56028,46 +57404,60 @@ define("awesomplete", (function (global) { className: 'controlbox-pane', id: 'chatrooms', events: { - 'submit form.add-chatroom': 'createChatRoom', + 'submit form.add-chatroom': 'openChatRoom', 'click input#show-rooms': 'showRooms', - 'click a.open-room': 'createChatRoom', + 'click a.open-room': 'openChatRoom', 'click a.room-info': 'toggleRoomInfo', 'change input[name=server]': 'setDomain', 'change input[name=nick]': 'setNick' }, initialize: function (cfg) { - this.$parent = cfg.$parent; + this.parent_el = cfg.$parent[0]; + this.tab_el = document.createElement('li'); this.model.on('change:muc_domain', this.onDomainChange, this); this.model.on('change:nick', this.onNickChange, this); + _converse.chatboxes.on('change:num_unread', this.renderTab, this); }, render: function () { - this.$parent.append( - this.$el.html( - tpl_room_panel({ - 'server_input_type': _converse.hide_muc_server && 'hidden' || 'text', - 'server_label_global_attr': _converse.hide_muc_server && ' hidden' || '', - 'label_room_name': __('Room name'), - 'label_nickname': __('Nickname'), - 'label_server': __('Server'), - 'label_join': __('Join Room'), - 'label_show_rooms': __('Show rooms') - }) - )); - this.$tabs = this.$parent.parent().find('#controlbox-tabs'); - + this.el.innerHTML = tpl_room_panel({ + 'server_input_type': _converse.hide_muc_server && 'hidden' || 'text', + 'server_label_global_attr': _converse.hide_muc_server && ' hidden' || '', + 'label_room_name': __('Room name'), + 'label_nickname': __('Nickname'), + 'label_server': __('Server'), + 'label_join': __('Join Room'), + 'label_show_rooms': __('Show rooms') + }); + this.renderTab(); var controlbox = _converse.chatboxes.get('controlbox'); - this.$tabs.append(tpl_chatrooms_tab({ - 'label_rooms': __('Rooms'), - 'is_current': controlbox.get('active-panel') === ROOMS_PANEL_ID - })); if (controlbox.get('active-panel') !== ROOMS_PANEL_ID) { - this.$el.addClass('hidden'); + this.el.classList.add('hidden'); } return this; }, + renderTab: function () { + var controlbox = _converse.chatboxes.get('controlbox'); + var chatrooms = fp.filter( + _.partial(utils.isOfType, CHATROOMS_TYPE), + _converse.chatboxes.models + ); + this.tab_el.innerHTML = tpl_chatrooms_tab({ + 'label_rooms': __('Rooms'), + 'is_current': controlbox.get('active-panel') === ROOMS_PANEL_ID, + 'num_unread': fp.sum(fp.map(fp.curry(utils.getAttribute)('num_unread'), chatrooms)) + }); + }, + + insertIntoDOM: function () { + this.parent_el.appendChild(this.render().el); + this.tabs = this.parent_el.parentNode.querySelector('#controlbox-tabs'); + this.tabs.appendChild(this.tab_el); + return this; + }, + onDomainChange: function (model) { var $server = this.$el.find('input.new-chatroom-server'); $server.val(model.get('muc_domain')); @@ -56213,7 +57603,7 @@ define("awesomplete", (function (global) { } }, - createChatRoom: function (ev) { + openChatRoom: function (ev) { ev.preventDefault(); var name, $name, server, $server, jid; if (ev.type === 'click') { @@ -56236,11 +57626,11 @@ define("awesomplete", (function (global) { return; } } - _converse.createChatRoom({ + _converse.openChatRoom({ 'id': jid, 'jid': jid, 'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)), - 'type': 'chatroom', + 'type': CHATROOMS_TYPE, 'box_id': b64_sha1(jid) }); }, @@ -56290,12 +57680,11 @@ define("awesomplete", (function (global) { } } if (result === true) { - var chatroom = _converse.createChatRoom({ + var chatroom = _converse.openChatRoom({ 'id': room_jid, 'jid': room_jid, 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(room_jid)), - 'nick': Strophe.unescapeNode(Strophe.getNodeFromJid(_converse.connection.jid)), - 'type': 'chatroom', + 'type': CHATROOMS_TYPE, 'box_id': b64_sha1(room_jid), 'password': $x.attr('password') }); @@ -56341,7 +57730,7 @@ define("awesomplete", (function (global) { 'id': jid, 'jid': jid, 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)), - 'type': 'chatroom', + 'type': CHATROOMS_TYPE, 'box_id': b64_sha1(jid) }, attrs))); }; @@ -56383,9 +57772,9 @@ define("awesomplete", (function (global) { if (_.isUndefined(jids)) { throw new TypeError('rooms.open: You need to provide at least one JID'); } else if (_.isString(jids)) { - return _converse.getChatRoom(jids, attrs, _converse.createChatRoom); + return _converse.getChatRoom(jids, attrs, _converse.openChatRoom); } - return _.map(jids, _.partial(_converse.getChatRoom, _, attrs, _converse.createChatRoom)); + return _.map(jids, _.partial(_converse.getChatRoom, _, attrs, _converse.openChatRoom)); }, 'get': function (jids, attrs, create) { if (_.isString(attrs)) { @@ -56396,7 +57785,7 @@ define("awesomplete", (function (global) { if (_.isUndefined(jids)) { var result = []; _converse.chatboxes.each(function (chatbox) { - if (chatbox.get('type') === 'chatroom') { + if (chatbox.get('type') === CHATROOMS_TYPE) { result.push(_converse.getViewForChatBox(chatbox)); } }); @@ -56419,7 +57808,7 @@ define("awesomplete", (function (global) { * all the open chat rooms. */ _converse.chatboxviews.each(function (view) { - if (view.model.get('type') === 'chatroom') { + if (view.model.get('type') === CHATROOMS_TYPE) { view.model.save('connection_status', ROOMSTATUS.DISCONNECTED); view.join(); } @@ -56433,7 +57822,7 @@ define("awesomplete", (function (global) { * when fetched from session storage. */ _converse.chatboxes.each(function (model) { - if (model.get('type') === 'chatroom') { + if (model.get('type') === CHATROOMS_TYPE) { model.save('connection_status', ROOMSTATUS.DISCONNECTED); } }); @@ -56490,23 +57879,30 @@ return __p define('tpl!bookmark', ['lodash'], function(_) {return function(obj) { obj || (obj = {}); -var __t, __p = '', __e = _.escape; +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } with (obj) { -__p += '
        \n \n' + __e(name) + -'\n  \n  \n \n
        \n'; @@ -56519,7 +57915,7 @@ define('tpl!bookmarks_list', ['lodash'], function(_) {return function(obj) { obj || (obj = {}); var __t, __p = '', __e = _.escape; with (obj) { -__p += '' + +__e(label_rooms) + +'\n
        \n'; + +} +return __p +};}); + + +define('tpl!rooms_list_item', ['lodash'], function(_) {return function(obj) { +obj || (obj = {}); +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '
        \n'; + if (num_unread) { ; +__p += '\n ' + +__e( num_unread ) + +'\n'; + } ; +__p += '\n' + +__e(name) + +'\n \n \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 */ + +/* This is a non-core Converse.js plugin which shows a list of currently open + * rooms in the "Rooms Panel" of the ControlBox. + */ +(function (root, factory) { + define('converse-roomslist',["utils", + "converse-core", + "converse-muc", + "tpl!rooms_list", + "tpl!rooms_list_item" + ], factory); +}(this, function (utils, converse, muc, tpl_rooms_list, tpl_rooms_list_item) { + var $ = converse.env.jQuery, + Backbone = converse.env.Backbone, + b64_sha1 = converse.env.b64_sha1, + sizzle = converse.env.sizzle, + _ = converse.env._; + + converse.plugins.add('converse-roomslist', { + initialize: function () { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + var _converse = this._converse, + __ = _converse.__, + ___ = _converse.___; + + _converse.RoomsList = Backbone.Model.extend({ + defaults: { + "toggle-state": _converse.OPENED + }, + }); + + _converse.RoomsListView = Backbone.View.extend({ + tagName: 'div', + className: 'open-rooms-list rooms-list-container', + events: { + 'click .close-room': 'closeRoom', + 'click .open-rooms-toggle': 'toggleRoomsList' + }, + + initialize: function () { + this.model.on('add', this.renderRoomsListElement, this); + this.model.on('change:bookmarked', this.renderRoomsListElement, this); + this.model.on('change:name', this.renderRoomsListElement, this); + this.model.on('change:num_unread', this.renderRoomsListElement, this); + this.model.on('change:num_unread_general', this.renderRoomsListElement, this); + this.model.on('remove', this.removeRoomsListElement, this); + + var cachekey = 'converse.roomslist'+_converse.bare_jid; + this.list_model = new _converse.RoomsList(); + this.list_model.id = cachekey; + this.list_model.browserStorage = new Backbone.BrowserStorage[_converse.storage]( + b64_sha1(cachekey) + ); + this.list_model.fetch(); + this.render(); + }, + + render: function () { + this.el.innerHTML = + tpl_rooms_list({ + 'toggle_state': this.list_model.get('toggle-state'), + 'desc_rooms': __('Click to toggle the rooms list'), + 'label_rooms': __('Open Rooms') + }) + this.hide(); + if (this.list_model.get('toggle-state') !== _converse.OPENED) { + this.$('.open-rooms-list').hide(); + } + this.model.each(this.renderRoomsListElement.bind(this)); + var controlboxview = _converse.chatboxviews.get('controlbox'); + + if (!_.isUndefined(controlboxview) && + !document.body.contains(this.el)) { + var container = controlboxview.el.querySelector('#chatrooms'); + if (!_.isNull(container)) { + container.insertBefore(this.el, container.firstChild); + } + } + return this.el; + }, + + hide: function () { + this.el.classList.add('hidden'); + }, + + show: function () { + this.el.classList.remove('hidden'); + }, + + closeRoom: function (ev) { + ev.preventDefault(); + var name = $(ev.target).data('roomName'); + var jid = $(ev.target).data('roomJid'); + if (confirm(__(___("Are you sure you want to leave the room \"%1$s\"?"), name))) { + _converse.chatboxviews.get(jid).leave(); + } + }, + + renderRoomsListElement: function (item) { + if (item.get('type') !== 'chatroom') { + return; + } + this.removeRoomsListElement(item); + + var name, bookmark + if (item.get('bookmarked')) { + bookmark = _.head(_converse.bookmarksview.model.where({'jid': item.get('jid')})); + name = bookmark.get('name'); + } else { + name = item.get('name'); + } + var div = document.createElement('div'); + div.innerHTML = tpl_rooms_list_item(_.extend(item.toJSON(), { + 'info_leave_room': __('Leave this room'), + 'info_remove_bookmark': __('Unbookmark this room'), + 'info_title': __('Show more information on this room'), + 'name': name, + 'open_title': __('Click to open this room') + })); + this.el.querySelector('.open-rooms-list').appendChild(div.firstChild); + this.show(); + }, + + removeRoomsListElement: function (item) { + var list_el = this.el.querySelector('.open-rooms-list'); + var el = _.head(sizzle('.available-chatroom[data-room-jid="'+item.get('jid')+'"]', list_el)); + if (el) { + list_el.removeChild(el); + } + if (list_el.childElementCount === 0) { + this.hide(); + } + }, + + toggleRoomsList: function (ev) { + if (ev && ev.preventDefault) { ev.preventDefault(); } + var el = ev.target; + if (el.classList.contains("icon-opened")) { + this.$('.open-rooms-list').slideUp('fast'); + this.list_model.save({'toggle-state': _converse.CLOSED}); + el.classList.remove("icon-opened"); + el.classList.add("icon-closed"); + } else { + el.classList.remove("icon-closed"); + el.classList.add("icon-opened"); + this.$('.open-rooms-list').slideDown('fast'); + this.list_model.save({'toggle-state': _converse.OPENED}); + } + } + }); + + var initRoomsListView = function () { + _converse.rooms_list_view = new _converse.RoomsListView( + {'model': _converse.chatboxes} + ); + }; + _converse.on('bookmarksInitialized', initRoomsListView); + _converse.on('roomsPanelRendered', function () { + if (_converse.allow_bookmarks) { + return; + } + initRoomsListView(); + }); + + var afterReconnection = function () { + if (_.isUndefined(_converse.rooms_list_view)) { + initRoomsListView(); + } else { + _converse.rooms_list_view.render(); + } + }; + _converse.api.listen.on('reconnected', afterReconnection); + } + }); +})); + // http://xmpp.org/extensions/xep-0059.html (function (root, factory) { @@ -57103,9 +58796,6 @@ Strophe.RSM.prototype = { // XEP-0313 Message Archive Management var MAM_ATTRIBUTES = ['with', 'start', 'end']; - Strophe.addNamespace('MAM', 'urn:xmpp:mam:0'); - Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm'); - converse.plugins.add('converse-mam', { overrides: { @@ -65631,11 +67321,7 @@ return __p return result; }, - renderLoginPanel: function () { - /* Also render a registration panel, when rendering the - * login panel. - */ - this.__super__.renderLoginPanel.apply(this, arguments); + renderRegistrationPanel: function () { var _converse = this.__super__._converse; if (_converse.allow_registration) { this.registerpanel = new _converse.RegisterPanel({ @@ -65645,6 +67331,15 @@ return __p this.registerpanel.render().$el.addClass('hidden'); } return this; + }, + + renderLoginPanel: function () { + /* Also render a registration panel, when rendering the + * login panel. + */ + this.__super__.renderLoginPanel.apply(this, arguments); + this.renderRegistrationPanel(); + return this; } } }, @@ -66556,10 +68251,11 @@ return __p } }; - _converse.handleMessageNotification = function (message) { + _converse.handleMessageNotification = function (data) { /* Event handler for the on('message') event. Will call methods * to play sounds and show HTML5 notifications. */ + var message = data.stanza; if (!_converse.shouldNotifyOfMessage(message)) { return false; } @@ -66626,9 +68322,9 @@ __p += __e(Minimized) + ' (' + __e(num_minimized) + -')\n\n\n - - + + - + diff --git a/inverse.html b/inverse.html index 2e97fa01c..ddcb91106 100644 --- a/inverse.html +++ b/inverse.html @@ -6,9 +6,9 @@ inVerse - - - + + +
        @@ -21,19 +21,20 @@
        diff --git a/src/build-inverse.js b/src/build-inverse.js new file mode 100644 index 000000000..94db45ee0 --- /dev/null +++ b/src/build-inverse.js @@ -0,0 +1,9 @@ +({ + baseUrl: "../", + name: "almond", + mainConfigFile: 'config.js', + wrap: { + startFile: "start.frag", + endFile: "inverse-end.frag" + } +}) diff --git a/src/config.js b/src/config.js index a1cc0e629..a27bb2dec 100644 --- a/src/config.js +++ b/src/config.js @@ -36,6 +36,7 @@ require.config({ "strophe.rsm": "node_modules/strophejs-plugin-rsm/strophe.rsm", "strophe.vcard": "node_modules/strophejs-plugin-vcard/strophe.vcard", "text": "node_modules/text/text", + "tpl": "node_modules/lodash-template-loader/loader", "typeahead": "components/typeahead.js/index", "lodash": "node_modules/lodash/lodash", "lodash.converter": "3rdparty/lodash.fp", @@ -104,10 +105,6 @@ require.config({ // or node_modules, depending on how moment was installed. 'location': 'node_modules/moment', 'main': 'moment' - }, { - 'name': 'tpl', - 'location': 'node_modules/lodash-template-loader', - 'main': 'loader' }], map: { diff --git a/src/converse-inverse.js b/src/converse-inverse.js index b40b37ffb..1259dc3e1 100644 --- a/src/converse-inverse.js +++ b/src/converse-inverse.js @@ -102,7 +102,6 @@ var _converse = this._converse; this.updateSettings({ - blacklisted_plugins: ['converse-minimize', 'converse-dragresize'], chatview_avatar_height: 44, chatview_avatar_width: 44, hide_open_bookmarks: true, diff --git a/src/converse.js b/src/converse.js index 91bb88c8f..355919398 100755 --- a/src/converse.js +++ b/src/converse.js @@ -14,7 +14,7 @@ if (typeof define !== 'undefined') { "converse-chatview", // Renders standalone chat boxes for single user chat "converse-controlbox", // The control box "converse-bookmarks", // XEP-0048 Bookmarks - "converse-roomslist", // XEP-0048 Bookmarks + "converse-roomslist", // Show currently open chat rooms "converse-mam", // XEP-0313 Message Archive Management "converse-muc", // XEP-0045 Multi-user chat "converse-vcard", // XEP-0054 VCard-temp diff --git a/src/inverse-end.frag b/src/inverse-end.frag new file mode 100644 index 000000000..9a5e02a92 --- /dev/null +++ b/src/inverse-end.frag @@ -0,0 +1,8 @@ +/* jshint ignore:start */ + //The modules for your project will be inlined above + //this snippet. Ask almond to synchronously require the + //module value for 'converse' here and return it as the + //value to use for the public API for the built file. + return require('inverse'); +})); +/* jshint ignore:end */ diff --git a/src/inverse.js b/src/inverse.js index df2992b88..c1bcff179 100644 --- a/src/inverse.js +++ b/src/inverse.js @@ -1,16 +1,16 @@ -/* Converse.js components configuration +/* Inverse.js components configuration * * This file is used to tell require.js which components (or plugins) to load - * when it generates a build. + * when it generates a build of inverse.js (in dist/inverse.js) */ if (typeof define !== 'undefined') { - /* When running tests, define is not defined. */ - define("inverse", [ + // The section below determines which plugins will be included in a build + define([ "converse-core", /* START: Removable components - * -------------------- - * Any of the following components may be removed if they're not needed. - */ + * -------------------- + * Any of the following components may be removed if they're not needed. + */ "converse-chatview", // Renders standalone chat boxes for single user chat "converse-controlbox", // The control box "converse-bookmarks", // XEP-0048 Bookmarks @@ -23,14 +23,10 @@ if (typeof define !== 'undefined') { "converse-ping", // XEP-0199 XMPP Ping "converse-notification",// HTML5 Notifications "converse-headline", // Support for headline messages - - "converse-inverse", // Inverse plugin for converse.js /* END: Removable components */ + "converse-inverse", // Inverse plugin for converse.js ], function(converse) { - var $ = converse.env.jQuery; - window.converse = converse; - $(window).trigger('converse-loaded', converse); return converse; }); } diff --git a/src/locales.js b/src/locales.js index 6f82a46ff..d42cf0321 100755 --- a/src/locales.js +++ b/src/locales.js @@ -8,7 +8,7 @@ */ /*global define */ (function (root, factory) { - define("locales", ['jed', + define(['jed', 'text!af', 'text!ca', 'text!de',