/** * @license almond 0.3.1 Copyright (c) 2011-2014, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/jrburke/almond for details */ //Going sloppy to avoid 'use strict' string cost, but strict practices should //be followed. /*jslint sloppy: true */ /*global setTimeout: false */ var requirejs, require, define; (function (undef) { var main, req, makeMap, handlers, defined = {}, waiting = {}, config = {}, defining = {}, hasOwn = Object.prototype.hasOwnProperty, aps = [].slice, jsSuffixRegExp = /\.js$/; function hasProp(obj, prop) { return hasOwn.call(obj, prop); } /** * Given a relative module name, like ./something, normalize it to * a real name that can be mapped to a path. * @param {String} name the relative name * @param {String} baseName a real name that the name arg is relative * to. * @returns {String} normalized name */ function normalize(name, baseName) { var nameParts, nameSegment, mapValue, foundMap, lastIndex, foundI, foundStarMap, starI, i, j, part, baseParts = baseName && baseName.split("/"), map = config.map, starMap = (map && map['*']) || {}; //Adjust any relative paths. if (name && name.charAt(0) === ".") { //If have a base name, try to normalize against it, //otherwise, assume it is a top-level require that will //be relative to baseUrl in the end. if (baseName) { name = name.split('/'); lastIndex = name.length - 1; // Node .js allowance: if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); } //Lop off the last part of baseParts, so that . matches the //"directory" and not name of the baseName's module. For instance, //baseName of "one/two/three", maps to "one/two/three.js", but we //want the directory, "one/two" for this normalization. name = baseParts.slice(0, baseParts.length - 1).concat(name); //start trimDots for (i = 0; i < name.length; i += 1) { part = name[i]; if (part === ".") { name.splice(i, 1); i -= 1; } else if (part === "..") { if (i === 1 && (name[2] === '..' || name[0] === '..')) { //End of the line. Keep at least one non-dot //path segment at the front so it can be mapped //correctly to disk. Otherwise, there is likely //no path mapping for a path starting with '..'. //This can still fail, but catches the most reasonable //uses of .. break; } else if (i > 0) { name.splice(i - 1, 2); i -= 2; } } } //end trimDots name = name.join("/"); } else if (name.indexOf('./') === 0) { // No baseName, so this is ID is resolved relative // to baseUrl, pull off the leading dot. name = name.substring(2); } } //Apply map config if available. if ((baseParts || starMap) && map) { nameParts = name.split('/'); for (i = nameParts.length; i > 0; i -= 1) { nameSegment = nameParts.slice(0, i).join("/"); if (baseParts) { //Find the longest baseName segment match in the config. //So, do joins on the biggest to smallest lengths of baseParts. for (j = baseParts.length; j > 0; j -= 1) { mapValue = map[baseParts.slice(0, j).join('/')]; //baseName segment has config, find if it has one for //this name. if (mapValue) { mapValue = mapValue[nameSegment]; if (mapValue) { //Match, update name to the new value. foundMap = mapValue; foundI = i; break; } } } } if (foundMap) { break; } //Check for a star map match, but just hold on to it, //if there is a shorter segment match later in a matching //config, then favor over this star map. if (!foundStarMap && starMap && starMap[nameSegment]) { foundStarMap = starMap[nameSegment]; starI = i; } } if (!foundMap && foundStarMap) { foundMap = foundStarMap; foundI = starI; } if (foundMap) { nameParts.splice(0, foundI, foundMap); name = nameParts.join('/'); } } return name; } function makeRequire(relName, forceSync) { return function () { //A version of a require function that passes a moduleName //value for items that may need to //look up paths relative to the moduleName var args = aps.call(arguments, 0); //If first arg is not require('string'), and there is only //one arg, it is the array form without a callback. Insert //a null so that the following concat is correct. if (typeof args[0] !== 'string' && args.length === 1) { args.push(null); } return req.apply(undef, args.concat([relName, forceSync])); }; } function makeNormalize(relName) { return function (name) { return normalize(name, relName); }; } function makeLoad(depName) { return function (value) { defined[depName] = value; }; } function callDep(name) { if (hasProp(waiting, name)) { var args = waiting[name]; delete waiting[name]; defining[name] = true; main.apply(undef, args); } if (!hasProp(defined, name) && !hasProp(defining, name)) { throw new Error('No ' + name); } return defined[name]; } //Turns a plugin!resource to [plugin, resource] //with the plugin being undefined if the name //did not have a plugin prefix. function splitPrefix(name) { var prefix, index = name ? name.indexOf('!') : -1; if (index > -1) { prefix = name.substring(0, index); name = name.substring(index + 1, name.length); } return [prefix, name]; } /** * Makes a name map, normalizing the name, and using a plugin * for normalization if necessary. Grabs a ref to plugin * too, as an optimization. */ makeMap = function (name, relName) { var plugin, parts = splitPrefix(name), prefix = parts[0]; name = parts[1]; if (prefix) { prefix = normalize(prefix, relName); plugin = callDep(prefix); } //Normalize according if (prefix) { if (plugin && plugin.normalize) { name = plugin.normalize(name, makeNormalize(relName)); } else { name = normalize(name, relName); } } else { name = normalize(name, relName); parts = splitPrefix(name); prefix = parts[0]; name = parts[1]; if (prefix) { plugin = callDep(prefix); } } //Using ridiculous property names for space reasons return { f: prefix ? prefix + '!' + name : name, //fullName n: name, pr: prefix, p: plugin }; }; function makeConfig(name) { return function () { return (config && config.config && config.config[name]) || {}; }; } handlers = { require: function (name) { return makeRequire(name); }, exports: function (name) { var e = defined[name]; if (typeof e !== 'undefined') { return e; } else { return (defined[name] = {}); } }, module: function (name) { return { id: name, uri: '', exports: defined[name], config: makeConfig(name) }; } }; main = function (name, deps, callback, relName) { var cjsModule, depName, ret, map, i, args = [], callbackType = typeof callback, usingExports; //Use name if no relName relName = relName || name; //Call the callback to define the module, if necessary. if (callbackType === 'undefined' || callbackType === 'function') { //Pull out the defined dependencies and pass the ordered //values to the callback. //Default to [require, exports, module] if no deps deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; for (i = 0; i < deps.length; i += 1) { map = makeMap(deps[i], relName); depName = map.f; //Fast path CommonJS standard dependencies. if (depName === "require") { args[i] = handlers.require(name); } else if (depName === "exports") { //CommonJS module spec 1.1 args[i] = handlers.exports(name); usingExports = true; } else if (depName === "module") { //CommonJS module spec 1.1 cjsModule = args[i] = handlers.module(name); } else if (hasProp(defined, depName) || hasProp(waiting, depName) || hasProp(defining, depName)) { args[i] = callDep(depName); } else if (map.p) { map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); args[i] = defined[depName]; } else { throw new Error(name + ' missing ' + depName); } } ret = callback ? callback.apply(defined[name], args) : undefined; if (name) { //If setting exports via "module" is in play, //favor that over return value and exports. After that, //favor a non-undefined return value over exports use. if (cjsModule && cjsModule.exports !== undef && cjsModule.exports !== defined[name]) { defined[name] = cjsModule.exports; } else if (ret !== undef || !usingExports) { //Use the return value from the function. defined[name] = ret; } } } else if (name) { //May just be an object definition for the module. Only //worry about defining if have a module name. defined[name] = callback; } }; requirejs = require = req = function (deps, callback, relName, forceSync, alt) { if (typeof deps === "string") { if (handlers[deps]) { //callback in this case is really relName return handlers[deps](callback); } //Just return the module wanted. In this scenario, the //deps arg is the module name, and second arg (if passed) //is just the relName. //Normalize module name, if it contains . or .. return callDep(makeMap(deps, callback).f); } else if (!deps.splice) { //deps is a config object, not an array. config = deps; if (config.deps) { req(config.deps, config.callback); } if (!callback) { return; } if (callback.splice) { //callback is an array, which means it is a dependency list. //Adjust args if there are dependencies deps = callback; callback = relName; relName = null; } else { deps = undef; } } //Support require(['a']) callback = callback || function () {}; //If relName is a function, it is an errback handler, //so remove it. if (typeof relName === 'function') { relName = forceSync; forceSync = alt; } //Simulate async callback; if (forceSync) { main(undef, deps, callback, relName); } else { //Using a non-zero value because of concern for what old browsers //do, and latest browsers "upgrade" to 4 if lower value is used: //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout: //If want a value immediately, use require('id') instead -- something //that works in almond on the global level, but not guaranteed and //unlikely to work in other AMD implementations. setTimeout(function () { main(undef, deps, callback, relName); }, 4); } return req; }; /** * Just drops the config on the floor, but returns req in case * the config return value is used. */ req.config = function (cfg) { return req(cfg); }; /** * Expose module registry for debugging and tooling */ requirejs._defined = defined; define = function (name, deps, callback) { if (typeof name !== 'string') { throw new Error('See almond README: incorrect module build, no module name'); } //This module may not have dependencies if (!deps.splice) { //deps is not an array, so probably means //an object literal or factory function for //the value. Adjust args. callback = deps; deps = []; } if (!hasProp(defined, name) && !hasProp(waiting, name)) { waiting[name] = [name, deps, callback]; } }; define.amd = { jQuery: true }; }()); define("components/almond/almond.js", function(){}); /** * @license RequireJS text 2.0.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/requirejs/text for details */ /*jslint regexp: true */ /*global require, XMLHttpRequest, ActiveXObject, define, window, process, Packages, java, location, Components, FileUtils */ define('text',['module'], function (module) { 'use strict'; var text, fs, Cc, Ci, xpcIsWindows, progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, hasLocation = typeof location !== 'undefined' && location.href, defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), defaultHostName = hasLocation && location.hostname, defaultPort = hasLocation && (location.port || undefined), buildMap = {}, masterConfig = (module.config && module.config()) || {}; text = { version: '2.0.14', strip: function (content) { //Strips declarations so that external SVG and XML //documents can be added to a document without worry. Also, if the string //is an HTML document, only the part inside the body tag is returned. if (content) { content = content.replace(xmlRegExp, ""); var matches = content.match(bodyRegExp); if (matches) { content = matches[1]; } } else { content = ""; } return content; }, jsEscape: function (content) { return content.replace(/(['\\])/g, '\\$1') .replace(/[\f]/g, "\\f") .replace(/[\b]/g, "\\b") .replace(/[\n]/g, "\\n") .replace(/[\t]/g, "\\t") .replace(/[\r]/g, "\\r") .replace(/[\u2028]/g, "\\u2028") .replace(/[\u2029]/g, "\\u2029"); }, createXhr: masterConfig.createXhr || function () { //Would love to dump the ActiveX crap in here. Need IE 6 to die first. var xhr, i, progId; if (typeof XMLHttpRequest !== "undefined") { return new XMLHttpRequest(); } else if (typeof ActiveXObject !== "undefined") { for (i = 0; i < 3; i += 1) { progId = progIds[i]; try { xhr = new ActiveXObject(progId); } catch (e) {} if (xhr) { progIds = [progId]; // so faster next time break; } } } return xhr; }, /** * Parses a resource name into its component parts. Resource names * look like: module/name.ext!strip, where the !strip part is * optional. * @param {String} name the resource name * @returns {Object} with properties "moduleName", "ext" and "strip" * where strip is a boolean. */ parseName: function (name) { var modName, ext, temp, strip = false, index = name.lastIndexOf("."), isRelative = name.indexOf('./') === 0 || name.indexOf('../') === 0; if (index !== -1 && (!isRelative || index > 1)) { modName = name.substring(0, index); ext = name.substring(index + 1); } else { modName = name; } temp = ext || modName; index = temp.indexOf("!"); if (index !== -1) { //Pull off the strip arg. strip = temp.substring(index + 1) === "strip"; temp = temp.substring(0, index); if (ext) { ext = temp; } else { modName = temp; } } return { moduleName: modName, ext: ext, strip: strip }; }, xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, /** * Is an URL on another domain. Only works for browser use, returns * false in non-browser environments. Only used to know if an * optimized .js version of a text resource should be loaded * instead. * @param {String} url * @returns Boolean */ useXhr: function (url, protocol, hostname, port) { var uProtocol, uHostName, uPort, match = text.xdRegExp.exec(url); if (!match) { return true; } uProtocol = match[2]; uHostName = match[3]; uHostName = uHostName.split(':'); uPort = uHostName[1]; uHostName = uHostName[0]; return (!uProtocol || uProtocol === protocol) && (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && ((!uPort && !uHostName) || uPort === port); }, finishLoad: function (name, strip, content, onLoad) { content = strip ? text.strip(content) : content; if (masterConfig.isBuild) { buildMap[name] = content; } onLoad(content); }, load: function (name, req, onLoad, config) { //Name has format: some.module.filext!strip //The strip part is optional. //if strip is present, then that means only get the string contents //inside a body tag in an HTML string. For XML/SVG content it means //removing the declarations so the content can be inserted //into the current doc without problems. // Do not bother with the work if a build and text will // not be inlined. if (config && config.isBuild && !config.inlineText) { onLoad(); return; } masterConfig.isBuild = config && config.isBuild; var parsed = text.parseName(name), nonStripName = parsed.moduleName + (parsed.ext ? '.' + parsed.ext : ''), url = req.toUrl(nonStripName), useXhr = (masterConfig.useXhr) || text.useXhr; // Do not load if it is an empty: url if (url.indexOf('empty:') === 0) { onLoad(); return; } //Load the text. Use XHR if possible and in a browser. if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { text.get(url, function (content) { text.finishLoad(name, parsed.strip, content, onLoad); }, function (err) { if (onLoad.error) { onLoad.error(err); } }); } else { //Need to fetch the resource across domains. Assume //the resource has been optimized into a JS module. Fetch //by the module name + extension, but do not include the //!strip part to avoid file system issues. req([nonStripName], function (content) { text.finishLoad(parsed.moduleName + '.' + parsed.ext, parsed.strip, content, onLoad); }); } }, write: function (pluginName, moduleName, write, config) { if (buildMap.hasOwnProperty(moduleName)) { var content = text.jsEscape(buildMap[moduleName]); write.asModule(pluginName + "!" + moduleName, "define(function () { return '" + content + "';});\n"); } }, writeFile: function (pluginName, moduleName, req, write, config) { var parsed = text.parseName(moduleName), extPart = parsed.ext ? '.' + parsed.ext : '', nonStripName = parsed.moduleName + extPart, //Use a '.js' file name so that it indicates it is a //script that can be loaded across domains. fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; //Leverage own load() method to load plugin value, but only //write out values that do not have the strip argument, //to avoid any potential issues with ! in file names. text.load(nonStripName, req, function (value) { //Use own write() method to construct full module value. //But need to create shell that translates writeFile's //write() to the right interface. var textWrite = function (contents) { return write(fileName, contents); }; textWrite.asModule = function (moduleName, contents) { return write.asModule(moduleName, fileName, contents); }; text.write(pluginName, nonStripName, textWrite, config); }, config); } }; if (masterConfig.env === 'node' || (!masterConfig.env && typeof process !== "undefined" && process.versions && !!process.versions.node && !process.versions['node-webkit'] && !process.versions['atom-shell'])) { //Using special require.nodeRequire, something added by r.js. fs = require.nodeRequire('fs'); text.get = function (url, callback, errback) { try { var file = fs.readFileSync(url, 'utf8'); //Remove BOM (Byte Mark Order) from utf8 files if it is there. if (file[0] === '\uFEFF') { file = file.substring(1); } callback(file); } catch (e) { if (errback) { errback(e); } } }; } else if (masterConfig.env === 'xhr' || (!masterConfig.env && text.createXhr())) { text.get = function (url, callback, errback, headers) { var xhr = text.createXhr(), header; xhr.open('GET', url, true); //Allow plugins direct access to xhr headers if (headers) { for (header in headers) { if (headers.hasOwnProperty(header)) { xhr.setRequestHeader(header.toLowerCase(), headers[header]); } } } //Allow overrides specified in config if (masterConfig.onXhr) { masterConfig.onXhr(xhr, url); } xhr.onreadystatechange = function (evt) { var status, err; //Do not explicitly handle errors, those should be //visible via console output in the browser. if (xhr.readyState === 4) { status = xhr.status || 0; if (status > 399 && status < 600) { //An http 4xx or 5xx error. Signal an error. err = new Error(url + ' HTTP status: ' + status); err.xhr = xhr; if (errback) { errback(err); } } else { callback(xhr.responseText); } if (masterConfig.onXhrComplete) { masterConfig.onXhrComplete(xhr, url); } } }; xhr.send(null); }; } else if (masterConfig.env === 'rhino' || (!masterConfig.env && typeof Packages !== 'undefined' && typeof java !== 'undefined')) { //Why Java, why is this so awkward? text.get = function (url, callback) { var stringBuffer, line, encoding = "utf-8", file = new java.io.File(url), lineSeparator = java.lang.System.getProperty("line.separator"), input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), content = ''; try { stringBuffer = new java.lang.StringBuffer(); line = input.readLine(); // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 // http://www.unicode.org/faq/utf_bom.html // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 if (line && line.length() && line.charAt(0) === 0xfeff) { // Eat the BOM, since we've already found the encoding on this file, // and we plan to concatenating this buffer with others; the BOM should // only appear at the top of a file. line = line.substring(1); } if (line !== null) { stringBuffer.append(line); } while ((line = input.readLine()) !== null) { stringBuffer.append(lineSeparator); stringBuffer.append(line); } //Make sure we return a JavaScript string and not a Java string. content = String(stringBuffer.toString()); //String } finally { input.close(); } callback(content); }; } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env && typeof Components !== 'undefined' && Components.classes && Components.interfaces)) { //Avert your gaze! Cc = Components.classes; Ci = Components.interfaces; Components.utils['import']('resource://gre/modules/FileUtils.jsm'); xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc); text.get = function (url, callback) { var inStream, convertStream, fileObj, readData = {}; if (xpcIsWindows) { url = url.replace(/\//g, '\\'); } fileObj = new FileUtils.File(url); //XPCOM, you so crazy try { inStream = Cc['@mozilla.org/network/file-input-stream;1'] .createInstance(Ci.nsIFileInputStream); inStream.init(fileObj, 1, 0, false); convertStream = Cc['@mozilla.org/intl/converter-input-stream;1'] .createInstance(Ci.nsIConverterInputStream); convertStream.init(inStream, "utf-8", inStream.available(), Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); convertStream.readString(inStream.available(), readData); convertStream.close(); inStream.close(); callback(readData.value); } catch (e) { throw new Error((fileObj && fileObj.path || '') + ': ' + e); } }; } return text; }); // RequireJS UnderscoreJS template plugin // http://github.com/jfparadis/requirejs-tpl // // An alternative to http://github.com/ZeeAgency/requirejs-tpl // // Using UnderscoreJS micro-templates at http://underscorejs.org/#template // Using and RequireJS text.js at http://requirejs.org/docs/api.html#text // @author JF Paradis // @version 0.0.2 // // Released under the MIT license // // Usage: // require(['backbone', 'tpl!mytemplate'], function (Backbone, mytemplate) { // return Backbone.View.extend({ // initialize: function(){ // this.render(); // }, // render: function(){ // this.$el.html(mytemplate({message: 'hello'})); // }); // }); // // Configuration: (optional) // require.config({ // tpl: { // extension: '.tpl' // default = '.html' // } // }); /*jslint nomen: true */ /*global define: false */ define('tpl',['text', 'underscore'], function (text, _) { 'use strict'; var buildMap = {}, buildTemplateSource = "define('{pluginName}!{moduleName}', function () { return {source}; });\n"; return { version: '0.0.2', load: function (moduleName, parentRequire, onload, config) { if (config.tpl && config.tpl.templateSettings) { _.templateSettings = config.tpl.templateSettings; } if (buildMap[moduleName]) { onload(buildMap[moduleName]); } else { var ext = (config.tpl && config.tpl.extension) || '.html'; var path = (config.tpl && config.tpl.path) || ''; text.load(path + moduleName + ext, parentRequire, function (source) { buildMap[moduleName] = _.template(source); onload(buildMap[moduleName]); }, config); } }, write: function (pluginName, moduleName, write) { var build = buildMap[moduleName], source = build && build.source; if (source) { write.asModule(pluginName + '!' + moduleName, buildTemplateSource .replace('{pluginName}', pluginName) .replace('{moduleName}', moduleName) .replace('{source}', source)); } } }; }); define('tpl!action', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
\n '+ ((__t=(time))==null?'':__t)+ ' **'+ ((__t=(username))==null?'':__t)+ ' \n '+ ((__t=(message))==null?'':__t)+ '\n
\n'; } return __p; }; }); define('tpl!add_contact_dropdown', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='\n'; } return __p; }; }); define('tpl!add_contact_form', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
  • \n
    \n \n \n
    \n
  • \n'; } return __p; }; }); define('tpl!change_status_message', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
    \n
    \n \n \n \n \n
    \n
    \n'; } return __p; }; }); define('tpl!chat_status', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
    \n \n '+ ((__t=(status_message))==null?'':__t)+ '\n \n \n
    \n'; } return __p; }; }); define('tpl!chatarea', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
    \n
    \n
    \n '; if (show_toolbar) { __p+='\n
      \n '; } __p+='\n \n'; } return __p; }; }); define('tpl!form_username', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+=''; if (label) { __p+='\n\n'; } __p+='\n
      \n '+ ((__t=(domain))==null?'':__t)+ '\n
      \n'; } return __p; }; }); define('tpl!group_header', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+=''+ ((__t=(label_group))==null?'':__t)+ '\n'; } return __p; }; }); define('tpl!info', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='
      '+ ((__t=(message))==null?'':__t)+ '
      \n'; } return __p; }; }); define('tpl!login_panel', [],function () { return function(obj){ var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='\n '; if (auto_login) { __p+='\n
      ')); } if (spinner === true) { this.$content.append(''); } else if (spinner === false) { this.$content.find('span.spinner').remove(); } return this.scrollDown(); }, onMessageAdded: function (message) { /* Handler that gets called when a new message object is created. * * Parameters: * (Object) message - The message Backbone object that was added. */ if (typeof this.clear_status_timeout !== 'undefined') { window.clearTimeout(this.clear_status_timeout); delete this.clear_status_timeout; } if (!message.get('message')) { if (message.get('chat_state') === COMPOSING) { this.showStatusNotification(message.get('fullname')+' '+__('is typing')); this.clear_status_timeout = window.setTimeout(this.clearStatusNotification.bind(this), 10000); return; } else if (message.get('chat_state') === PAUSED) { this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing')); return; } else if (_.contains([INACTIVE, ACTIVE], message.get('chat_state'))) { this.$content.find('div.chat-event').remove(); return; } else if (message.get('chat_state') === GONE) { this.showStatusNotification(message.get('fullname')+' '+__('has gone away')); return; } } else { this.showMessage(_.clone(message.attributes)); } if ((message.get('sender') !== 'me') && (converse.windowState === 'blur')) { converse.incrementMsgCounter(); } if (!this.model.get('minimized') && !this.$el.is(':visible')) { this.show(); } }, createMessageStanza: function (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(ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up(); }, sendMessage: function (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 (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*$/), msgs; if (match) { if (match[1] === "clear") { return this.clearMessages(); } else if (match[1] === "help") { 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 () { /* 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}) ); }, setChatState: function (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 (typeof this.chat_state_timeout !== 'undefined') { window.clearTimeout(this.chat_state_timeout); delete this.chat_state_timeout; } if (state === COMPOSING) { this.chat_state_timeout = window.setTimeout( this.setChatState.bind(this), converse.TIMEOUTS.PAUSED, PAUSED); } else if (state === PAUSED) { this.chat_state_timeout = window.setTimeout( this.setChatState.bind(this), converse.TIMEOUTS.INACTIVE, INACTIVE); } if (!no_save && this.model.get('chat_state') !== state) { this.model.set('chat_state', state); } return this; }, keyPressed: function (ev) { /* Event handler for when a key is pressed in a chat box textarea. */ var $textarea = $(ev.target), message; if (ev.keyCode === KEY.ENTER) { ev.preventDefault(); message = $textarea.val(); $textarea.val('').focus(); if (message !== '') { if (this.model.get('chatroom')) { this.onChatRoomMessageSubmitted(message); } else { this.onMessageSubmitted(message); } converse.emit('messageSend', message); } this.setChatState(ACTIVE); } else if (!this.model.get('chatroom')) { // chat state data is currently only for single user chat // Set chat state to composing if keyCode is not a forward-slash // (which would imply an internal command and not a message). this.setChatState(COMPOSING, ev.keyCode === KEY.FORWARD_SLASH); } }, onStartVerticalResize: function (ev) { if (!converse.allow_dragresize) { return true; } // Record element attributes for mouseMove(). this.height = this.$el.children('.box-flyout').height(); converse.resizing = { 'chatbox': this, 'direction': 'top' }; this.prev_pageY = ev.pageY; }, onStartHorizontalResize: function (ev) { if (!converse.allow_dragresize) { return true; } this.width = this.$el.children('.box-flyout').width(); converse.resizing = { 'chatbox': this, 'direction': 'left' }; this.prev_pageX = ev.pageX; }, onStartDiagonalResize: function (ev) { this.onStartHorizontalResize(ev); this.onStartVerticalResize(ev); converse.resizing.direction = 'topleft'; }, setChatBoxHeight: function (height) { if (!this.model.get('minimized')) { if (height) { height = converse.applyDragResistance(height, this.model.get('default_height'))+'px'; } else { height = ""; } this.$el.children('.box-flyout')[0].style.height = height; } }, setChatBoxWidth: function (width) { if (!this.model.get('minimized')) { if (width) { width = converse.applyDragResistance(width, this.model.get('default_width'))+'px'; } else { width = ""; } this.$el[0].style.width = width; this.$el.children('.box-flyout')[0].style.width = width; } }, resizeChatBox: function (ev) { var diff; if (converse.resizing.direction.indexOf('top') === 0) { diff = ev.pageY - this.prev_pageY; if (diff) { this.height = ((this.height-diff) > (this.model.get('min_height') || 0)) ? (this.height-diff) : this.model.get('min_height'); this.prev_pageY = ev.pageY; this.setChatBoxHeight(this.height); } } if (converse.resizing.direction.indexOf('left') !== -1) { diff = this.prev_pageX - ev.pageX; if (diff) { this.width = ((this.width+diff) > (this.model.get('min_width') || 0)) ? (this.width+diff) : this.model.get('min_width'); this.prev_pageX = ev.pageX; this.setChatBoxWidth(this.width); } } }, clearMessages: function (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; }, insertEmoticon: function (ev) { ev.stopPropagation(); this.$el.find('.toggle-smiley ul').slideToggle(200); var $textbox = this.$el.find('textarea.chat-textarea'); var value = $textbox.val(); var $target = $(ev.target); $target = $target.is('a') ? $target : $target.children('a'); if (value && (value[value.length-1] !== ' ')) { value = value + ' '; } $textbox.focus().val(value+$target.data('emoticon')+' '); }, toggleEmoticonMenu: function (ev) { ev.stopPropagation(); this.$el.find('.toggle-smiley ul').slideToggle(200); }, toggleCall: function (ev) { ev.stopPropagation(); converse.emit('callButtonClicked', { connection: converse.connection, model: this.model }); }, onChatStatusChanged: function (item) { var chat_status = item.get('chat_status'), 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(); } } converse.emit('contactStatusChanged', item.attributes, item.get('chat_status')); }, onStatusChanged: function (item) { this.showStatusMessage(); converse.emit('contactStatusMessageChanged', item.attributes, item.get('status')); }, onMinimizedChanged: function (item) { if (item.get('minimized')) { this.hide(); } else { this.maximize(); } }, showStatusMessage: function (msg) { msg = msg || this.model.get('status'); if (typeof msg === "string") { this.$el.find('p.user-custom-message').text(msg).attr('title', msg); } return this; }, close: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } if (converse.connection.connected) { this.model.destroy(); this.setChatState(INACTIVE); } else { this.hide(); } converse.emit('chatBoxClosed', this); return this; }, maximize: function () { var chatboxviews = converse.chatboxviews; // Restores a minimized chat box this.$el.insertAfter(chatboxviews.get("controlbox").$el).show('fast', function () { /* Now that the chat box is visible, we can call trimChats * to make space available if need be. */ chatboxviews.trimChats(this); utils.refreshWebkit(); this.$content.scrollTop(this.model.get('scroll')); this.setChatState(ACTIVE).focus(); converse.emit('chatBoxMaximized', this); }.bind(this)); }, minimize: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } // save the scroll position to restore it on maximize this.model.save({'scroll': this.$content.scrollTop()}); // Minimizes a chat box this.setChatState(INACTIVE).model.minimize(); this.$el.hide('fast', utils.refreshwebkit); converse.emit('chatBoxMinimized', this); }, updateVCard: function () { if (!this.use_vcards) { return this; } var jid = this.model.get('jid'), contact = converse.roster.get(jid); if ((contact) && (!contact.get('vcard_updated'))) { converse.getVCard( jid, function (iq, jid, fullname, image, image_type, url) { this.model.save({ 'fullname' : fullname || jid, 'url': url, 'image_type': image_type, 'image': image }); }.bind(this), function () { converse.log("ChatBoxView.initialize: An error occured while fetching vcard"); } ); } return this; }, renderToolbar: function (options) { if (!converse.show_toolbar) { return; } options = _.extend(options || {}, { label_clear: __('Clear all messages'), label_hide_occupants: __('Hide the list of occupants'), 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, show_emoticons: converse.visible_toolbar_buttons.emoticons, // FIXME Leaky abstraction MUC show_occupants_toggle: this.is_chatroom && converse.visible_toolbar_buttons.toggle_occupants }); this.$el.find('.chat-toolbar').html(converse.templates.toolbar(_.extend(this.model.toJSON(), options || {}))); return this; }, renderAvatar: function () { if (!this.model.get('image')) { return; } var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'), canvas = $('').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, 32, 32*(1/ratio)); } else { ctx.drawImage(img, 0,0, 32, 32*ratio); } }; img.src = img_src; this.$el.find('.chat-title').before(canvas); return this; }, focus: function () { this.$el.find('.chat-textarea').focus(); converse.emit('chatBoxFocused', this); return this; }, hide: function () { if (this.$el.is(':visible') && this.$el.css('opacity') === "1") { this.$el.hide(); utils.refreshWebkit(); } return this; }, show: _.debounce(function (focus) { if (this.$el.is(':visible') && this.$el.css('opacity') === "1") { if (focus) { this.focus(); } return this; } this.initDragResize().setDimensions(); this.$el.fadeIn(function () { if (converse.connection.connected) { // Without a connection, we haven't yet initialized // localstorage this.model.save(); } this.setChatState(ACTIVE); this.scrollDown(); if (focus) { this.focus(); } }.bind(this)); return this; }, 250, true), scrollDownMessageHeight: function ($message) { if (this.$content.is(':visible')) { this.$content.scrollTop(this.$content.scrollTop() + $message[0].scrollHeight); } return this; }, scrollDown: function () { if (this.$content.is(':visible')) { this.$content.scrollTop(this.$content[0].scrollHeight); } return this; } }); this.ContactsPanel = Backbone.View.extend({ tagName: 'div', className: 'controlbox-pane', id: 'users', events: { 'click a.toggle-xmpp-contact-form': 'toggleContactForm', 'submit form.add-xmpp-contact': 'addContactFromForm', 'submit form.search-xmpp-contact': 'searchContacts', 'click a.subscribe-to-user': 'addContactFromList' }, initialize: function (cfg) { cfg.$parent.append(this.$el); this.$tabs = cfg.$parent.parent().find('#controlbox-tabs'); }, render: function () { var markup; var widgets = converse.templates.contacts_panel({ label_online: __('Online'), label_busy: __('Busy'), label_away: __('Away'), label_offline: __('Offline'), label_logout: __('Log out'), include_offline_state: converse.include_offline_state, allow_logout: converse.allow_logout }); this.$tabs.append(converse.templates.contacts_tab({label_contacts: LABEL_CONTACTS})); if (converse.xhr_user_search) { markup = converse.templates.search_contact({ label_contact_name: __('Contact name'), label_search: __('Search') }); } else { markup = converse.templates.add_contact_form({ label_contact_username: __('e.g. user@example.com'), label_add: __('Add') }); } if (converse.allow_contact_requests) { widgets += converse.templates.add_contact_dropdown({ label_click_to_chat: __('Click to add new chat contacts'), label_add_contact: __('Add a contact') }); } this.$el.html(widgets); this.$el.find('.search-xmpp ul').append(markup); return this; }, toggleContactForm: function (ev) { ev.preventDefault(); this.$el.find('.search-xmpp').toggle('fast', function () { if ($(this).is(':visible')) { $(this).find('input.username').focus(); } }); }, searchContacts: function (ev) { ev.preventDefault(); $.getJSON(converse.xhr_user_search_url+ "?q=" + $(ev.target).find('input.username').val(), function (data) { var $ul= $('.search-xmpp ul'); $ul.find('li.found-user').remove(); $ul.find('li.chat-info').remove(); if (!data.length) { $ul.append('
    • '+__('No users found')+'
    • '); } $(data).each(function (idx, obj) { $ul.append( $('
    • ') .append( $('') .attr('data-recipient', Strophe.getNodeFromJid(obj.id)+"@"+Strophe.getDomainFromJid(obj.id)) .text(obj.fullname) ) ); }); }); }, addContactFromForm: function (ev) { ev.preventDefault(); var $input = $(ev.target).find('input'); var jid = $input.val(); if (! jid) { // this is not a valid JID $input.addClass('error'); return; } converse.roster.addAndSubscribe(jid); $('.search-xmpp').hide(); }, addContactFromList: function (ev) { ev.preventDefault(); var $target = $(ev.target), jid = $target.attr('data-recipient'), name = $target.text(); converse.roster.addAndSubscribe(jid, name); $target.parent().remove(); $('.search-xmpp').hide(); } }); this.ControlBoxView = converse.ChatBoxView.extend({ tagName: 'div', className: 'chatbox', id: 'controlbox', events: { 'click a.close-chatbox-button': 'close', 'click ul#controlbox-tabs li a': 'switchTab', 'mousedown .dragresize-top': 'onStartVerticalResize', 'mousedown .dragresize-left': 'onStartHorizontalResize', 'mousedown .dragresize-topleft': 'onStartDiagonalResize' }, initialize: function () { this.$el.insertAfter(converse.controlboxtoggle.$el); $(window).on('resize', _.debounce(this.setDimensions.bind(this), 100)); this.model.on('change:connected', this.onConnected, this); this.model.on('destroy', this.hide, this); this.model.on('hide', this.hide, this); this.model.on('show', this.show, this); this.model.on('change:closed', this.ensureClosedState, this); this.render(); if (this.model.get('connected')) { this.initRoster(); } if (typeof this.model.get('closed')==='undefined') { this.model.set('closed', !converse.show_controlbox_by_default); } if (!this.model.get('closed')) { this.show(); } else { this.hide(); } }, render: function () { if (!converse.connection.connected || !converse.connection.authenticated || converse.connection.disconnecting) { // TODO: we might need to take prebinding into consideration here. this.renderLoginPanel(); } else if (!this.contactspanel || !this.contactspanel.$el.is(':visible')) { this.renderContactsPanel(); } return this; }, giveFeedback: function (message, klass) { var $el = this.$('.conn-feedback'); $el.addClass('conn-feedback').text(message); if (klass) { $el.addClass(klass); } }, onConnected: function () { if (this.model.get('connected')) { this.render().initRoster(); } }, initRoster: function () { /* We initialize the roster, which will appear inside the * Contacts Panel. */ converse.roster = new converse.RosterContacts(); converse.roster.browserStorage = new Backbone.BrowserStorage[converse.storage]( b64_sha1('converse.contacts-'+converse.bare_jid)); var rostergroups = new converse.RosterGroups(); rostergroups.browserStorage = new Backbone.BrowserStorage[converse.storage]( b64_sha1('converse.roster.groups'+converse.bare_jid)); converse.rosterview = new converse.RosterView({model: rostergroups}); this.contactspanel.$el.append(converse.rosterview.$el); converse.rosterview.render().fetch().update(); return this; }, renderLoginPanel: function () { var $feedback = this.$('.conn-feedback'); // we want to still show any existing feedback. this.$el.html(converse.templates.controlbox(this.model.toJSON())); var cfg = { '$parent': this.$el.find('.controlbox-panes'), 'model': this }; if (!this.loginpanel) { this.loginpanel = new converse.LoginPanel(cfg); } else { this.loginpanel.delegateEvents().initialize(cfg); } this.loginpanel.render(); this.initDragResize().setDimensions(); if ($feedback.length && $feedback.text() !== __('Connecting')) { this.$('.conn-feedback').replaceWith($feedback); } return this; }, renderContactsPanel: function () { this.$el.html(converse.templates.controlbox(this.model.toJSON())); this.contactspanel = new converse.ContactsPanel({ '$parent': this.$el.find('.controlbox-panes') }); this.contactspanel.render(); converse.xmppstatusview = new converse.XMPPStatusView({ 'model': converse.xmppstatus }); converse.xmppstatusview.render(); this.initDragResize().setDimensions(); }, close: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } if (converse.connection.connected) { this.model.save({'closed': true}); } else { this.model.trigger('hide'); } converse.emit('controlBoxClosed', this); return this; }, ensureClosedState: function () { if (this.model.get('closed')) { this.hide(); } else { this.show(); } }, hide: function (callback) { this.$el.hide('fast', function () { utils.refreshWebkit(); converse.emit('chatBoxClosed', this); converse.controlboxtoggle.show(function () { if (typeof callback === "function") { callback(); } }); }); return this; }, show: function () { converse.controlboxtoggle.hide(function () { this.$el.show('fast', function () { if (converse.rosterview) { converse.rosterview.update(); } utils.refreshWebkit(); }.bind(this)); converse.emit('controlBoxOpened', this); }.bind(this)); return this; }, switchTab: function (ev) { // TODO: automatically focus the relevant input if (ev && ev.preventDefault) { ev.preventDefault(); } var $tab = $(ev.target), $sibling = $tab.parent().siblings('li').children('a'), $tab_panel = $($tab.attr('href')); $($sibling.attr('href')).hide(); $sibling.removeClass('current'); $tab.addClass('current'); $tab_panel.show(); return this; }, showHelpMessages: function (msgs) { // Override showHelpMessages in ChatBoxView, for now do nothing. return; } }); this.ChatBoxes = Backbone.Collection.extend({ model: converse.ChatBox, comparator: 'time_opened', registerMessageHandler: function () { converse.connection.addHandler( function (message) { this.onMessage(message); return true; }.bind(this), null, 'message', 'chat'); }, onConnected: function () { this.browserStorage = new Backbone.BrowserStorage[converse.storage]( b64_sha1('converse.chatboxes-'+converse.bare_jid)); this.registerMessageHandler(); this.fetch({ add: true, success: function (collection, resp) { collection.each(function (chatbox) { if (chatbox.get('id') !== 'controlbox' && !chatbox.get('minimized')) { chatbox.trigger('show'); } }); if (!_.include(_.pluck(resp, 'id'), 'controlbox')) { this.add({ id: 'controlbox', box_id: 'controlbox' }); } this.get('controlbox').save({connected:true}); }.bind(this) }); }, onMessage: function (message) { /* Handler method for all incoming single-user chat "message" stanzas. */ var $message = $(message), contact_jid, $forwarded, $delay, from_bare_jid, from_resource, is_me, msgid, chatbox, resource, from_jid = $message.attr('from'), to_jid = $message.attr('to'), to_resource = Strophe.getResourceFromJid(to_jid), archive_id = $message.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('id'); if (to_resource && to_resource !== converse.resource) { converse.log('Ignore incoming message intended for a different resource: '+to_jid, 'info'); return true; } if (from_jid === converse.connection.jid) { // FIXME: Forwarded messages should be sent to specific resources, not broadcasted converse.log("Ignore incoming message sent from this client's JID: "+from_jid, 'info'); return true; } $forwarded = $message.find('forwarded'); if ($forwarded.length) { $message = $forwarded.children('message'); $delay = $forwarded.children('delay'); from_jid = $message.attr('from'); to_jid = $message.attr('to'); } from_bare_jid = Strophe.getBareJidFromJid(from_jid); from_resource = Strophe.getResourceFromJid(from_jid); is_me = from_bare_jid === converse.bare_jid; msgid = $message.attr('id'); 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. chatbox = this.getChatBox(contact_jid, $message.find('body').length > 0); if (!chatbox) { return true; } if (msgid && chatbox.messages.findWhere({msgid: msgid})) { return true; // We already have this message stored. } if (chatbox.shouldPlayNotification($message)) { converse.playNotification(); } chatbox.createMessage($message, $delay, archive_id); converse.roster.addResource(contact_jid, resource); converse.emit('message', message); return true; }, getChatBox: function (jid, create) { /* 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? */ jid = jid.toLowerCase(); var bare_jid = Strophe.getBareJidFromJid(jid); var chatbox = this.get(bare_jid); if (!chatbox && create) { var roster_item = converse.roster.get(bare_jid); if (roster_item === undefined) { converse.log('Could not get roster item for JID '+bare_jid, 'error'); return; } chatbox = this.create({ 'id': bare_jid, 'jid': bare_jid, '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') }); } return chatbox; } }); this.ChatBoxViews = Backbone.Overview.extend({ initialize: function () { this.model.on("add", this.onChatBoxAdded, this); this.model.on("change:minimized", function (item) { if (item.get('minimized') === true) { /* When a chat is minimized in trimChats, trimChats needs to be * called again (in case the minimized chats toggle is newly shown). */ this.trimChats(); } else { this.trimChats(this.get(item.get('id'))); } }, this); }, _ensureElement: function () { /* Override method from backbone.js * If the #conversejs element doesn't exist, create it. */ if (!this.el) { var $el = $('#conversejs'); if (!$el.length) { $el = $('
      '); $('body').append($el); } $el.html(converse.templates.chats_panel()); this.setElement($el, false); } else { this.setElement(_.result(this, 'el'), false); } }, onChatBoxAdded: function (item) { var view = this.get(item.get('id')); if (!view) { if (item.get('chatroom')) { view = new converse.ChatRoomView({'model': item}); } else if (item.get('box_id') === 'controlbox') { view = new converse.ControlBoxView({model: item}); } else { view = new converse.ChatBoxView({model: item}); } this.add(item.get('id'), view); } else { delete view.model; // Remove ref to old model to help garbage collection view.model = item; view.initialize(); } this.trimChats(view); }, trimChats: function (newchat) { /* This method is called when a newly created chat box will * be shown. * * It checks whether there is enough space on the page to show * another chat box. Otherwise it minimize the oldest chat box * to create space. */ if (converse.no_trimming || (this.model.length <= 1)) { return; } var oldest_chat, controlbox_width = 0, $minimized = converse.minimized_chats.$el, minimized_width = _.contains(this.model.pluck('minimized'), true) ? $minimized.outerWidth(true) : 0, boxes_width = newchat ? newchat.$el.outerWidth(true) : 0, new_id = newchat ? newchat.model.get('id') : null, controlbox = this.get('controlbox'); if (!controlbox || !controlbox.$el.is(':visible')) { controlbox_width = converse.controlboxtoggle.$el.outerWidth(true); } else { controlbox_width = controlbox.$el.outerWidth(true); } _.each(this.getAll(), function (view) { var id = view.model.get('id'); if ((id !== 'controlbox') && (id !== new_id) && (!view.model.get('minimized')) && view.$el.is(':visible')) { boxes_width += view.$el.outerWidth(true); } }); if ((minimized_width + boxes_width + controlbox_width) > $('body').outerWidth(true)) { oldest_chat = this.getOldestMaximizedChat(); if (oldest_chat && oldest_chat.get('id') !== new_id) { oldest_chat.minimize(); } } }, getOldestMaximizedChat: function () { // Get oldest view (which is not controlbox) var i = 0; var model = this.model.sort().at(i); while (model.get('id') === 'controlbox' || model.get('minimized') === true) { i++; model = this.model.at(i); if (!model) { return null; } } return model; }, closeAllChatBoxes: function (include_controlbox) { // TODO: once Backbone.Overview has been refactored, we should // be able to call .each on the views themselves. var ids = []; this.model.each(function (model) { var id = model.get('id'); if (include_controlbox || id !== 'controlbox') { ids.push(id); } }); ids.forEach(function(id) { var chatbox = this.get(id); if (chatbox) { chatbox.close(); } }, this); return this; }, showChat: function (attrs) { /* Find the chat box and show it. If it doesn't exist, create it. */ var chatbox = this.model.get(attrs.jid); if (!chatbox) { chatbox = this.model.create(attrs, { 'error': function (model, response) { converse.log(response.responseText); } }); } if (chatbox.get('minimized')) { chatbox.maximize(); } else { chatbox.trigger('show', true); } return chatbox; } }); this.MinimizedChatBoxView = Backbone.View.extend({ tagName: 'div', className: 'chat-head', events: { 'click .close-chatbox-button': 'close', 'click .restore-chat': 'restore' }, initialize: function () { this.model.messages.on('add', function (m) { if (m.get('message')) { this.updateUnreadMessagesCounter(); } }, this); this.model.on('change:minimized', this.clearUnreadMessagesCounter, this); }, render: function () { var data = _.extend( this.model.toJSON(), { 'tooltip': __('Click to restore this chat') } ); if (this.model.get('chatroom')) { data.title = this.model.get('name'); this.$el.addClass('chat-head-chatroom'); } else { data.title = this.model.get('fullname'); this.$el.addClass('chat-head-chatbox'); } return this.$el.html(converse.templates.trimmed_chat(data)); }, clearUnreadMessagesCounter: function () { this.model.set({'num_unread': 0}); this.render(); }, updateUnreadMessagesCounter: function () { this.model.set({'num_unread': this.model.get('num_unread') + 1}); this.render(); }, close: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.remove(); this.model.destroy(); converse.emit('chatBoxClosed', this); return this; }, restore: _.debounce(function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.model.messages.off('add',null,this); this.remove(); this.model.maximize(); }, 200, true) }); this.MinimizedChats = Backbone.Overview.extend({ el: "#minimized-chats", events: { "click #toggle-minimized-chats": "toggle" }, initialize: function () { this.initToggle(); this.model.on("add", this.onChanged, this); this.model.on("destroy", this.removeChat, this); this.model.on("change:minimized", this.onChanged, this); this.model.on('change:num_unread', this.updateUnreadMessagesCounter, this); }, tearDown: function () { this.model.off("add", this.onChanged); this.model.off("destroy", this.removeChat); this.model.off("change:minimized", this.onChanged); this.model.off('change:num_unread', this.updateUnreadMessagesCounter); return this; }, initToggle: function () { this.toggleview = new converse.MinimizedChatsToggleView({ model: new converse.MinimizedChatsToggle() }); var id = b64_sha1('converse.minchatstoggle'+converse.bare_jid); this.toggleview.model.id = id; // Appears to be necessary for backbone.browserStorage this.toggleview.model.browserStorage = new Backbone.BrowserStorage[converse.storage](id); this.toggleview.model.fetch(); }, render: function () { if (this.keys().length === 0) { this.$el.hide('fast'); } else if (this.keys().length === 1) { this.$el.show('fast'); } return this.$el; }, toggle: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.toggleview.model.save({'collapsed': !this.toggleview.model.get('collapsed')}); this.$('.minimized-chats-flyout').toggle(); }, onChanged: function (item) { if (item.get('id') !== 'controlbox' && item.get('minimized')) { this.addChat(item); } else if (this.get(item.get('id'))) { this.removeChat(item); } }, addChat: function (item) { var existing = this.get(item.get('id')); if (existing && existing.$el.parent().length !== 0) { return; } var view = new converse.MinimizedChatBoxView({model: item}); this.$('.minimized-chats-flyout').append(view.render()); this.add(item.get('id'), view); this.toggleview.model.set({'num_minimized': this.keys().length}); this.render(); }, removeChat: function (item) { this.remove(item.get('id')); this.toggleview.model.set({'num_minimized': this.keys().length}); this.render(); }, updateUnreadMessagesCounter: function () { var ls = this.model.pluck('num_unread'), count = 0, i; for (i=0; i name2? 1 : 0); } else { return converse.STATUS_WEIGHTS[status1] < converse.STATUS_WEIGHTS[status2] ? -1 : 1; } }, subscribeToSuggestedItems: function (msg) { $(msg).find('item').each(function (i, items) { if (this.getAttribute('action') === 'add') { converse.roster.addAndSubscribe( this.getAttribute('jid'), null, converse.xmppstatus.get('fullname')); } }); return true; }, isSelf: function (jid) { return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid)); }, addAndSubscribe: function (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. */ this.addContact(jid, name, groups, attributes).done(function (contact) { if (contact instanceof converse.RosterContact) { contact.subscribe(message); } }); }, sendContactAddIQ: function (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 VCard 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 }); _.map(groups, function (group) { iq.c('group').t(group).up(); }); converse.connection.sendIQ(iq, callback, errback); }, addContact: function (jid, name, groups, attributes) { /* 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. */ var deferred = new $.Deferred(); groups = groups || []; name = _.isEmpty(name)? jid: name; this.sendContactAddIQ(jid, name, groups, function (iq) { var contact = this.create(_.extend({ ask: undefined, fullname: name, groups: groups, jid: jid, requesting: false, subscription: 'none' }, attributes), {sort: false}); deferred.resolve(contact); }.bind(this), function (err) { alert(__("Sorry, there was an error while trying to add "+name+" as a contact.")); converse.log(err); deferred.resolve(err); } ); return deferred.promise(); }, addResource: function (bare_jid, resource) { var item = this.get(bare_jid), resources; if (item) { resources = item.get('resources'); if (resources) { if (_.indexOf(resources, resource) === -1) { resources.push(resource); item.set({'resources': resources}); } } else { item.set({'resources': [resource]}); } } }, subscribeBack: function (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 this.addContact(bare_jid, '', [], { 'subscription': 'from' }).done(function (contact) { if (contact instanceof converse.RosterContact) { contact.authorize().subscribe(); } }); } }, getNumOnlineContacts: function () { var count = 0, ignored = ['offline', 'unavailable'], models = this.models, models_length = models.length, i; if (converse.show_only_online_users) { ignored = _.union(ignored, ['dnd', 'xa', 'away']); } for (i=0; i 0) { this.$el.show(); } }, toggle: function (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: CLOSED}); $el.removeClass("icon-opened").addClass("icon-closed"); } else { $el.removeClass("icon-closed").addClass("icon-opened"); this.model.save({state: OPENED}); this.filter( converse.rosterview.$('.roster-filter').val(), converse.rosterview.$('.filter-type').val() ); } }, onContactGroupChange: function (contact) { var in_this_group = _.contains(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 (contact) { if ((this.model.get('name') === HEADER_PENDING_CONTACTS) && contact.get('subscription') !== 'from') { this.model.contacts.remove(contact.get('id')); } }, onContactRequestChange: function (contact) { if ((this.model.get('name') === HEADER_REQUESTING_CONTACTS) && !contact.get('requesting')) { this.model.contacts.remove(contact.get('id')); } }, onRemove: function (contact) { this.remove(contact.get('id')); if (this.model.contacts.length === 0) { this.$el.hide(); } } }); this.RosterGroups = Backbone.Collection.extend({ model: converse.RosterGroup, comparator: 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 = _.contains(special_groups, a); var b_is_special = _.contains(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_CURRENT_CONTACTS) ? 1 : -1; } else if (a_is_special && !b_is_special) { return (a === HEADER_CURRENT_CONTACTS) ? -1 : 1; } } }); this.RosterView = Backbone.Overview.extend({ tagName: 'div', id: 'converse-roster', events: { "keydown .roster-filter": "liveFilter", "click .onX": "clearFilter", "mousemove .x": "togglePointer", "change .filter-type": "changeFilterType" }, initialize: function () { this.roster_handler_ref = this.registerRosterHandler(); this.rosterx_handler_ref = this.registerRosterXHandler(); this.presence_ref = this.registerPresenceHandler(); 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); this.$roster = $(''); }, unregisterHandlers: function () { converse.connection.deleteHandler(this.roster_handler_ref); delete this.roster_handler_ref; converse.connection.deleteHandler(this.rosterx_handler_ref); delete this.rosterx_handler_ref; converse.connection.deleteHandler(this.presence_ref); delete this.presence_ref; }, update: _.debounce(function () { var $count = $('#online-count'); $count.text('('+converse.roster.getNumOnlineContacts()+')'); if (!$count.is(':visible')) { $count.show(); } if (this.$roster.parent().length === 0) { this.$el.append(this.$roster.show()); } return this.showHideFilter(); }, converse.animate ? 100 : 0), render: function () { this.$el.html(converse.templates.roster({ placeholder: __('Type to filter'), label_contacts: LABEL_CONTACTS, label_groups: LABEL_GROUPS })); 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.addClass('no-contact-requests'); } return this; }, fetch: function () { this.model.fetch({ silent: true, // We use the success handler to handle groups that were added, // we need to first have all groups before positionFetchedGroups // will work properly. success: function (collection, resp, options) { if (collection.length !== 0) { this.positionFetchedGroups(collection, resp, options); } converse.roster.fetch({ add: true, success: function (collection) { if (collection.length === 0) { /* We don't have any roster contacts stored in sessionStorage, * so lets fetch the roster from the XMPP server. We pass in * 'sendPresence' as callback method, because after initially * fetching the roster we are ready to receive presence * updates from our contacts. */ converse.roster.fetchFromServer(function () { converse.xmppstatus.sendPresence(); }); } else if (converse.send_initial_presence) { /* We're not going to fetch the roster again because we have * it already cached in sessionStorage, but we still need to * send out a presence stanza because this is a new session. * See: https://github.com/jcbrand/converse.js/issues/536 */ converse.xmppstatus.sendPresence(); } } }); }.bind(this) }); return this; }, changeFilterType: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.clearFilter(); this.filter( this.$('.roster-filter').val(), ev.target.value ); }, tog: function (v) { return v?'addClass':'removeClass'; }, togglePointer: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } var el = ev.target; $(el)[this.tog(el.offsetWidth-18 < ev.clientX-el.getBoundingClientRect().left)]('onX'); }, filter: function (query, type) { query = query.toLowerCase(); if (type === 'groups') { _.each(this.getAll(), function (view, idx) { if (view.model.get('name').toLowerCase().indexOf(query.toLowerCase()) === -1) { view.hide(); } else if (view.model.contacts.length > 0) { view.show(); } }); } else { _.each(this.getAll(), function (view) { view.filter(query, type); }); } }, liveFilter: _.debounce(function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } var $filter = this.$('.roster-filter'); var q = $filter.val(); var t = this.$('.filter-type').val(); $filter[this.tog(q)]('x'); this.filter(q, t); }, 300), clearFilter: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); $(ev.target).removeClass('x onX').val(''); } this.filter(''); }, showHideFilter: function () { if (!this.$el.is(':visible')) { return; } var $filter = this.$('.roster-filter'); var $type = this.$('.filter-type'); var visible = $filter.is(':visible'); if (visible && $filter.val().length > 0) { // Don't hide if user is currently filtering. return; } if (this.$roster.hasScrollBar()) { if (!visible) { $filter.show(); $type.show(); } } else { $filter.hide(); $type.hide(); } return this; }, reset: function () { converse.roster.reset(); this.removeAll(); this.$roster = $(''); this.render().update(); return this; }, registerRosterHandler: function () { converse.connection.addHandler( converse.roster.onRosterPush.bind(converse.roster), Strophe.NS.ROSTER, 'iq', "set" ); }, registerRosterXHandler: function () { var t = 0; converse.connection.addHandler( function (msg) { window.setTimeout( function () { converse.connection.flush(); converse.roster.subscribeToSuggestedItems.bind(converse.roster)(msg); }, t ); t += $(msg).find('item').length*250; return true; }, Strophe.NS.ROSTERX, 'message', null ); }, registerPresenceHandler: function () { converse.connection.addHandler( function (presence) { converse.roster.presenceHandler(presence); return true; }.bind(this), null, 'presence', null); }, onGroupAdd: function (group) { var view = new converse.RosterGroupView({model: group}); this.add(group.get('name'), view.render()); this.positionGroup(view); }, onContactAdd: function (contact) { this.addRosterContact(contact).update(); if (!contact.get('vcard_updated')) { // This will update the vcard, which triggers a change // request which will rerender the roster contact. converse.getVCard(contact.get('jid')); } }, onContactChange: function (contact) { this.updateChatBox(contact).update(); if (_.has(contact.changed, 'subscription')) { if (contact.changed.subscription === 'from') { this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); } else if (_.contains(['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.liveFilter(); }, updateChatBox: function (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 (model, resp, options) { /* 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. */ model.sort(); model.each(function (group, idx) { var view = this.get(group.get('name')); if (!view) { view = new converse.RosterGroupView({model: group}); this.add(group.get('name'), view.render()); } if (idx === 0) { this.$roster.append(view.$el); } else { this.appendGroup(view); } }.bind(this)); }, positionGroup: function (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 (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 (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 (contact, name) { this.getGroup(name).contacts.add(contact); }, addExistingContact: function (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 (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; } }); this.XMPPStatus = Backbone.Model.extend({ initialize: function () { this.set({ 'status' : this.getStatus() }); this.on('change', function (item) { if (this.get('fullname') === undefined) { converse.getVCard( null, // No 'to' attr when getting one's own vCard function (iq, jid, fullname, image, image_type, url) { this.save({'fullname': fullname}); }.bind(this) ); } if (_.has(item.changed, 'status')) { converse.emit('statusChanged', this.get('status')); } if (_.has(item.changed, 'status_message')) { converse.emit('statusMessageChanged', this.get('status_message')); } }.bind(this)); }, constructPresence: function (type, status_message) { if (typeof type === 'undefined') { type = this.get('status') || 'online'; } if (typeof status_message === 'undefined') { status_message = this.get('status_message'); } var presence; // Most of these presence types are actually not explicitly sent, // but I add all of them here fore 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'}); if (status_message) { presence.c('show').t(type); } } else { if (type === 'online') { presence = $pres(); } else { presence = $pres().c('show').t(type).up(); } if (status_message) { presence.c('status').t(status_message); } } return presence; }, sendPresence: function (type, status_message) { converse.connection.send(this.constructPresence(type, status_message)); }, setStatus: function (value) { this.sendPresence(value); this.save({'status': value}); }, getStatus: function () { return this.get('status') || 'online'; }, setStatusMessage: function (status_message) { this.sendPresence(this.getStatus(), status_message); var prev_status = this.get('status_message'); this.save({'status_message': status_message}); if (this.xhr_custom_status) { $.ajax({ url: this.xhr_custom_status_url, type: 'POST', data: {'msg': status_message} }); } if (prev_status === status_message) { this.trigger("update-status-ui", this); } } }); this.XMPPStatusView = Backbone.View.extend({ el: "span#xmpp-status-holder", events: { "click a.choose-xmpp-status": "toggleOptions", "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm", "submit #set-custom-xmpp-status": "setStatusMessage", "click .dropdown dd ul li a": "setStatus" }, initialize: function () { 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); }, render: function () { // Replace the default dropdown with something nicer var $select = this.$el.find('select#select-xmpp-status'), chat_status = this.model.get('status') || 'offline', options = $('option', $select), $options_target, options_list = []; this.$el.html(converse.templates.choose_status()); this.$el.find('#fancy-xmpp-status-select') .html(converse.templates.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') })); // iterate through all the