/**
* @license almond 0.3.2 Copyright jQuery Foundation and other contributors.
* Released under MIT license, http://github.com/requirejs/almond/LICENSE
*/
//Going sloppy to avoid 'use strict' string cost, but strict practices should
//be followed.
/*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, normalizedBaseParts,
baseParts = baseName && baseName.split("/"),
map = config.map,
starMap = (map && map['*']) || {};
//Adjust any relative paths.
if (name) {
name = name.split('/');
lastIndex = name.length - 1;
// If wanting node ID compatibility, strip .js from end
// of IDs. Have to do this here, and not in nameToUrl
// because node allows either .js or non .js to map
// to same file.
if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
}
// Starts with a '.' so need the baseName
if (name[0].charAt(0) === '.' && baseParts) {
//Convert baseName to array, and lop off the last part,
//so that . matches that '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.
normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
name = normalizedBaseParts.concat(name);
}
//start trimDots
for (i = 0; i < name.length; i++) {
part = name[i];
if (part === '.') {
name.splice(i, 1);
i -= 1;
} else if (part === '..') {
// If at the start, or previous value is still ..,
// keep them so that when converted to a path it may
// still work when converted to a path, even though
// as an ID it is less than ideal. In larger point
// releases, may be better to just kick out an error.
if (i === 0 || (i === 1 && name[2] === '..') || name[i - 1] === '..') {
continue;
} else if (i > 0) {
name.splice(i - 1, 2);
i -= 2;
}
}
}
//end trimDots
name = name.join('/');
}
//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 text 2.0.15 Copyright jQuery Foundation and other contributors.
* Released under MIT license, http://github.com/requirejs/text/LICENSE
*/
/*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()) || {};
function useDefault(value, defaultValue) {
return value === undefined || value === '' ? defaultValue : value;
}
//Allow for default ports for http and https.
function isSamePort(protocol1, port1, protocol2, port2) {
if (port1 === port2) {
return true;
} else if (protocol1 === protocol2) {
if (protocol1 === 'http') {
return useDefault(port1, '80') === useDefault(port2, '80');
} else if (protocol1 === 'https') {
return useDefault(port1, '443') === useDefault(port2, '443');
}
}
return false;
}
text = {
version: '2.0.15',
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) || isSamePort(uProtocol, uPort, protocol, 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';
}
__p+='\n';
}
return __p;
}; });
define('tpl!trimmed_chat', [],function () { return function(obj){
var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
with(obj||{}){
__p+='\n'+
((__t=(num_unread))==null?'':__t)+
'\n\n '+
((__t=( title ))==null?'':__t)+
'\n\n';
}
return __p;
}; });
define('tpl!vcard', [],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!chatbox_minimize', [],function () { return function(obj){
var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
with(obj||{}){
__p+='\n';
}
return __p;
}; });
define("converse-templates", [
"tpl!action",
"tpl!add_contact_dropdown",
"tpl!add_contact_form",
"tpl!change_status_message",
"tpl!chat_status",
"tpl!chatarea",
"tpl!chatbox",
"tpl!chatroom",
"tpl!chatroom_form",
"tpl!chatroom_password_form",
"tpl!chatroom_sidebar",
"tpl!chatrooms_tab",
"tpl!chats_panel",
"tpl!choose_status",
"tpl!contacts_panel",
"tpl!contacts_tab",
"tpl!controlbox",
"tpl!controlbox_toggle",
"tpl!field",
"tpl!form_captcha",
"tpl!form_checkbox",
"tpl!form_input",
"tpl!form_select",
"tpl!form_textarea",
"tpl!form_username",
"tpl!group_header",
"tpl!info",
"tpl!login_panel",
"tpl!login_tab",
"tpl!message",
"tpl!new_day",
"tpl!occupant",
"tpl!pending_contact",
"tpl!pending_contacts",
"tpl!register_panel",
"tpl!register_tab",
"tpl!registration_form",
"tpl!registration_request",
"tpl!requesting_contact",
"tpl!requesting_contacts",
"tpl!room_description",
"tpl!room_item",
"tpl!room_panel",
"tpl!roster",
"tpl!roster_item",
"tpl!search_contact",
"tpl!select_option",
"tpl!status_option",
"tpl!toggle_chats",
"tpl!toolbar",
"tpl!toolbar_otr",
"tpl!trimmed_chat",
"tpl!vcard",
// Can be removed together with converse-minimize.js
// if minimization/trimming features not needed (for example for mobile
// apps).
"tpl!chatbox_minimize",
], function () {
return {
action: arguments[0],
add_contact_dropdown: arguments[1],
add_contact_form: arguments[2],
change_status_message: arguments[3],
chat_status: arguments[4],
chatarea: arguments[5],
chatbox: arguments[6],
chatroom: arguments[7],
chatroom_form: arguments[8],
chatroom_password_form: arguments[9],
chatroom_sidebar: arguments[10],
chatrooms_tab: arguments[11],
chats_panel: arguments[12],
choose_status: arguments[13],
contacts_panel: arguments[14],
contacts_tab: arguments[15],
controlbox: arguments[16],
controlbox_toggle: arguments[17],
field: arguments[18],
form_captcha: arguments[19],
form_checkbox: arguments[20],
form_input: arguments[21],
form_select: arguments[22],
form_textarea: arguments[23],
form_username: arguments[24],
group_header: arguments[25],
info: arguments[26],
login_panel: arguments[27],
login_tab: arguments[28],
message: arguments[29],
new_day: arguments[30],
occupant: arguments[31],
pending_contact: arguments[32],
pending_contacts: arguments[33],
register_panel: arguments[34],
register_tab: arguments[35],
registration_form: arguments[36],
registration_request: arguments[37],
requesting_contact: arguments[38],
requesting_contacts: arguments[39],
room_description: arguments[40],
room_item: arguments[41],
room_panel: arguments[42],
roster: arguments[43],
roster_item: arguments[44],
search_contact: arguments[45],
select_option: arguments[46],
status_option: arguments[47],
toggle_chats: arguments[48],
toolbar: arguments[49],
toolbar_otr: arguments[50],
trimmed_chat: arguments[51],
vcard: arguments[52],
chatbox_minimize: arguments[53]
};
});
/*global escape, locales, Jed */
(function (root, factory) {
define('utils',[
"jquery",
"jquery.browser",
"underscore",
"converse-templates"
], factory);
}(this, function ($, dummy, _, templates) {
"use strict";
var XFORM_TYPE_MAP = {
'text-private': 'password',
'text-single': 'text',
'fixed': 'label',
'boolean': 'checkbox',
'hidden': 'hidden',
'jid-multi': 'textarea',
'list-single': 'dropdown',
'list-multi': 'dropdown'
};
$.expr[':'].emptyVal = function(obj){
return obj.value === '';
};
$.fn.hasScrollBar = function() {
if (!$.contains(document, this.get(0))) {
return false;
}
if(this.parent().height() < this.get(0).scrollHeight) {
return true;
}
return false;
};
$.fn.addHyperlinks = function () {
if (this.length > 0) {
this.each(function (i, obj) {
var x = $(obj).html();
var list = x.match(/\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<]{2,200}\b/g );
if (list) {
for (i=0; i'+ list[i] + '' );
}
}
$(obj).html(x);
});
}
return this;
};
$.fn.addEmoticons = function (allowed) {
if (allowed) {
if (this.length > 0) {
this.each(function (i, obj) {
var text = $(obj).html();
text = text.replace(/>:\)/g, '');
text = text.replace(/:\)/g, '');
text = text.replace(/:\-\)/g, '');
text = text.replace(/;\)/g, '');
text = text.replace(/;\-\)/g, '');
text = text.replace(/:D/g, '');
text = text.replace(/:\-D/g, '');
text = text.replace(/:P/g, '');
text = text.replace(/:\-P/g, '');
text = text.replace(/:p/g, '');
text = text.replace(/:\-p/g, '');
text = text.replace(/8\)/g, '');
text = text.replace(/:S/g, '');
text = text.replace(/:\\/g, '');
text = text.replace(/:\/ /g, '');
text = text.replace(/>:\(/g, '');
text = text.replace(/:\(/g, '');
text = text.replace(/:\-\(/g, '');
text = text.replace(/:O/g, '');
text = text.replace(/:\-O/g, '');
text = text.replace(/\=\-O/g, '');
text = text.replace(/\(\^.\^\)b/g, '');
text = text.replace(/<3/g, '');
$(obj).html(text);
});
}
}
return this;
};
var utils = {
// Translation machinery
// ---------------------
__: function (str) {
if (typeof Jed === "undefined") {
return str;
}
// FIXME: this can be refactored to take the i18n obj as a
// parameter.
// Translation factory
if (typeof this.i18n === "undefined") {
this.i18n = locales.en;
}
if (typeof this.i18n === "string") {
this.i18n = $.parseJSON(this.i18n);
}
if (typeof this.jed === "undefined") {
this.jed = new Jed(this.i18n);
}
var t = this.jed.translate(str);
if (arguments.length>1) {
return t.fetch.apply(t, [].slice.call(arguments,1));
} else {
return t.fetch();
}
},
___: function (str) {
/* XXX: This is part of a hack to get gettext to scan strings to be
* translated. Strings we cannot send to the function above because
* they require variable interpolation and we don't yet have the
* variables at scan time.
*
* See actionInfoMessages in src/converse-muc.js
*/
return str;
},
isHeadlineMessage: function (message) {
var $message = $(message),
from_jid = $message.attr('from');
if ($message.attr('type') === 'headline' ||
// Some servers (I'm looking at you Prosody) don't set the message
// type to "headline" when sending server messages. For now we
// check if an @ signal is included, and if not, we assume it's
// a headline message.
( $message.attr('type') !== 'error' &&
typeof from_jid !== 'undefined' &&
from_jid.indexOf('@') === -1
)) {
return true;
}
return false;
},
refreshWebkit: function () {
/* This works around a webkit bug. Refreshes the browser's viewport,
* otherwise chatboxes are not moved along when one is closed.
*/
if ($.browser.webkit) {
var conversejs = document.getElementById('conversejs');
conversejs.style.display = 'none';
var tmp = conversejs.offsetHeight; // jshint ignore:line
conversejs.style.display = 'block';
}
},
webForm2xForm: function (field) {
/* Takes an HTML DOM and turns it into an XForm field.
*
* Parameters:
* (DOMElement) field - the field to convert
*/
var $input = $(field), value;
if ($input.is('[type=checkbox]')) {
value = $input.is(':checked') && 1 || 0;
} else if ($input.is('textarea')) {
value = [];
var lines = $input.val().split('\n');
for( var vk=0; vk into consideration
var options = [], j, $options, $values, value, values;
if ($field.attr('type') === 'list-single' || $field.attr('type') === 'list-multi') {
values = [];
$values = $field.children('value');
for (j=0; j<$values.length; j++) {
values.push($($values[j]).text());
}
$options = $field.children('option');
for (j=0; j<$options.length; j++) {
value = $($options[j]).find('value').text();
options.push(templates.select_option({
value: value,
label: $($options[j]).attr('label'),
selected: (values.indexOf(value) >= 0),
required: $field.find('required').length
}));
}
return templates.form_select({
name: $field.attr('var'),
label: $field.attr('label'),
options: options.join(''),
multiple: ($field.attr('type') === 'list-multi'),
required: $field.find('required').length
});
} else if ($field.attr('type') === 'fixed') {
return $('
').text($field.find('value').text());
} else if ($field.attr('type') === 'jid-multi') {
return templates.form_textarea({
name: $field.attr('var'),
label: $field.attr('label') || '',
value: $field.find('value').text(),
required: $field.find('required').length
});
} else if ($field.attr('type') === 'boolean') {
return templates.form_checkbox({
name: $field.attr('var'),
type: XFORM_TYPE_MAP[$field.attr('type')],
label: $field.attr('label') || '',
checked: $field.find('value').text() === "1" && 'checked="1"' || '',
required: $field.find('required').length
});
} else if ($field.attr('type') && $field.attr('var') === 'username') {
return templates.form_username({
domain: ' @'+this.domain,
name: $field.attr('var'),
type: XFORM_TYPE_MAP[$field.attr('type')],
label: $field.attr('label') || '',
value: $field.find('value').text(),
required: $field.find('required').length
});
} else if ($field.attr('type')) {
return templates.form_input({
name: $field.attr('var'),
type: XFORM_TYPE_MAP[$field.attr('type')],
label: $field.attr('label') || '',
value: $field.find('value').text(),
required: $field.find('required').length
});
} else {
if ($field.attr('var') === 'ocr') { // Captcha
return _.reduce(_.map($field.find('uri'),
$.proxy(function (uri) {
return templates.form_captcha({
label: this.$field.attr('label'),
name: this.$field.attr('var'),
data: this.$stanza.find('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]').text(),
type: uri.getAttribute('type'),
required: this.$field.find('required').length
});
}, {'$stanza': $stanza, '$field': $field})
),
function (memo, num) { return memo + num; }, ''
);
}
}
}
};
utils.contains.not = function (attr, query) {
return function (item) {
return !(utils.contains(attr, query)(item));
};
};
return utils;
}));
if (!String.prototype.endsWith) {
String.prototype.endsWith = function(searchString, position) {
var subjectString = this.toString();
if (position === undefined || position > subjectString.length) {
position = subjectString.length;
}
position -= searchString.length;
var lastIndex = subjectString.indexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position;
};
}
String.prototype.splitOnce = function (delimiter) {
var components = this.split(delimiter);
return [components.shift(), components.join(delimiter)];
};
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};
}
;
define("polyfill", function(){});
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global Backbone, define, window, document, locales */
(function (root, factory) {
// Two modules are loaded as dependencies.
//
// * **converse-dependencies**: A list of dependencies converse.js depends on.
// The path to this module is in main.js and the module itself can be overridden.
// * **converse-templates**: The HTML templates used by converse.js.
//
// The dependencies are then split up and passed into the factory function,
// which contains and instantiates converse.js.
define("converse-core", [
"jquery",
"underscore",
"polyfill",
"utils",
"moment_with_locales",
"strophe",
"converse-templates",
"strophe.disco",
"backbone.browserStorage",
"backbone.overview",
], factory);
}(this, function ($, _, dummy, utils, moment, Strophe, templates) {
/*
* Cannot use this due to Safari bug.
* See https://github.com/jcbrand/converse.js/issues/196
*/
// "use strict";
// Strophe globals
var $build = Strophe.$build;
var $iq = Strophe.$iq;
var $pres = Strophe.$pres;
var b64_sha1 = Strophe.SHA1.b64_sha1;
Strophe = Strophe.Strophe;
// Use Mustache style syntax for variable interpolation
/* Configuration of underscore templates (this config is distinct to the
* config of requirejs-tpl in main.js). This one is for normal inline templates.
*/
_.templateSettings = {
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
};
// We create an object to act as the "this" context for event handlers (as
// defined below and accessible via converse_api.listen).
// We don't want the inner converse object to be the context, since it
// contains sensitive information, and we don't want it to be something in
// the DOM or window, because then anyone can trigger converse events.
var event_context = {};
var converse = {
plugins: {},
initialized_plugins: [],
templates: templates,
emit: function (evt, data) {
$(event_context).trigger(evt, data);
},
once: function (evt, handler) {
$(event_context).one(evt, handler);
},
on: function (evt, handler) {
if (_.contains(['ready', 'initialized'], evt)) {
converse.log('Warning: The "'+evt+'" event has been deprecated and will be removed, please use "connected".');
}
$(event_context).bind(evt, handler);
},
off: function (evt, handler) {
$(event_context).unbind(evt, handler);
}
};
// Module-level constants
converse.STATUS_WEIGHTS = {
'offline': 6,
'unavailable': 5,
'xa': 4,
'away': 3,
'dnd': 2,
'chat': 1, // We currently don't differentiate between "chat" and "online"
'online': 1
};
converse.LOGIN = "login";
converse.ANONYMOUS = "anonymous";
converse.PREBIND = "prebind";
converse.OPENED = 'opened';
converse.CLOSED = 'closed';
var PRETTY_CONNECTION_STATUS = {
0: 'ERROR',
1: 'CONNECTING',
2: 'CONNFAIL',
3: 'AUTHENTICATING',
4: 'AUTHFAIL',
5: 'CONNECTED',
6: 'DISCONNECTED',
7: 'DISCONNECTING',
8: 'ATTACHED',
9: 'REDIRECT'
};
converse.log = function (txt, level) {
var logger;
if (typeof console === "undefined" || typeof console.log === "undefined") {
logger = { log: function () {}, error: function () {} };
} else {
logger = console;
}
if (converse.debug) {
if (level === 'error') {
logger.log('ERROR: '+txt);
} else {
logger.log(txt);
}
}
};
converse.initialize = function (settings, callback) {
"use strict";
var converse = this;
var unloadevent;
if ('onpagehide' in window) {
// Pagehide gets thrown in more cases than unload. Specifically it
// gets thrown when the page is cached and not just
// closed/destroyed. It's the only viable event on mobile Safari.
// https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
unloadevent = 'pagehide';
} else if ('onbeforeunload' in window) {
unloadevent = 'beforeunload';
} else if ('onunload' in window) {
unloadevent = 'unload';
}
// Logging
Strophe.log = function (level, msg) { converse.log(level+' '+msg, level); };
Strophe.error = function (msg) { converse.log(msg, 'error'); };
// Add Strophe Namespaces
Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2');
Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx');
Strophe.addNamespace('XFORM', 'jabber:x:data');
Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
// Instance level constants
this.TIMEOUTS = { // Set as module attr so that we can override in tests.
'PAUSED': 20000,
'INACTIVE': 90000
};
// XEP-0085 Chat states
// http://xmpp.org/extensions/xep-0085.html
this.INACTIVE = 'inactive';
this.ACTIVE = 'active';
this.COMPOSING = 'composing';
this.PAUSED = 'paused';
this.GONE = 'gone';
// Detect support for the user's locale
// ------------------------------------
this.isConverseLocale = function (locale) { return typeof locales[locale] !== "undefined"; };
this.isMomentLocale = function (locale) { return moment.locale() !== moment.locale(locale); };
this.user_settings = settings; // Save the user settings so that they can be used by plugins
this.wrappedChatBox = function (chatbox) {
/* Wrap a chatbox for outside consumption (i.e. so that it can be
* returned via the API.
*/
if (!chatbox) { return; }
var view = converse.chatboxviews.get(chatbox.get('jid'));
return {
'close': view.close.bind(view),
'focus': view.focus.bind(view),
'get': chatbox.get.bind(chatbox),
'open': view.show.bind(view),
'set': chatbox.set.bind(chatbox)
};
};
this.isLocaleAvailable = function (locale, available) {
/* Check whether the locale or sub locale (e.g. en-US, en) is supported.
*
* Parameters:
* (Function) available - returns a boolean indicating whether the locale is supported
*/
if (available(locale)) {
return locale;
} else {
var sublocale = locale.split("-")[0];
if (sublocale !== locale && available(sublocale)) {
return sublocale;
}
}
};
this.detectLocale = function (library_check) {
/* Determine which locale is supported by the user's system as well
* as by the relevant library (e.g. converse.js or moment.js).
*
* Parameters:
* (Function) library_check - returns a boolean indicating whether the locale is supported
*/
var locale, i;
if (window.navigator.userLanguage) {
locale = this.isLocaleAvailable(window.navigator.userLanguage, library_check);
}
if (window.navigator.languages && !locale) {
for (i=0; i 0) {
converse.idle_seconds = 0;
}
if (!converse.connection.authenticated) {
// We can't send out any stanzas when there's no authenticated connection.
// converse can happen when the connection reconnects.
return;
}
if (converse.inactive) {
converse.sendCSI(converse.ACTIVE);
}
if (converse.auto_changed_status === true) {
converse.auto_changed_status = false;
converse.xmppstatus.setStatus('online');
}
};
this.onEverySecond = function () {
/* An interval handler running every second.
* Used for CSI and the auto_away and auto_xa features.
*/
if (!converse.connection.authenticated) {
// We can't send out any stanzas when there's no authenticated connection.
// This can happen when the connection reconnects.
return;
}
var stat = converse.xmppstatus.getStatus();
converse.idle_seconds++;
if (converse.csi_waiting_time > 0 &&
converse.idle_seconds > converse.csi_waiting_time &&
!converse.inactive) {
converse.sendCSI(converse.INACTIVE);
}
if (converse.auto_away > 0 &&
converse.idle_seconds > converse.auto_away &&
stat !== 'away' && stat !== 'xa') {
converse.auto_changed_status = true;
converse.xmppstatus.setStatus('away');
} else if (converse.auto_xa > 0 &&
converse.idle_seconds > converse.auto_xa && stat !== 'xa') {
converse.auto_changed_status = true;
converse.xmppstatus.setStatus('xa');
}
};
this.registerIntervalHandler = function () {
/* Set an interval of one second and register a handler for it.
* Required for the auto_away, auto_xa and csi_waiting_time features.
*/
if (converse.auto_away < 1 && converse.auto_xa < 1 && converse.csi_waiting_time < 1) {
// Waiting time of less then one second means features aren't used.
return;
}
converse.idle_seconds = 0;
converse.auto_changed_status = false; // Was the user's status changed by converse.js?
$(window).on('click mousemove keypress focus'+unloadevent, converse.onUserActivity);
converse.everySecondTrigger = window.setInterval(converse.onEverySecond, 1000);
};
this.giveFeedback = function (message, klass) {
$('.conn-feedback').each(function (idx, el) {
var $el = $(el);
$el.addClass('conn-feedback').text(message);
if (klass) {
$el.addClass(klass);
} else {
$el.removeClass('error');
}
});
converse.emit('feedback', {'message': message, 'klass': klass});
};
this.rejectPresenceSubscription = function (jid, message) {
/* Reject or cancel another user's subscription to our presence updates.
* Parameters:
* (String) jid - The Jabber ID of the user whose subscription
* is being canceled.
* (String) message - An optional message to the user
*/
var pres = $pres({to: jid, type: "unsubscribed"});
if (message && message !== "") { pres.c("status").t(message); }
converse.connection.send(pres);
};
this.reconnect = _.debounce(function (condition) {
converse.connection.disconnect('re-connecting');
converse.connection.reset();
converse.log('Attempting to reconnect');
converse.giveFeedback(__('Attempting to reconnect'), 'error');
converse.clearSession();
converse._tearDown();
if (converse.authentication !== "prebind") {
converse.attemptNonPreboundSession();
} else if (converse.prebind_url) {
converse.startNewBOSHSession();
}
}, 1000);
this.onDisconnected = function (condition) {
if (!converse.auto_reconnect) { return; }
if (converse.disconnection_cause === Strophe.Status.CONNFAIL) {
converse.reconnect(condition);
return 'reconnecting';
} else {
converse.emit('disconnected');
return 'disconnected';
}
};
this.onConnectStatusChanged = function (status, condition, reconnect) {
converse.log("Status changed to: "+PRETTY_CONNECTION_STATUS[status]);
if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) {
// By default we always want to send out an initial presence stanza.
converse.send_initial_presence = true;
delete converse.disconnection_cause;
if ((typeof reconnect !== 'undefined') && (reconnect)) {
converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached');
converse.onReconnected();
} else {
converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached');
if (converse.connection.restored) {
converse.send_initial_presence = false; // No need to send an initial presence stanza when
// we're restoring an existing session.
}
converse.onConnected();
}
} else if (status === Strophe.Status.DISCONNECTED) {
converse.onDisconnected(condition);
} else if (status === Strophe.Status.ERROR) {
converse.giveFeedback(__('Error'), 'error');
} else if (status === Strophe.Status.CONNECTING) {
converse.giveFeedback(__('Connecting'));
} else if (status === Strophe.Status.AUTHENTICATING) {
converse.giveFeedback(__('Authenticating'));
} else if (status === Strophe.Status.AUTHFAIL) {
converse.giveFeedback(__('Authentication Failed'), 'error');
converse.connection.disconnect(__('Authentication Failed'));
converse.disconnection_cause = Strophe.Status.AUTHFAIL;
} else if (status === Strophe.Status.CONNFAIL) {
if (converse.connection.authenticated) {
// Only set the disconnection_cause if we're still
// authenticated. If we're not, then the user logged out,
// and it's therefore not strictly speaking a connection
// failure (so we won't automatically reconnect).
converse.disconnection_cause = Strophe.Status.CONNFAIL;
}
} else if (status === Strophe.Status.DISCONNECTING) {
if (condition) {
converse.giveFeedback(condition, 'error');
}
}
};
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 + ") ");
}
window.blur();
window.focus();
} else if (document.title.search(/^Messages \(\d+\) /) !== -1) {
document.title = document.title.replace(/^Messages \(\d+\) /, "");
}
};
this.incrementMsgCounter = function () {
this.msg_counter += 1;
this.updateMsgCounter();
};
this.clearMsgCounter = function () {
this.msg_counter = 0;
this.updateMsgCounter();
};
this.initStatus = function () {
var deferred = new $.Deferred();
this.xmppstatus = new this.XMPPStatus();
var id = b64_sha1('converse.xmppstatus-'+converse.bare_jid);
this.xmppstatus.id = id; // Appears to be necessary for backbone.browserStorage
this.xmppstatus.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
this.xmppstatus.fetch({
success: deferred.resolve,
error: deferred.resolve
});
converse.emit('statusInitialized');
return deferred.promise();
};
this.initSession = function () {
this.session = new this.Session();
var id = b64_sha1('converse.bosh-session');
this.session.id = id; // Appears to be necessary for backbone.browserStorage
this.session.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
this.session.fetch();
};
this.clearSession = function () {
if (this.roster) {
this.roster.browserStorage._clear();
}
this.session.browserStorage._clear();
};
this.logOut = function () {
converse.chatboxviews.closeAllChatBoxes();
converse.clearSession();
if (typeof converse.connection !== 'undefined') {
converse.connection.disconnect();
converse.connection.reset();
}
};
this.registerGlobalEventHandlers = function () {
$(window).on("blur focus", function (ev) {
if ((converse.windowState !== ev.type) && (ev.type === 'focus')) {
converse.clearMsgCounter();
}
converse.windowState = ev.type;
});
};
this.afterReconnected = function () {
this.chatboxes.registerMessageHandler();
this.xmppstatus.sendPresence();
this.giveFeedback(__('Contacts'));
};
this.onReconnected = function () {
// We need to re-register all the event handlers on the newly
// created connection.
var deferred = new $.Deferred();
this.initStatus().done(function () {
this.afterReconnected();
deferred.resolve();
}.bind(this));
converse.emit('reconnected');
return deferred.promise();
};
this.enableCarbons = function () {
/* Ask the XMPP server to enable Message Carbons
* See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling
*/
if (!this.message_carbons || this.session.get('carbons_enabled')) {
return;
}
var carbons_iq = new Strophe.Builder('iq', {
from: this.connection.jid,
id: 'enablecarbons',
type: 'set'
})
.c('enable', {xmlns: Strophe.NS.CARBONS});
this.connection.addHandler(function (iq) {
if ($(iq).find('error').length > 0) {
converse.log('ERROR: An error occured while trying to enable message carbons.');
} else {
this.session.save({carbons_enabled: true});
converse.log('Message carbons have been enabled.');
}
}.bind(this), null, "iq", null, "enablecarbons");
this.connection.send(carbons_iq);
};
this.onStatusInitialized = function (deferred) {
this.registerIntervalHandler();
this.roster = new this.RosterContacts();
this.roster.browserStorage = new Backbone.BrowserStorage[this.storage](
b64_sha1('converse.contacts-'+this.bare_jid));
this.chatboxes.onConnected();
this.giveFeedback(__('Contacts'));
if (typeof this.callback === 'function') {
// A callback method may be passed in via the
// converse.initialize method.
// XXX: Can we use $.Deferred instead of this callback?
if (this.connection.service === 'jasmine tests') {
// XXX: Call back with the internal converse object. This
// object should never be exposed to production systems.
// 'jasmine tests' is an invalid http bind service value,
// so we're sure that this is just for tests.
this.callback(this);
} else {
this.callback();
}
}
deferred.resolve();
converse.emit('initialized');
};
this.onConnected = function (callback) {
// When reconnecting, there might be some open chat boxes. We don't
// know whether these boxes are of the same account or not, so we
// close them now.
var deferred = new $.Deferred();
// XXX: ran into an issue where a returned PubSub BOSH response was
// not received by the browser. The solution was to flush the
// connection early on. I don't know what the underlying cause of
// this issue is, and whether it's a Strophe.js or Prosody bug.
// My suspicion is that Prosody replies to an invalid/expired
// Request, which is why the browser then doesn't receive it.
// In any case, flushing here (sending out a new BOSH request)
// solves the problem.
converse.connection.flush();
/* Called as soon as a new connection has been established, either
* by logging in or by attaching to an existing BOSH session.
*/
this.chatboxviews.closeAllChatBoxes();
this.jid = this.connection.jid;
this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
this.resource = Strophe.getResourceFromJid(this.connection.jid);
this.domain = Strophe.getDomainFromJid(this.connection.jid);
this.features = new this.Features();
this.enableCarbons();
this.initStatus().done(_.bind(this.onStatusInitialized, this, deferred));
converse.emit('connected');
converse.emit('ready'); // BBB: Will be removed.
return deferred.promise();
};
this.RosterContact = Backbone.Model.extend({
initialize: function (attributes, options) {
var jid = attributes.jid;
var bare_jid = Strophe.getBareJidFromJid(jid);
var resource = Strophe.getResourceFromJid(jid);
attributes.jid = bare_jid;
this.set(_.extend({
'id': bare_jid,
'jid': bare_jid,
'fullname': bare_jid,
'chat_status': 'offline',
'user_id': Strophe.getNodeFromJid(jid),
'resources': resource ? [resource] : [],
'groups': [],
'image_type': 'image/png',
'image': "iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAIAAABt+uBvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gwHCy455JBsggAABkJJREFUeNrtnM1PE1sUwHvvTD8otWLHST/Gimi1CEgr6M6FEWuIBo2pujDVsNDEP8GN/4MbN7oxrlipG2OCgZgYlxAbkRYw1KqkIDRCSkM7nXvvW8x7vjyNeQ9m7p1p3z1LQk/v/Dhz7vkEXL161cHl9wI5Ag6IA+KAOCAOiAPigDggLhwQB2S+iNZ+PcYY/SWEEP2HAAAIoSAIoihCCP+ngDDGtVotGAz29/cfOXJEUZSOjg6n06lp2sbGRqlUWlhYyGazS0tLbrdbEASrzgksyeYJId3d3el0uqenRxRFAAAA4KdfIIRgjD9+/Pj8+fOpqSndslofEIQwHA6Pjo4mEon//qmFhYXHjx8vLi4ihBgDEnp7e9l8E0Jo165dQ0NDd+/eDYVC2/qsJElDQ0OEkKWlpa2tLZamxAhQo9EIBoOjo6MXL17csZLe3l5FUT59+lQul5l5JRaAVFWNRqN37tw5ceKEQVWRSOTw4cOFQuHbt2+iKLYCIISQLMu3b99OJpOmKAwEAgcPHszn8+vr6wzsiG6UQQhxuVyXLl0aGBgwUW0sFstkMl6v90fo1KyAMMYDAwPnzp0zXfPg4GAqlWo0Gk0MiBAiy/L58+edTqf5Aa4onj59OhaLYYybFRCEMBaL0fNxBw4cSCQStN0QRUBut3t4eJjq6U+dOiVJElVPRBFQIBDo6+ujCqirqyscDlONGykC2lYyYSR6pBoQQapHZwAoHo/TuARYAOrs7GQASFEUqn6aIiBJkhgA6ujooFpUo6iaTa7koFwnaoWadLNe81tbWwzoaJrWrICWl5cZAFpbW6OabVAEtLi4yABQsVjUNK0pAWWzWQaAcrlcswKanZ1VVZUqHYRQEwOq1Wpv3ryhCmh6erpcLjdrNl+v1ycnJ+l5UELI27dvv3//3qxxEADgy5cvExMT9Mznw4cPtFtAdAPFarU6Pj5eKpVM17yxsfHy5cvV1VXazXu62gVBKBQKT58+rdVqJqrFGL948eLdu3dU8/g/H4FBUaJYLAqC0NPTY9brMD4+PjY25mDSracOCABACJmZmXE6nUePHjWu8NWrV48ePSKEsGlAs7Agfd5nenq6Wq0mk0kjDzY2NvbkyRMIIbP2PLvhBUEQ8vl8NpuNx+M+n29bzhVjvLKycv/+/YmJCcazQuwA6YzW1tYmJyf1SY+2trZ/rRk1Go1SqfT69esHDx4UCgVmNaa/zZ/9ABUhRFXVYDB48uTJeDweiUQkSfL7/T9MA2NcqVTK5fLy8vL8/PzU1FSxWHS5XJaM4wGr9sUwxqqqer3eUCgkSZJuUBBCfTRvc3OzXC6vrKxUKhWn02nhCJ5lM4oQQo/HgxD6+vXr58+fHf8sDOp+HQDg8XgclorFU676dKLlo6yWRdItIBwQB8QBcUCtfosRQjRNQwhhjPUC4w46WXryBSHU1zgEQWBz99EFhDGu1+t+v//48ePxeFxRlD179ng8nh0Efgiher2+vr6ur3HMzMysrq7uTJVdACGEurq6Ll++nEgkPB7Pj9jPoDHqOxyqqubz+WfPnuVyuV9XPeyeagAAAoHArVu3BgcHab8CuVzu4cOHpVKJUnfA5GweY+xyuc6cOXPv3r1IJMLAR8iyPDw8XK/Xi8Wiqqqmm5KZgBBC7e3tN27cuHbtGuPVpf7+/lAoNDs7W61WzfVKpgHSSzw3b95MpVKW3MfRaDQSiczNzVUqFRMZmQOIEOL1eq9fv3727FlL1t50URRFluX5+flqtWpWEGAOIFEUU6nUlStXLKSjy759+xwOx9zcnKZpphzGHMzhcDiTydgk9r1w4YIp7RPTAAmCkMlk2FeLf/tIEKbTab/fbwtAhJBoNGrutpNx6e7uPnTokC1eMU3T0um0DZPMkZER6wERQnw+n/FFSxpy7Nix3bt3WwwIIcRgIWnHkkwmjecfRgGx7DtuV/r6+iwGhDHev3+/bQF1dnYaH6E2CkiWZdsC2rt3r8WAHA5HW1ubbQGZcjajgOwTH/4qNko1Wlg4IA6IA+KAOKBWBUQIsfNojyliKIoRRfH9+/dut9umf3wzpoUNNQ4BAJubmwz+ic+OxefzWWlBhJD29nbug7iT5sIBcUAcEAfEAXFAHBAHxOVn+QMrmWpuPZx12gAAAABJRU5ErkJggg==",
'status': ''
}, attributes));
this.on('destroy', function () { this.removeFromRoster(); }.bind(this));
this.on('change:chat_status', function (item) {
converse.emit('contactStatusChanged', item.attributes);
});
},
subscribe: function (message) {
/* Send a presence subscription request to this roster contact
*
* Parameters:
* (String) message - An optional message to explain the
* reason for the subscription request.
*/
this.save('ask', "subscribe"); // ask === 'subscribe' Means we have ask to subscribe to them.
var pres = $pres({to: this.get('jid'), type: "subscribe"});
if (message && message !== "") {
pres.c("status").t(message).up();
}
var nick = converse.xmppstatus.get('fullname');
if (nick && nick !== "") {
pres.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
}
converse.connection.send(pres);
return this;
},
ackSubscribe: function () {
/* Upon receiving the presence stanza of type "subscribed",
* the user SHOULD acknowledge receipt of that subscription
* state notification by sending a presence stanza of type
* "subscribe" to the contact
*/
converse.connection.send($pres({
'type': 'subscribe',
'to': this.get('jid')
}));
},
ackUnsubscribe: function (jid) {
/* Upon receiving the presence stanza of type "unsubscribed",
* the user SHOULD acknowledge receipt of that subscription state
* notification by sending a presence stanza of type "unsubscribe"
* this step lets the user's server know that it MUST no longer
* send notification of the subscription state change to the user.
* Parameters:
* (String) jid - The Jabber ID of the user who is unsubscribing
*/
converse.connection.send($pres({'type': 'unsubscribe', 'to': this.get('jid')}));
this.destroy(); // Will cause removeFromRoster to be called.
},
unauthorize: function (message) {
/* Unauthorize this contact's presence subscription
* Parameters:
* (String) message - Optional message to send to the person being unauthorized
*/
converse.rejectPresenceSubscription(this.get('jid'), message);
return this;
},
authorize: function (message) {
/* Authorize presence subscription
* Parameters:
* (String) message - Optional message to send to the person being authorized
*/
var pres = $pres({to: this.get('jid'), type: "subscribed"});
if (message && message !== "") {
pres.c("status").t(message);
}
converse.connection.send(pres);
return this;
},
removeResource: function (resource) {
var resources = this.get('resources'), idx;
if (resource) {
idx = _.indexOf(resources, resource);
if (idx !== -1) {
resources.splice(idx, 1);
this.save({'resources': resources});
}
}
else {
// if there is no resource (resource is null), it probably
// means that the user is now completely offline. To make sure
// that there isn't any "ghost" resources left, we empty the array
this.save({'resources': []});
return 0;
}
return resources.length;
},
removeFromRoster: function (callback) {
/* Instruct the XMPP server to remove this contact from our roster
* Parameters:
* (Function) callback
*/
var iq = $iq({type: 'set'})
.c('query', {xmlns: Strophe.NS.ROSTER})
.c('item', {jid: this.get('jid'), subscription: "remove"});
converse.connection.sendIQ(iq, callback, callback);
return this;
}
});
this.RosterContacts = Backbone.Collection.extend({
model: converse.RosterContact,
comparator: function (contact1, contact2) {
var name1, name2;
var status1 = contact1.get('chat_status') || 'offline';
var status2 = contact2.get('chat_status') || 'offline';
if (converse.STATUS_WEIGHTS[status1] === converse.STATUS_WEIGHTS[status2]) {
name1 = contact1.get('fullname').toLowerCase();
name2 = contact2.get('fullname').toLowerCase();
return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
} else {
return converse.STATUS_WEIGHTS[status1] < converse.STATUS_WEIGHTS[status2] ? -1 : 1;
}
},
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 IQ is returned
* (Function) errback - A function to call if an error occured
*/
name = _.isEmpty(name)? jid: name;
var iq = $iq({type: 'set'})
.c('query', {xmlns: Strophe.NS.ROSTER})
.c('item', { jid: jid, name: name });
_.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,
fullname = this.get('fullname'),
is_groupchat = $message.attr('type') === 'groupchat',
msgid = $message.attr('id'),
chat_state = $message.find(converse.COMPOSING).length && converse.COMPOSING ||
$message.find(converse.PAUSED).length && converse.PAUSED ||
$message.find(converse.INACTIVE).length && converse.INACTIVE ||
$message.find(converse.ACTIVE).length && converse.ACTIVE ||
$message.find(converse.GONE).length && converse.GONE,
stamp, time, sender, from;
if (is_groupchat) {
from = Strophe.unescapeNode(Strophe.getResourceFromJid($message.attr('from')));
} else {
from = Strophe.getBareJidFromJid($message.attr('from'));
}
if (_.isEmpty(fullname)) {
fullname = from;
}
if (delayed) {
stamp = $delay.attr('stamp');
time = stamp;
} else {
time = moment().format();
}
if ((is_groupchat && from === this.get('nick')) || (!is_groupchat && from === converse.bare_jid)) {
sender = 'me';
} else {
sender = 'them';
}
return this.messages.create({
chat_state: chat_state,
delayed: delayed,
fullname: fullname,
message: body || undefined,
msgid: msgid,
sender: sender,
time: time
});
}
});
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');
},
chatBoxMayBeShown: function (chatbox) {
return true;
},
onChatBoxesFetched: function (collection) {
/* Show chat boxes upon receiving them from sessionStorage
*
* This method gets overridden entirely in src/converse-controlbox.js
* if the controlbox plugin is active.
*/
collection.each(function (chatbox) {
if (this.chatBoxMayBeShown(chatbox)) {
chatbox.trigger('show');
}
}.bind(this));
converse.emit('chatBoxesFetched');
},
onConnected: function () {
this.browserStorage = new Backbone.BrowserStorage[converse.storage](
b64_sha1('converse.chatboxes-'+converse.bare_jid));
this.registerMessageHandler();
this.fetch({
add: true,
success: this.onChatBoxesFetched.bind(this)
});
},
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);
if (to_resource && to_resource !== converse.resource) {
converse.log(
'onMessage: Ignoring incoming message intended for a different resource: '+to_jid,
'info'
);
return true;
} else if (from_jid === converse.connection.jid) {
// FIXME: Forwarded messages should be sent to specific
// resources, not broadcasted
converse.log(
"onMessage: Ignoring incoming message sent from this client's JID: "+from_jid,
'info'
);
return true;
} else if (utils.isHeadlineMessage(message)) {
// XXX: Ideally we wouldn't have to check for headline
// messages, but Prosody sends headline messages with the
// wrong type ('chat'), so we need to filter them out here.
converse.log(
"onMessage: Ignoring incoming headline message sent with type 'chat' from JID: "+from_jid,
'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.
}
chatbox.createMessage($message, $delay);
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("destroy", this.removeChat, 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 = $('
'));
}
if (spinner === true) {
this.$content.append('');
} else if (spinner === false) {
this.$content.find('span.spinner').remove();
}
return this.scrollDown();
},
handleChatStateMessage: function (message) {
if (message.get('chat_state') === converse.COMPOSING) {
this.showStatusNotification(message.get('fullname')+' '+__('is typing'));
this.clear_status_timeout = window.setTimeout(this.clearStatusNotification.bind(this), 10000);
} else if (message.get('chat_state') === converse.PAUSED) {
this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing'));
} else if (_.contains([converse.INACTIVE, converse.ACTIVE], message.get('chat_state'))) {
this.$content.find('div.chat-event').remove();
} else if (message.get('chat_state') === converse.GONE) {
this.showStatusNotification(message.get('fullname')+' '+__('has gone away'));
}
},
shouldShowOnTextMessage: function () {
return !this.$el.is(':visible');
},
handleTextMessage: function (message) {
this.showMessage(_.clone(message.attributes));
if ((message.get('sender') !== 'me') && (converse.windowState === 'blur')) {
converse.incrementMsgCounter();
}
if (this.shouldShowOnTextMessage()) {
this.show();
}
},
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')) {
this.handleChatStateMessage(message);
} else {
this.handleTextMessage(message);
}
},
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(converse.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 === converse.COMPOSING) {
this.chat_state_timeout = window.setTimeout(
this.setChatState.bind(this), converse.TIMEOUTS.PAUSED, converse.PAUSED);
} else if (state === converse.PAUSED) {
this.chat_state_timeout = window.setTimeout(
this.setChatState.bind(this), converse.TIMEOUTS.INACTIVE, converse.INACTIVE);
}
if (!no_save && this.model.get('chat_state') !== state) {
this.model.set('chat_state', state);
}
return this;
},
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 !== '') {
// XXX: leaky abstraction from MUC
if (this.model.get('type') === 'chatroom') {
this.onChatRoomMessageSubmitted(message);
} else {
this.onMessageSubmitted(message);
}
converse.emit('messageSend', message);
}
this.setChatState(converse.ACTIVE);
// XXX: leaky abstraction from MUC
} else if (this.model.get('type') !== '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(converse.COMPOSING, ev.keyCode === KEY.FORWARD_SLASH);
}
},
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();
}
}
},
onStatusChanged: function (item) {
this.showStatusMessage();
converse.emit('contactStatusMessageChanged', {
'contact': item.attributes,
'message': item.get('status')
});
},
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) {
// Immediately sending the chat state, because the
// model is going to be destroyed afterwards.
this.model.set('chat_state', converse.INACTIVE);
this.sendChatState();
this.model.destroy();
}
this.remove();
converse.emit('chatBoxClosed', this);
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;
},
afterShown: function () {
if (converse.connection.connected) {
// Without a connection, we haven't yet initialized
// localstorage
this.model.save();
}
this.setChatState(converse.ACTIVE);
this.scrollDown();
if (focus) {
this.focus();
}
},
_show: function (focus) {
/* Inner show method that gets debounced */
if (this.$el.is(':visible') && this.$el.css('opacity') === "1") {
if (focus) { this.focus(); }
return;
}
this.$el.fadeIn(this.afterShown.bind(this));
},
show: function (focus) {
if (typeof this.debouncedShow === 'undefined') {
/* We wrap the method in a debouncer and set it on the
* instance, so that we have it debounced per instance.
* Debouncing it on the class-level is too broad.
*/
this.debouncedShow = _.debounce(this._show, 250, true);
}
this.debouncedShow.apply(this, arguments);
return this;
},
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;
}
});
}
});
}));
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global Backbone, define */
(function (root, factory) {
define("converse-rosterview", ["converse-core", "converse-api"], factory);
}(this, function (converse, converse_api) {
"use strict";
var $ = converse_api.env.jQuery,
utils = converse_api.env.utils,
Strophe = converse_api.env.Strophe,
$iq = converse_api.env.$iq,
b64_sha1 = converse_api.env.b64_sha1,
_ = converse_api.env._,
__ = utils.__.bind(converse);
converse_api.plugins.add('rosterview', {
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
afterReconnected: function () {
this.rosterview.registerRosterXHandler();
this.rosterview.registerPresenceHandler();
this._super.afterReconnected.apply(this, arguments);
}
},
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
this.updateSettings({
allow_chat_pending_contacts: false,
allow_contact_removal: true,
show_toolbar: true,
});
var STATUSES = {
'dnd': __('This contact is busy'),
'online': __('This contact is online'),
'offline': __('This contact is offline'),
'unavailable': __('This contact is unavailable'),
'xa': __('This contact is away for an extended period'),
'away': __('This contact is away')
};
var DESC_GROUP_TOGGLE = __('Click to hide these contacts');
var LABEL_CONTACTS = __('Contacts');
var LABEL_GROUPS = __('Groups');
var HEADER_CURRENT_CONTACTS = __('My contacts');
var HEADER_PENDING_CONTACTS = __('Pending contacts');
var HEADER_REQUESTING_CONTACTS = __('Contact requests');
var HEADER_UNGROUPED = __('Ungrouped');
var HEADER_WEIGHTS = {};
HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 0;
HEADER_WEIGHTS[HEADER_UNGROUPED] = 1;
HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 2;
HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3;
converse.RosterFilter = Backbone.Model.extend({
initialize: function () {
this.set({
'filter_text': '',
'filter_type': 'contacts',
'chat_state': ''
});
},
});
converse.RosterFilterView = Backbone.View.extend({
tagName: 'span',
events: {
"keydown .roster-filter": "liveFilter",
"click .onX": "clearFilter",
"mousemove .x": "toggleX",
"change .filter-type": "changeTypeFilter",
"change .state-type": "changeChatStateFilter"
},
initialize: function () {
this.model.on('change', this.render, this);
},
render: function () {
this.$el.html(converse.templates.roster(
_.extend(this.model.toJSON(), {
placeholder: __('Filter'),
label_contacts: LABEL_CONTACTS,
label_groups: LABEL_GROUPS,
label_state: __('State'),
label_any: __('Any'),
label_online: __('Online'),
label_chatty: __('Chatty'),
label_busy: __('Busy'),
label_away: __('Away'),
label_xa: __('Extended Away'),
label_offline: __('Offline')
})
));
var $roster_filter = this.$('.roster-filter');
$roster_filter[this.tog($roster_filter.val())]('x');
return this.$el;
},
tog: function (v) {
return v?'addClass':'removeClass';
},
toggleX: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
var el = ev.target;
$(el)[this.tog(el.offsetWidth-18 < ev.clientX-el.getBoundingClientRect().left)]('onX');
},
changeChatStateFilter: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
this.model.save({
'chat_state': this.$('.state-type').val()
});
},
changeTypeFilter: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
var type = ev.target.value;
if (type === 'state') {
this.model.save({
'filter_type': type,
'chat_state': this.$('.state-type').val()
});
} else {
this.model.save({
'filter_type': type,
'filter_text': this.$('.roster-filter').val(),
});
}
},
liveFilter: _.debounce(function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
this.model.save({
'filter_type': this.$('.filter-type').val(),
'filter_text': this.$('.roster-filter').val()
});
}, 250),
isActive: function () {
/* Returns true if the filter is enabled (i.e. if the user
* has added values to the filter).
*/
if (this.model.get('filter_type') === 'state' ||
this.model.get('filter_text')) {
return true;
}
return false;
},
show: function () {
if (this.$el.is(':visible')) { return this; }
this.$el.show();
return this;
},
hide: function () {
if (!this.$el.is(':visible')) { return this; }
if (this.$('.roster-filter').val().length > 0) {
// Don't hide if user is currently filtering.
return;
}
this.model.save({
'filter_text': '',
'chat_state': ''
});
this.$el.hide();
return this;
},
clearFilter: function (ev) {
if (ev && ev.preventDefault) {
ev.preventDefault();
$(ev.target).removeClass('x onX').val('');
}
this.model.save({
'filter_text': ''
});
}
});
converse.RosterView = Backbone.Overview.extend({
tagName: 'div',
id: 'converse-roster',
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 = $('
');
// Create a model on which we can store filter properties
var model = new converse.RosterFilter();
model.id = b64_sha1('converse.rosterfilter'+converse.bare_jid);
model.browserStorage = new Backbone.BrowserStorage.local(this.filter.id);
this.filter_view = new converse.RosterFilterView({'model': model});
this.filter_view.model.on('change', this.updateFilter, this);
this.filter_view.model.fetch();
},
render: function () {
this.$el.html(this.filter_view.render());
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;
},
updateFilter: _.debounce(function () {
/* Filter the roster again.
* Called whenever the filter settings have been changed or
* when contacts have been added, removed or changed.
*
* Debounced so that it doesn't get called for every
* contact fetched from browser storage.
*/
var type = this.filter_view.model.get('filter_type');
if (type === 'state') {
this.filter(this.filter_view.model.get('chat_state'), type);
} else {
this.filter(this.filter_view.model.get('filter_text'), type);
}
}, 100),
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 () {
if (this.$roster.parent().length === 0) {
this.$el.append(this.$roster.show());
}
return this.showHideFilter();
}, converse.animate ? 100 : 0),
showHideFilter: function () {
if (!this.$el.is(':visible')) {
return;
}
if (this.$roster.hasScrollBar()) {
this.filter_view.show();
} else if (!this.filter_view.isActive()) {
this.filter_view.hide();
}
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(
converse.xmppstatus.sendPresence.bind(converse.xmppstatus));
} 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;
},
filter: function (query, type) {
// First we make sure the filter is restored to its
// original state
_.each(this.getAll(), function (view) {
if (view.model.contacts.length > 0) {
view.show().filter('');
}
});
// Now we can filter
query = query.toLowerCase();
if (type === 'groups') {
_.each(this.getAll(), function (view, idx) {
if (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);
});
}
},
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();
this.updateFilter();
},
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.updateFilter();
},
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;
}
});
converse.RosterContactView = Backbone.View.extend({
tagName: 'dd',
events: {
"click .accept-xmpp-request": "acceptRequest",
"click .decline-xmpp-request": "declineRequest",
"click .open-chat": "openChat",
"click .remove-xmpp-contact": "removeContact"
},
initialize: function () {
this.model.on("change", this.render, this);
this.model.on("remove", this.remove, this);
this.model.on("destroy", this.remove, this);
this.model.on("open", this.openChat, this);
},
render: function () {
if (!this.mayBeShown()) {
this.$el.hide();
return this;
}
var item = this.model,
ask = item.get('ask'),
chat_status = item.get('chat_status'),
requesting = item.get('requesting'),
subscription = item.get('subscription');
var classes_to_remove = [
'current-xmpp-contact',
'pending-xmpp-contact',
'requesting-xmpp-contact'
].concat(_.keys(STATUSES));
_.each(classes_to_remove,
function (cls) {
if (this.el.className.indexOf(cls) !== -1) {
this.$el.removeClass(cls);
}
}, this);
this.$el.addClass(chat_status).data('status', chat_status);
if ((ask === 'subscribe') || (subscription === 'from')) {
/* ask === 'subscribe'
* Means we have asked to subscribe to them.
*
* subscription === 'from'
* They are subscribed to use, but not vice versa.
* We assume that there is a pending subscription
* from us to them (otherwise we're in a state not
* supported by converse.js).
*
* So in both cases the user is a "pending" contact.
*/
this.$el.addClass('pending-xmpp-contact');
this.$el.html(converse.templates.pending_contact(
_.extend(item.toJSON(), {
'desc_remove': __('Click to remove this contact'),
'allow_chat_pending_contacts': converse.allow_chat_pending_contacts
})
));
} else if (requesting === true) {
this.$el.addClass('requesting-xmpp-contact');
this.$el.html(converse.templates.requesting_contact(
_.extend(item.toJSON(), {
'desc_accept': __("Click to accept this contact request"),
'desc_decline': __("Click to decline this contact request"),
'allow_chat_pending_contacts': converse.allow_chat_pending_contacts
})
));
converse.controlboxtoggle.showControlBox();
} else if (subscription === 'both' || subscription === 'to') {
this.$el.addClass('current-xmpp-contact');
this.$el.removeClass(_.without(['both', 'to'], subscription)[0]).addClass(subscription);
this.$el.html(converse.templates.roster_item(
_.extend(item.toJSON(), {
'desc_status': STATUSES[chat_status||'offline'],
'desc_chat': __('Click to chat with this contact'),
'desc_remove': __('Click to remove this contact'),
'title_fullname': __('Name'),
'allow_contact_removal': converse.allow_contact_removal
})
));
}
return this;
},
isGroupCollapsed: function () {
/* Check whether the group in which this contact appears is
* collapsed.
*/
// XXX: this sucks and is fragile.
// It's because I tried to do the "right thing"
// and use definition lists to represent roster groups.
// If roster group items were inside the group elements, we
// would simplify things by not having to check whether the
// group is collapsed or not.
var name = this.$el.prevAll('dt:first').data('group');
var group = converse.rosterview.model.where({'name': name})[0];
if (group.get('state') === converse.CLOSED) {
return true;
}
return false;
},
mayBeShown: function () {
/* Return a boolean indicating whether this contact should
* generally be visible in the roster.
*
* It doesn't check for the more specific case of whether
* the group it's in is collapsed (see isGroupCollapsed).
*/
var chatStatus = this.model.get('chat_status');
if ((converse.show_only_online_users && chatStatus !== 'online') ||
(converse.hide_offline_users && chatStatus === 'offline')) {
// If pending or requesting, show
if ((this.model.get('ask') === 'subscribe') ||
(this.model.get('subscription') === 'from') ||
(this.model.get('requesting') === true)) {
return true;
}
return false;
}
return true;
},
openChat: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
return converse.chatboxviews.showChat(this.model.attributes);
},
removeContact: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
if (!converse.allow_contact_removal) { return; }
var result = confirm(__("Are you sure you want to remove this contact?"));
if (result === true) {
var iq = $iq({type: 'set'})
.c('query', {xmlns: Strophe.NS.ROSTER})
.c('item', {jid: this.model.get('jid'), subscription: "remove"});
converse.connection.sendIQ(iq,
function (iq) {
this.model.destroy();
this.remove();
}.bind(this),
function (err) {
alert(__("Sorry, there was an error while trying to remove "+name+" as a contact."));
converse.log(err);
}
);
}
},
acceptRequest: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
converse.roster.sendContactAddIQ(
this.model.get('jid'),
this.model.get('fullname'),
[],
function () { this.model.authorize().subscribe(); }.bind(this)
);
},
declineRequest: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
var result = confirm(__("Are you sure you want to decline this contact request?"));
if (result === true) {
this.model.unauthorize().destroy();
}
return this;
}
});
converse.RosterGroup = Backbone.Model.extend({
initialize: function (attributes, options) {
this.set(_.extend({
description: DESC_GROUP_TOGGLE,
state: converse.OPENED
}, attributes));
// Collection of contacts belonging to this group.
this.contacts = new converse.RosterContacts();
}
});
converse.RosterGroupView = Backbone.Overview.extend({
tagName: 'dt',
className: 'roster-group',
events: {
"click a.group-toggle": "toggle"
},
initialize: function () {
this.model.contacts.on("add", this.addContact, this);
this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this);
this.model.contacts.on("change:requesting", this.onContactRequestChange, this);
this.model.contacts.on("change:chat_status", function (contact) {
// This might be optimized by instead of first sorting,
// finding the correct position in positionContact
this.model.contacts.sort();
this.positionContact(contact).render();
}, this);
this.model.contacts.on("destroy", this.onRemove, this);
this.model.contacts.on("remove", this.onRemove, this);
converse.roster.on('change:groups', this.onContactGroupChange, this);
},
render: function () {
this.$el.attr('data-group', this.model.get('name'));
this.$el.html(
$(converse.templates.group_header({
label_group: this.model.get('name'),
desc_group_toggle: this.model.get('description'),
toggle_state: this.model.get('state')
}))
);
return this;
},
addContact: function (contact) {
var view = new converse.RosterContactView({model: contact});
this.add(contact.get('id'), view);
view = this.positionContact(contact).render();
if (view.mayBeShown()) {
if (this.model.get('state') === converse.CLOSED) {
if (view.$el[0].style.display !== "none") { view.$el.hide(); }
if (!this.$el.is(':visible')) { this.$el.show(); }
} else {
if (this.$el[0].style.display !== "block") { this.show(); }
}
}
},
positionContact: function (contact) {
/* Place the contact's DOM element in the correct alphabetical
* position amongst the other contacts in this group.
*/
var view = this.get(contact.get('id'));
var index = this.model.contacts.indexOf(contact);
view.$el.detach();
if (index === 0) {
this.$el.after(view.$el);
} else if (index === (this.model.contacts.length-1)) {
this.$el.nextUntil('dt').last().after(view.$el);
} else {
this.$el.nextUntil('dt').eq(index).before(view.$el);
}
return view;
},
show: function () {
this.$el.show();
_.each(this.getAll(), function (view) {
if (view.mayBeShown() && !view.isGroupCollapsed()) {
view.$el.show();
}
});
return this;
},
hide: function () {
this.$el.nextUntil('dt').addBack().hide();
},
filter: function (q, type) {
/* Filter the group's contacts based on the query "q".
* The query is matched against the contact's full name.
* If all contacts are filtered out (i.e. hidden), then the
* group must be filtered out as well.
*/
var matches;
if (q.length === 0) {
if (this.model.get('state') === converse.OPENED) {
this.model.contacts.each(function (item) {
var view = this.get(item.get('id'));
if (view.mayBeShown() && !view.isGroupCollapsed()) {
view.$el.show();
}
}.bind(this));
}
this.showIfNecessary();
} else {
q = q.toLowerCase();
if (type === 'state') {
matches = this.model.contacts.filter(
utils.contains.not('chat_status', q)
);
} else {
matches = this.model.contacts.filter(
utils.contains.not('fullname', q)
);
}
if (matches.length === this.model.contacts.length) {
// hide the whole group
this.hide();
} else {
_.each(matches, function (item) {
this.get(item.get('id')).$el.hide();
}.bind(this));
_.each(this.model.contacts.reject(utils.contains.not('fullname', q)), function (item) {
this.get(item.get('id')).$el.show();
}.bind(this));
this.showIfNecessary();
}
}
},
showIfNecessary: function () {
if (!this.$el.is(':visible') && this.model.contacts.length > 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: converse.CLOSED});
$el.removeClass("icon-opened").addClass("icon-closed");
} else {
$el.removeClass("icon-closed").addClass("icon-opened");
this.model.save({state: converse.OPENED});
this.filter(
converse.rosterview.$('.roster-filter').val() || '',
converse.rosterview.$('.filter-type').val()
);
}
},
onContactGroupChange: function (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();
}
}
});
converse.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;
}
}
});
}
});
}));
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define, Backbone */
(function (root, factory) {
define("converse-controlbox", [
"converse-core",
"converse-api",
// TODO: remove the next two dependencies
"converse-rosterview",
"converse-chatview"
], factory);
}(this, function (converse, converse_api) {
"use strict";
// Strophe methods for building stanzas
var Strophe = converse_api.env.Strophe,
b64_sha1 = converse_api.env.b64_sha1,
utils = converse_api.env.utils;
// Other necessary globals
var $ = converse_api.env.jQuery,
_ = converse_api.env._,
__ = utils.__.bind(converse),
moment = converse_api.env.moment;
converse_api.plugins.add('controlbox', {
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
initSession: function () {
this.controlboxtoggle = new this.ControlBoxToggle();
this._super.initSession.apply(this, arguments);
},
initConnection: function () {
this._super.initConnection.apply(this, arguments);
if (this.connection) {
this.addControlBox();
}
},
onDisconnected: function () {
var result = this._super.onDisconnected.apply(this, arguments);
if (result === 'disconnected') {
converse._tearDown();
var view = converse.chatboxviews.get('controlbox');
view.model.set({connected:false});
view.$('#controlbox-tabs').empty();
view.renderLoginPanel();
}
},
_tearDown: function () {
this._super._tearDown.apply(this, arguments);
if (this.rosterview) {
this.rosterview.unregisterHandlers();
// Removes roster groups
this.rosterview.model.off().reset();
this.rosterview.undelegateEvents().remove();
}
},
clearSession: function () {
this._super.clearSession.apply(this, arguments);
if (typeof this.connection !== 'undefined' && this.connection.connected) {
this.chatboxes.get('controlbox').save({'connected': false});
}
},
ChatBoxes: {
chatBoxMayBeShown: function (chatbox) {
return this._super.chatBoxMayBeShown.apply(this, arguments) &&
chatbox.get('id') !== 'controlbox';
},
onChatBoxesFetched: function (collection, resp) {
this._super.onChatBoxesFetched.apply(this, arguments);
if (!_.include(_.pluck(resp, 'id'), 'controlbox')) {
this.add({
id: 'controlbox',
box_id: 'controlbox'
});
}
this.get('controlbox').save({connected:true});
},
},
ChatBoxViews: {
onChatBoxAdded: function (item) {
if (item.get('box_id') === 'controlbox') {
var view = this.get(item.get('id'));
if (view) {
view.model = item;
view.initialize();
return view;
} else {
view = new converse.ControlBoxView({model: item});
return this.add(item.get('id'), view);
}
} else {
return this._super.onChatBoxAdded.apply(this, arguments);
}
},
closeAllChatBoxes: function () {
this.each(function (view) {
if (view.model.get('id') !== 'controlbox') {
view.close();
}
});
return this;
},
getChatBoxWidth: function (view) {
var controlbox = this.get('controlbox');
if (view.model.get('id') === 'controlbox') {
/* We return the width of the controlbox or its toggle,
* depending on which is visible.
*/
if (!controlbox || !controlbox.$el.is(':visible')) {
return converse.controlboxtoggle.$el.outerWidth(true);
} else {
return controlbox.$el.outerWidth(true);
}
} else {
return this._super.getChatBoxWidth.apply(this, arguments);
}
}
},
ChatBox: {
initialize: function () {
if (this.get('id') === 'controlbox') {
this.set({
'time_opened': moment(0).valueOf(),
'num_unread': 0
});
} else {
this._super.initialize.apply(this, arguments);
}
},
},
ChatBoxView: {
insertIntoPage: function () {
this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
return this;
}
}
},
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
var converse = this.converse;
this.updateSettings({
allow_logout: true,
default_domain: undefined,
show_controlbox_by_default: false,
sticky_controlbox: false,
xhr_user_search: false,
xhr_user_search_url: ''
});
var LABEL_CONTACTS = __('Contacts');
converse.addControlBox = function () {
return converse.chatboxes.add({
id: 'controlbox',
box_id: 'controlbox',
closed: !converse.show_controlbox_by_default
});
};
converse.ControlBoxView = converse.ChatBoxView.extend({
tagName: 'div',
className: 'chatbox',
id: 'controlbox',
events: {
'click a.close-chatbox-button': 'close',
'click ul#controlbox-tabs li a': 'switchTab',
},
initialize: function () {
this.$el.insertAfter(converse.controlboxtoggle.$el);
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 () {
this.$el.html(converse.templates.controlbox(
_.extend(this.model.toJSON(), {
sticky_controlbox: converse.sticky_controlbox
}))
);
if (!converse.connection.connected || !converse.connection.authenticated || converse.connection.disconnecting) {
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.
*/
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.loginpanel = new converse.LoginPanel({
'$parent': this.$el.find('.controlbox-panes'),
'model': this
});
this.loginpanel.render();
if ($feedback.length && $feedback.text() !== __('Connecting')) {
this.$('.conn-feedback').replaceWith($feedback);
}
return this;
},
renderContactsPanel: function () {
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();
},
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;
},
onControlBoxToggleHidden: function () {
this.$el.show('fast', function () {
converse.controlboxtoggle.updateOnlineCount();
utils.refreshWebkit();
converse.emit('controlBoxOpened', this);
}.bind(this));
},
show: function () {
converse.controlboxtoggle.hide(
this.onControlBoxToggleHidden.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;
}
});
converse.LoginPanel = Backbone.View.extend({
tagName: 'div',
id: "login-dialog",
className: 'controlbox-pane',
events: {
'submit form#converse-login': 'authenticate'
},
initialize: function (cfg) {
cfg.$parent.html(this.$el.html(
converse.templates.login_panel({
'LOGIN': converse.LOGIN,
'ANONYMOUS': converse.ANONYMOUS,
'PREBIND': converse.PREBIND,
'auto_login': converse.auto_login,
'authentication': converse.authentication,
'label_username': __('XMPP Username:'),
'label_password': __('Password:'),
'label_anon_login': __('Click here to log in anonymously'),
'label_login': __('Log In'),
'placeholder_username': (converse.locked_domain || converse.default_domain) && __('Username') || __('user@server'),
'placeholder_password': __('password')
})
));
this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
},
render: function () {
this.$tabs.append(converse.templates.login_tab({label_sign_in: __('Sign in')}));
this.$el.find('input#jid').focus();
if (!this.$el.is(':visible')) {
this.$el.show();
}
return this;
},
authenticate: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
var $form = $(ev.target);
if (converse.authentication === converse.ANONYMOUS) {
this.connect($form, converse.jid, null);
return;
}
var $jid_input = $form.find('input[name=jid]'),
jid = $jid_input.val(),
$pw_input = $form.find('input[name=password]'),
password = $pw_input.val(),
errors = false;
if (! jid) {
errors = true;
$jid_input.addClass('error');
}
if (! password) {
errors = true;
$pw_input.addClass('error');
}
if (errors) { return; }
if (converse.locked_domain) {
jid = Strophe.escapeNode(jid) + '@' + converse.locked_domain;
} else if (converse.default_domain && jid.indexOf('@') === -1) {
jid = jid + '@' + converse.default_domain;
}
this.connect($form, jid, password);
return false;
},
connect: function ($form, jid, password) {
var resource;
if ($form) {
$form.find('input[type=submit]').hide().after('');
}
if (jid) {
resource = Strophe.getResourceFromJid(jid);
if (!resource) {
jid = jid.toLowerCase() + converse.generateResource();
} else {
jid = Strophe.getBareJidFromJid(jid).toLowerCase()+'/'+resource;
}
}
converse.connection.connect(jid, password, converse.onConnectStatusChanged);
},
remove: function () {
this.$tabs.empty();
this.$el.parent().empty();
}
});
converse.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
')
}
});
$el.on('typeahead:selected', function (ev, suggestion, dname) {
var reason = prompt(
__(___('You are about to invite %1$s to the chat room "%2$s". '), suggestion.value, this.model.get('id')) +
__("You may optionally include a message, explaining the reason for the invitation.")
);
if (reason !== null) {
this.chatroomview.directInvite(suggestion.jid, reason);
}
$(ev.target).typeahead('val', '');
}.bind(this));
return this;
}
});
converse.RoomsPanel = Backbone.View.extend({
/* Backbone View which renders the "Rooms" tab and accompanying
* panel in the control box.
*
* In this panel, chat rooms can be listed, joined and new rooms
* can be created.
*/
tagName: 'div',
className: 'controlbox-pane',
id: 'chatrooms',
events: {
'submit form.add-chatroom': 'createChatRoom',
'click input#show-rooms': 'showRooms',
'click a.open-room': 'createChatRoom',
'click a.room-info': 'showRoomInfo',
'change input[name=server]': 'setDomain',
'change input[name=nick]': 'setNick'
},
initialize: function (cfg) {
this.$parent = cfg.$parent;
this.model.on('change:muc_domain', this.onDomainChange, this);
this.model.on('change:nick', this.onNickChange, this);
},
render: function () {
this.$parent.append(
this.$el.html(
converse.templates.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')
})
).hide());
this.$tabs = this.$parent.parent().find('#controlbox-tabs');
this.$tabs.append(converse.templates.chatrooms_tab({label_rooms: __('Rooms')}));
return this;
},
onDomainChange: function (model) {
var $server = this.$el.find('input.new-chatroom-server');
$server.val(model.get('muc_domain'));
if (converse.auto_list_rooms) {
this.updateRoomsList();
}
},
onNickChange: function (model) {
var $nick = this.$el.find('input.new-chatroom-nick');
$nick.val(model.get('nick'));
},
informNoRoomsFound: function () {
var $available_chatrooms = this.$el.find('#available-chatrooms');
// For translators: %1$s is a variable and will be replaced with the XMPP server name
$available_chatrooms.html('
'+__('No rooms on %1$s',this.model.get('muc_domain'))+'
');
$('input#show-rooms').show().siblings('span.spinner').remove();
},
onRoomsFound: function (iq) {
/* Handle the IQ stanza returned from the server, containing
* all its public rooms.
*/
var name, jid, i, fragment,
$available_chatrooms = this.$el.find('#available-chatrooms');
this.rooms = $(iq).find('query').find('item');
if (this.rooms.length) {
// For translators: %1$s is a variable and will be
// replaced with the XMPP server name
$available_chatrooms.html('
'+__('Rooms on %1$s',this.model.get('muc_domain'))+'
');
fragment = document.createDocumentFragment();
for (i=0; i');
this.model.save({muc_domain: server});
this.updateRoomsList();
},
showRoomInfo: function (ev) {
var target = ev.target,
$dd = $(target).parent('dd'),
$div = $dd.find('div.room-info');
if ($div.length) {
$div.remove();
} else {
$dd.find('span.spinner').remove();
$dd.append('');
converse.connection.disco.info(
$(target).attr('data-room-jid'),
null,
function (stanza) {
var $stanza = $(stanza);
// All MUC features found here: http://xmpp.org/registrar/disco-features.html
$dd.find('span.spinner').replaceWith(
converse.templates.room_description({
'desc': $stanza.find('field[var="muc#roominfo_description"] value').text(),
'occ': $stanza.find('field[var="muc#roominfo_occupants"] value').text(),
'hidden': $stanza.find('feature[var="muc_hidden"]').length,
'membersonly': $stanza.find('feature[var="muc_membersonly"]').length,
'moderated': $stanza.find('feature[var="muc_moderated"]').length,
'nonanonymous': $stanza.find('feature[var="muc_nonanonymous"]').length,
'open': $stanza.find('feature[var="muc_open"]').length,
'passwordprotected': $stanza.find('feature[var="muc_passwordprotected"]').length,
'persistent': $stanza.find('feature[var="muc_persistent"]').length,
'publicroom': $stanza.find('feature[var="muc_public"]').length,
'semianonymous': $stanza.find('feature[var="muc_semianonymous"]').length,
'temporary': $stanza.find('feature[var="muc_temporary"]').length,
'unmoderated': $stanza.find('feature[var="muc_unmoderated"]').length,
'label_desc': __('Description:'),
'label_occ': __('Occupants:'),
'label_features': __('Features:'),
'label_requires_auth': __('Requires authentication'),
'label_hidden': __('Hidden'),
'label_requires_invite': __('Requires an invitation'),
'label_moderated': __('Moderated'),
'label_non_anon': __('Non-anonymous'),
'label_open_room': __('Open room'),
'label_permanent_room': __('Permanent room'),
'label_public': __('Public'),
'label_semi_anon': __('Semi-anonymous'),
'label_temp_room': __('Temporary room'),
'label_unmoderated': __('Unmoderated')
}));
}.bind(this));
}
},
createChatRoom: function (ev) {
ev.preventDefault();
var name, $name,
server, $server,
jid,
$nick = this.$el.find('input.new-chatroom-nick'),
nick = $nick.val(),
chatroom;
if (!nick) { $nick.addClass('error'); }
else { $nick.removeClass('error'); }
if (ev.type === 'click') {
name = $(ev.target).text();
jid = $(ev.target).attr('data-room-jid');
} else {
$name = this.$el.find('input.new-chatroom-name');
$server= this.$el.find('input.new-chatroom-server');
server = $server.val();
name = $name.val().trim();
$name.val(''); // Clear the input
if (name && server) {
jid = Strophe.escapeNode(name.toLowerCase()) + '@' + server.toLowerCase();
$name.removeClass('error');
$server.removeClass('error');
this.model.save({muc_domain: server});
} else {
if (!name) { $name.addClass('error'); }
if (!server) { $server.addClass('error'); }
return;
}
}
if (!nick) { return; }
chatroom = converse.chatboxviews.showChat({
'id': jid,
'jid': jid,
'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
'nick': nick,
'type': 'chatroom',
'box_id': b64_sha1(jid)
});
},
setDomain: function (ev) {
this.model.save({muc_domain: ev.target.value});
},
setNick: function (ev) {
this.model.save({nick: ev.target.value});
}
});
/* Support for XEP-0249: Direct MUC invitations */
/* ------------------------------------------------------------ */
converse.onDirectMUCInvitation = function (message) {
/* A direct MUC invitation to join a room has been received */
var $message = $(message),
$x = $message.children('x[xmlns="jabber:x:conference"]'),
from = Strophe.getBareJidFromJid($message.attr('from')),
room_jid = $x.attr('jid'),
reason = $x.attr('reason'),
contact = converse.roster.get(from),
result;
if (converse.auto_join_on_invite) {
result = true;
} else {
// Invite request might come from someone not your roster list
contact = contact? contact.get('fullname'): Strophe.getNodeFromJid(from);
if (!reason) {
result = confirm(
__(___("%1$s has invited you to join a chat room: %2$s"),
contact, room_jid)
);
} else {
result = confirm(
__(___('%1$s has invited you to join a chat room: %2$s, and left the following reason: "%3$s"'),
contact, room_jid, reason)
);
}
}
if (result === true) {
var chatroom = converse.chatboxviews.showChat({
'id': room_jid,
'jid': room_jid,
'name': Strophe.unescapeNode(Strophe.getNodeFromJid(room_jid)),
'nick': Strophe.unescapeNode(Strophe.getNodeFromJid(converse.connection.jid)),
'type': 'chatroom',
'box_id': b64_sha1(room_jid),
'password': $x.attr('password')
});
if (!_.contains(
[Strophe.Status.CONNECTING, Strophe.Status.CONNECTED],
chatroom.get('connection_status'))
) {
converse.chatboxviews.get(room_jid).join(null);
}
}
};
var autoJoinRooms = function () {
_.each(converse.auto_join_rooms, function (room) {
if (typeof room === 'string') {
converse_api.rooms.open(room);
} else if (typeof room === 'object') {
converse_api.rooms.open(room.jid, room.nick);
} else {
converse.log('Invalid room criteria specified for "auto_join_rooms"', 'error');
}
});
};
converse.on('chatBoxesFetched', autoJoinRooms);
var onConnected = function () {
converse.connection.addHandler(
function (message) {
converse.onDirectMUCInvitation(message);
return true;
}, 'jabber:x:conference', 'message');
};
converse.on('connected', onConnected);
converse.on('reconnected', onConnected);
/* ------------------------------------------------------------ */
/* We extend the default converse.js API to add methods specific to MUC
* chat rooms.
*/
_.extend(converse_api, {
'rooms': {
'open': function (jids, nick) {
if (!nick) {
nick = Strophe.getNodeFromJid(converse.bare_jid);
}
if (typeof nick !== "string") {
throw new TypeError('rooms.open: invalid nick, must be string');
}
var _transform = function (jid) {
jid = jid.toLowerCase();
var chatroom = converse.chatboxes.get(jid);
converse.log('jid');
if (!chatroom) {
chatroom = converse.chatboxviews.showChat({
'id': jid,
'jid': jid,
'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
'nick': nick,
'type': 'chatroom',
'box_id': b64_sha1(jid)
});
}
return converse.wrappedChatBox(converse.chatboxes.getChatBox(jid, true));
};
if (typeof jids === "undefined") {
throw new TypeError('rooms.open: You need to provide at least one JID');
} else if (typeof jids === "string") {
return _transform(jids);
}
return _.map(jids, _transform);
},
'get': function (jids) {
if (typeof jids === "undefined") {
throw new TypeError("rooms.get: You need to provide at least one JID");
} else if (typeof jids === "string") {
return converse.wrappedChatBox(converse.chatboxes.getChatBox(jids, true));
}
return _.map(jids, _.partial(converse.wrappedChatBox, _.bind(converse.chatboxes.getChatBox, converse.chatboxes, _, true)));
}
}
});
}
});
}));
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
// XEP-0059 Result Set Management
(function (root, factory) {
define("converse-mam", [
"converse-core",
"converse-api",
"converse-chatview", // Could be made a soft dependency
"converse-muc", // Could be made a soft dependency
"strophe.rsm"
], factory);
}(this, function (converse, converse_api) {
"use strict";
var $ = converse_api.env.jQuery,
Strophe = converse_api.env.Strophe,
$iq = converse_api.env.$iq,
_ = converse_api.env._,
moment = converse_api.env.moment;
var RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'count'];
// 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_api.plugins.add('mam', {
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
Features: {
addClientFeatures: function () {
converse.connection.disco.addFeature(Strophe.NS.MAM);
return this._super.addClientFeatures.apply(this, arguments);
}
},
ChatBoxes: {
createMessage: function ($message, $delay) {
var message = this._super.createMessage.apply(this, arguments);
message.save({
archive_id: $message.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('id')
});
}
},
ChatBoxView: {
render: function () {
var result = this._super.render.apply(this, arguments);
if (!this.disable_mam) {
this.$content.on('scroll', _.debounce(this.onScroll.bind(this), 100));
}
return result;
},
afterMessagesFetched: function () {
if (this.disable_mam || !converse.features.findWhere({'var': Strophe.NS.MAM})) {
return this._super.afterMessagesFetched.apply(this, arguments);
}
if (this.model.messages.length < converse.archived_messages_page_size) {
this.fetchArchivedMessages({
'before': '', // Page backwards from the most recent message
'with': this.model.get('jid'),
'max': converse.archived_messages_page_size
});
}
return this._super.afterMessagesFetched.apply(this, arguments);
},
fetchArchivedMessages: function (options) {
/* Fetch archived chat messages from the XMPP server.
*
* Then, upon receiving them, call onMessage on the chat box,
* so that they are displayed inside it.
*/
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.log("Attempted to fetch archived messages but this user's server doesn't support XEP-0313");
return;
}
if (this.disable_mam) {
return;
}
this.addSpinner();
converse.queryForArchivedMessages(options, function (messages) {
this.clearSpinner();
if (messages.length) {
_.map(messages, converse.chatboxes.onMessage.bind(converse.chatboxes));
}
}.bind(this),
function () {
this.clearSpinner();
converse.log("Error or timeout while trying to fetch archived messages", "error");
}.bind(this)
);
},
onScroll: function (ev) {
if ($(ev.target).scrollTop() === 0 && this.model.messages.length) {
this.fetchArchivedMessages({
'before': this.model.messages.at(0).get('archive_id'),
'with': this.model.get('jid'),
'max': converse.archived_messages_page_size
});
}
},
},
ChatRoomView: {
render: function () {
var result = this._super.render.apply(this, arguments);
if (!this.disable_mam) {
this.$content.on('scroll', _.debounce(this.onScroll.bind(this), 100));
}
return result;
},
}
},
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
this.updateSettings({
archived_messages_page_size: '20',
message_archiving: 'never', // Supported values are 'always', 'never', 'roster' (https://xmpp.org/extensions/xep-0313.html#prefs)
message_archiving_timeout: 8000, // Time (in milliseconds) to wait before aborting MAM request
});
converse.queryForArchivedMessages = function (options, callback, errback) {
/* Do a MAM (XEP-0313) query for archived messages.
*
* Parameters:
* (Object) options - Query parameters, either MAM-specific or also for Result Set Management.
* (Function) callback - A function to call whenever we receive query-relevant stanza.
* (Function) errback - A function to call when an error stanza is received.
*
* The options parameter can also be an instance of
* Strophe.RSM to enable easy querying between results pages.
*
* The callback function may be called multiple times, first
* for the initial IQ result and then for each message
* returned. The last time the callback is called, a
* Strophe.RSM object is returned on which "next" or "previous"
* can be called before passing it in again to this method, to
* get the next or previous page in the result set.
*/
var date, messages = [];
if (typeof options === "function") {
callback = options;
errback = callback;
}
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.log('This server does not support XEP-0313, Message Archive Management');
errback(null);
return;
}
var queryid = converse.connection.getUniqueId();
var attrs = {'type':'set'};
if (typeof options !== "undefined" && options.groupchat) {
if (!options['with']) {
throw new Error('You need to specify a "with" value containing the chat room JID, when querying groupchat messages.');
}
attrs.to = options['with'];
}
var stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid});
if (typeof options !== "undefined") {
stanza.c('x', {'xmlns':Strophe.NS.XFORM, 'type': 'submit'})
.c('field', {'var':'FORM_TYPE', 'type': 'hidden'})
.c('value').t(Strophe.NS.MAM).up().up();
if (options['with'] && !options.groupchat) {
stanza.c('field', {'var':'with'}).c('value').t(options['with']).up().up();
}
_.each(['start', 'end'], function (t) {
if (options[t]) {
date = moment(options[t]);
if (date.isValid()) {
stanza.c('field', {'var':t}).c('value').t(date.format()).up().up();
} else {
throw new TypeError('archive.query: invalid date provided for: '+t);
}
}
});
stanza.up();
if (options instanceof Strophe.RSM) {
stanza.cnode(options.toXML());
} else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) {
stanza.cnode(new Strophe.RSM(options).toXML());
}
}
converse.connection.addHandler(function (message) {
var $msg = $(message), $fin, rsm;
if (typeof callback === "function") {
$fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]');
if ($fin.length) {
rsm = new Strophe.RSM({xml: $fin.find('set')[0]});
_.extend(rsm, _.pick(options, ['max']));
_.extend(rsm, _.pick(options, MAM_ATTRIBUTES));
callback(messages, rsm);
return false; // We've received all messages, decommission this handler
} else if (queryid === $msg.find('result').attr('queryid')) {
messages.push(message);
}
return true;
} else {
return false; // There's no callback, so no use in continuing this handler.
}
}, Strophe.NS.MAM);
converse.connection.sendIQ(stanza, null, errback, converse.message_archiving_timeout);
};
_.extend(converse_api, {
/* Extend default converse.js API to add methods specific to MAM
*/
'archive': {
'query': converse.queryForArchivedMessages.bind(converse)
}
});
converse.onMAMError = function (iq) {
if ($(iq).find('feature-not-implemented').length) {
converse.log("Message Archive Management (XEP-0313) not supported by this browser");
} else {
converse.log("An error occured while trying to set archiving preferences.");
converse.log(iq);
}
};
converse.onMAMPreferences = function (feature, iq) {
/* Handle returned IQ stanza containing Message Archive
* Management (XEP-0313) preferences.
*
* XXX: For now we only handle the global default preference.
* The XEP also provides for per-JID preferences, which is
* currently not supported in converse.js.
*
* Per JID preferences will be set in chat boxes, so it'll
* probbaly be handled elsewhere in any case.
*/
var $prefs = $(iq).find('prefs[xmlns="'+Strophe.NS.MAM+'"]');
var default_pref = $prefs.attr('default');
var stanza;
if (default_pref !== converse.message_archiving) {
stanza = $iq({'type': 'set'}).c('prefs', {'xmlns':Strophe.NS.MAM, 'default':converse.message_archiving});
$prefs.children().each(function (idx, child) {
stanza.cnode(child).up();
});
converse.connection.sendIQ(stanza, _.partial(function (feature, iq) {
// XXX: Strictly speaking, the server should respond with the updated prefs
// (see example 18: https://xmpp.org/extensions/xep-0313.html#config)
// but Prosody doesn't do this, so we don't rely on it.
feature.save({'preferences': {'default':converse.message_archiving}});
}, feature),
converse.onMAMError
);
} else {
feature.save({'preferences': {'default':converse.message_archiving}});
}
};
var onFeatureAdded = function (evt, feature) {
var prefs = feature.get('preferences') || {};
if (feature.get('var') === Strophe.NS.MAM && prefs['default'] !== converse.message_archiving) {
// Ask the server for archiving preferences
converse.connection.sendIQ(
$iq({'type': 'get'}).c('prefs', {'xmlns': Strophe.NS.MAM}),
_.partial(converse.onMAMPreferences, feature),
_.partial(converse.onMAMError, feature)
);
}
};
converse.on('serviceDiscovered', onFeatureAdded.bind(converse.features));
}
});
}));
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
(function (root, factory) {
define("converse-vcard", [
"converse-core",
"converse-api",
"strophe.vcard",
], factory);
}(this, function (converse, converse_api) {
"use strict";
var Strophe = converse_api.env.Strophe,
$ = converse_api.env.jQuery,
_ = converse_api.env._,
moment = converse_api.env.moment;
converse_api.plugins.add('vcard', {
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
Features: {
addClientFeatures: function () {
this._super.addClientFeatures.apply(this, arguments);
if (converse.use_vcards) {
converse.connection.disco.addFeature(Strophe.NS.VCARD);
}
}
},
RosterContacts: {
createRequestingContact: function (presence) {
var bare_jid = Strophe.getBareJidFromJid(presence.getAttribute('from'));
converse.getVCard(
bare_jid,
_.partial(converse.createRequestingContactFromVCard, presence),
function (iq, jid) {
converse.log("Error while retrieving vcard for "+jid);
converse.createRequestingContactFromVCard(presence, iq, jid);
}
);
}
}
},
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
this.updateSettings({
use_vcards: true,
});
converse.createRequestingContactFromVCard = function (presence, iq, jid, fullname, img, img_type, url) {
var bare_jid = Strophe.getBareJidFromJid(jid);
var nick = $(presence).children('nick[xmlns="'+Strophe.NS.NICK+'"]').text();
var user_data = {
jid: bare_jid,
subscription: 'none',
ask: null,
requesting: true,
fullname: fullname || nick || bare_jid,
image: img,
image_type: img_type,
url: url,
vcard_updated: moment().format()
};
converse.roster.create(user_data);
converse.emit('contactRequest', user_data);
};
converse.onVCardError = function (jid, iq, errback) {
var contact = converse.roster.get(jid);
if (contact) {
contact.save({ 'vcard_updated': moment().format() });
}
if (errback) { errback(iq, jid); }
};
converse.onVCardData = function (jid, iq, callback) {
var $vcard = $(iq).find('vCard'),
fullname = $vcard.find('FN').text(),
img = $vcard.find('BINVAL').text(),
img_type = $vcard.find('TYPE').text(),
url = $vcard.find('URL').text();
if (jid) {
var contact = converse.roster.get(jid);
if (contact) {
fullname = _.isEmpty(fullname)? contact.get('fullname') || jid: fullname;
contact.save({
'fullname': fullname,
'image_type': img_type,
'image': img,
'url': url,
'vcard_updated': moment().format()
});
}
}
if (callback) {
callback(iq, jid, fullname, img, img_type, url);
}
};
converse.getVCard = function (jid, callback, errback) {
/* Request the VCard of another user.
*
* Parameters:
* (String) jid - The Jabber ID of the user whose VCard
* is being requested.
* (Function) callback - A function to call once the VCard is
* returned.
* (Function) errback - A function to call if an error occured
* while trying to fetch the VCard.
*/
if (!converse.use_vcards) {
if (callback) { callback(null, jid); }
} else {
converse.connection.vcard.get(
_.partial(converse.onVCardData, jid, _, callback),
jid,
_.partial(converse.onVCardError, jid, _, errback));
}
};
var updateVCardForChatBox = function (evt, chatbox) {
if (!converse.use_vcards) { return; }
var jid = chatbox.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) {
chatbox.model.save({
'fullname' : fullname || jid,
'url': url,
'image_type': image_type,
'image': image
});
},
function () {
converse.log(
"updateVCardForChatBox: Error occured while fetching vcard"
);
}
);
}
};
converse.on('chatBoxInitialized', updateVCardForChatBox);
var onContactAdd = function (contact) {
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'));
}
};
converse.on('initialized', function () {
converse.roster.on("add", onContactAdd);
});
var fetchOwnVCard = function () {
if (converse.xmppstatus.get('fullname') === undefined) {
converse.getVCard(
null, // No 'to' attr when getting one's own vCard
function (iq, jid, fullname) {
converse.xmppstatus.save({'fullname': fullname});
}
);
}
};
converse.on('statusInitialized', fetchOwnVCard);
}
});
}));
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global Backbone, define, window, crypto, CryptoJS */
/* This is a Converse.js plugin which add support Off-the-record (OTR)
* encryption of one-on-one chat messages.
*/
(function (root, factory) {
define("converse-otr", [
"otr",
"converse-core",
"converse-api"
], factory);
}(this, function (otr, converse, converse_api) {
"use strict";
// Strophe methods for building stanzas
var Strophe = converse_api.env.Strophe,
utils = converse_api.env.utils,
b64_sha1 = converse_api.env.b64_sha1;
// Other necessary globals
var $ = converse_api.env.jQuery,
_ = converse_api.env._;
// For translations
var __ = utils.__.bind(converse);
var HAS_CSPRNG = ((typeof crypto !== 'undefined') &&
((typeof crypto.randomBytes === 'function') ||
(typeof crypto.getRandomValues === 'function')
));
var HAS_CRYPTO = HAS_CSPRNG && (
(typeof CryptoJS !== "undefined") &&
(typeof otr.OTR !== "undefined") &&
(typeof otr.DSA !== "undefined")
);
var UNENCRYPTED = 0;
var UNVERIFIED= 1;
var VERIFIED= 2;
var FINISHED = 3;
var OTR_TRANSLATED_MAPPING = {}; // Populated in initialize
var OTR_CLASS_MAPPING = {};
OTR_CLASS_MAPPING[UNENCRYPTED] = 'unencrypted';
OTR_CLASS_MAPPING[UNVERIFIED] = 'unverified';
OTR_CLASS_MAPPING[VERIFIED] = 'verified';
OTR_CLASS_MAPPING[FINISHED] = 'finished';
converse_api.plugins.add('otr', {
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
_initialize: function () {
this._super._initialize.apply(this, arguments);
this.otr = new this.OTR();
},
registerGlobalEventHandlers: function () {
this._super.registerGlobalEventHandlers();
$(document).click(function () {
if ($('.toggle-otr ul').is(':visible')) {
$('.toggle-otr ul', this).slideUp();
}
if ($('.toggle-smiley ul').is(':visible')) {
$('.toggle-smiley ul', this).slideUp();
}
});
},
wrappedChatBox: function (chatbox) {
var wrapped_chatbox = this._super.wrappedChatBox.apply(this, arguments);
if (!chatbox) { return; }
return _.extend(wrapped_chatbox, {
'endOTR': chatbox.endOTR.bind(chatbox),
'initiateOTR': chatbox.initiateOTR.bind(chatbox),
});
},
ChatBox: {
initialize: function () {
this._super.initialize.apply(this, arguments);
if (this.get('box_id') !== 'controlbox') {
this.save({
'otr_status': this.get('otr_status') || UNENCRYPTED
});
}
},
isOTRMessage: function ($message) {
var $body = $message.children('body'),
text = ($body.length > 0 ? $body.text() : undefined);
return !!text.match(/^\?OTR/);
},
shouldPlayNotification: function ($message) {
/* Don't play a notification if this is an OTR message but
* encryption is not yet set up. That would mean that the
* OTR session is still being established, so there are no
* "visible" OTR messages being exchanged.
*/
return this._super.shouldPlayNotification.apply(this, arguments) &&
!(this.isOTRMessage($message) && !_.contains([UNVERIFIED, VERIFIED], this.get('otr_status')));
},
createMessage: function ($message, $delay, archive_id) {
var converse = this._super.converse,
$body = $message.children('body'),
text = ($body.length > 0 ? $body.text() : undefined);
if ((!text) || (!converse.allow_otr)) {
return this._super.createMessage.apply(this, arguments);
}
if (text.match(/^\?OTRv23?/)) {
this.initiateOTR(text);
} else {
if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) {
this.otr.receiveMsg(text);
} else {
if (text.match(/^\?OTR/)) {
if (!this.otr) {
this.initiateOTR(text);
} else {
this.otr.receiveMsg(text);
}
} else {
// Normal unencrypted message.
return this._super.createMessage.apply(this, arguments);
}
}
}
},
getSession: function (callback) {
var converse = this._super.converse;
var cipher = CryptoJS.lib.PasswordBasedCipher;
var pass, instance_tag, saved_key, pass_check;
if (converse.cache_otr_key) {
pass = converse.otr.getSessionPassphrase();
if (typeof pass !== "undefined") {
instance_tag = window.sessionStorage[b64_sha1(this.id+'instance_tag')];
saved_key = window.sessionStorage[b64_sha1(this.id+'priv_key')];
pass_check = window.sessionStorage[b64_sha1(this.connection.jid+'pass_check')];
if (saved_key && instance_tag && typeof pass_check !== 'undefined') {
var decrypted = cipher.decrypt(CryptoJS.algo.AES, saved_key, pass);
var key = otr.DSA.parsePrivate(decrypted.toString(CryptoJS.enc.Latin1));
if (cipher.decrypt(CryptoJS.algo.AES, pass_check, pass).toString(CryptoJS.enc.Latin1) === 'match') {
// Verified that the passphrase is still the same
this.trigger('showHelpMessages', [__('Re-establishing encrypted session')]);
callback({
'key': key,
'instance_tag': instance_tag
});
return; // Our work is done here
}
}
}
}
// We need to generate a new key and instance tag
this.trigger('showHelpMessages', [
__('Generating private key.'),
__('Your browser might become unresponsive.')],
null,
true // show spinner
);
window.setTimeout(function () {
var instance_tag = otr.OTR.makeInstanceTag();
callback({
'key': converse.otr.generatePrivateKey.call(this, instance_tag),
'instance_tag': instance_tag
});
}, 500);
},
updateOTRStatus: function (state) {
switch (state) {
case otr.OTR.CONST.STATUS_AKE_SUCCESS:
if (this.otr.msgstate === otr.OTR.CONST.MSGSTATE_ENCRYPTED) {
this.save({'otr_status': UNVERIFIED});
}
break;
case otr.OTR.CONST.STATUS_END_OTR:
if (this.otr.msgstate === otr.OTR.CONST.MSGSTATE_FINISHED) {
this.save({'otr_status': FINISHED});
} else if (this.otr.msgstate === otr.OTR.CONST.MSGSTATE_PLAINTEXT) {
this.save({'otr_status': UNENCRYPTED});
}
break;
}
},
onSMP: function (type, data) {
// Event handler for SMP (Socialist's Millionaire Protocol)
// used by OTR (off-the-record).
switch (type) {
case 'question':
this.otr.smpSecret(prompt(__(
'Authentication request from %1$s\n\nYour chat contact is attempting to verify your identity, by asking you the question below.\n\n%2$s',
[this.get('fullname'), data])));
break;
case 'trust':
if (data === true) {
this.save({'otr_status': VERIFIED});
} else {
this.trigger(
'showHelpMessages',
[__("Could not verify this user's identify.")],
'error');
this.save({'otr_status': UNVERIFIED});
}
break;
default:
throw new TypeError('ChatBox.onSMP: Unknown type for SMP');
}
},
initiateOTR: function (query_msg) {
// Sets up an OTR object through which we can send and receive
// encrypted messages.
//
// If 'query_msg' is passed in, it means there is an alread incoming
// query message from our contact. Otherwise, it is us who will
// send the query message to them.
this.save({'otr_status': UNENCRYPTED});
this.getSession(function (session) {
var converse = this._super.converse;
this.otr = new otr.OTR({
fragment_size: 140,
send_interval: 200,
priv: session.key,
instance_tag: session.instance_tag,
debug: this.debug
});
this.otr.on('status', this.updateOTRStatus.bind(this));
this.otr.on('smp', this.onSMP.bind(this));
this.otr.on('ui', function (msg) {
this.trigger('showReceivedOTRMessage', msg);
}.bind(this));
this.otr.on('io', function (msg) {
this.trigger('sendMessage', new converse.Message({ message: msg }));
}.bind(this));
this.otr.on('error', function (msg) {
this.trigger('showOTRError', msg);
}.bind(this));
this.trigger('showHelpMessages', [__('Exchanging private key with contact.')]);
if (query_msg) {
this.otr.receiveMsg(query_msg);
} else {
this.otr.sendQueryMsg();
}
}.bind(this));
},
endOTR: function () {
if (this.otr) {
this.otr.endOtr();
}
this.save({'otr_status': UNENCRYPTED});
}
},
ChatBoxView: {
events: {
'click .toggle-otr': 'toggleOTRMenu',
'click .start-otr': 'startOTRFromToolbar',
'click .end-otr': 'endOTR',
'click .auth-otr': 'authOTR'
},
initialize: function () {
var converse = this._super.converse;
this._super.initialize.apply(this, arguments);
this.model.on('change:otr_status', this.onOTRStatusChanged, this);
this.model.on('showOTRError', this.showOTRError, this);
this.model.on('showSentOTRMessage', function (text) {
this.showMessage({'message': text, 'sender': 'me'});
}, this);
this.model.on('showReceivedOTRMessage', function (text) {
this.showMessage({'message': text, 'sender': 'them'});
}, this);
if ((_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) || converse.use_otr_by_default) {
this.model.initiateOTR();
}
},
createMessageStanza: function () {
var stanza = this._super.createMessageStanza.apply(this, arguments);
if (this.model.get('otr_status') !== UNENCRYPTED) {
// OTR messages aren't carbon copied
stanza.c('private', {'xmlns': Strophe.NS.CARBONS});
}
return stanza;
},
onMessageSubmitted: function (text) {
var converse = this._super.converse;
if (!converse.connection.authenticated) {
return this.showHelpMessages(
['Sorry, the connection has been lost, '+
'and your message could not be sent'],
'error'
);
}
var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
if (match) {
if ((converse.allow_otr) && (match[1] === "endotr")) {
return this.endOTR();
} else if ((converse.allow_otr) && (match[1] === "otr")) {
return this.model.initiateOTR();
}
}
if (_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) {
// Off-the-record encryption is active
this.model.otr.sendMsg(text);
this.model.trigger('showSentOTRMessage', text);
} else {
this._super.onMessageSubmitted.apply(this, arguments);
}
},
onOTRStatusChanged: function () {
this.renderToolbar().informOTRChange();
},
informOTRChange: function () {
var data = this.model.toJSON();
var msgs = [];
if (data.otr_status === UNENCRYPTED) {
msgs.push(__("Your messages are not encrypted anymore"));
} else if (data.otr_status === UNVERIFIED) {
msgs.push(__("Your messages are now encrypted but your contact's identity has not been verified."));
} else if (data.otr_status === VERIFIED) {
msgs.push(__("Your contact's identify has been verified."));
} else if (data.otr_status === FINISHED) {
msgs.push(__("Your contact has ended encryption on their end, you should do the same."));
}
return this.showHelpMessages(msgs, 'info', false);
},
showOTRError: function (msg) {
var converse = this._super.converse;
if (msg === 'Message cannot be sent at this time.') {
this.showHelpMessages(
[__('Your message could not be sent')], 'error');
} else if (msg === 'Received an unencrypted message.') {
this.showHelpMessages(
[__('We received an unencrypted message')], 'error');
} else if (msg === 'Received an unreadable encrypted message.') {
this.showHelpMessages(
[__('We received an unreadable encrypted message')],
'error');
} else {
this.showHelpMessages(['Encryption error occured: '+msg], 'error');
}
converse.log("OTR ERROR:"+msg);
},
startOTRFromToolbar: function (ev) {
$(ev.target).parent().parent().slideUp();
ev.stopPropagation();
this.model.initiateOTR();
},
endOTR: function (ev) {
if (typeof ev !== "undefined") {
ev.preventDefault();
ev.stopPropagation();
}
this.model.endOTR();
},
authOTR: function (ev) {
var converse = this._super.converse;
var scheme = $(ev.target).data().scheme;
var result, question, answer;
if (scheme === 'fingerprint') {
result = confirm(__('Here are the fingerprints, please confirm them with %1$s, outside of this chat.\n\nFingerprint for you, %2$s: %3$s\n\nFingerprint for %1$s: %4$s\n\nIf you have confirmed that the fingerprints match, click OK, otherwise click Cancel.', [
this.model.get('fullname'),
converse.xmppstatus.get('fullname')||converse.bare_jid,
this.model.otr.priv.fingerprint(),
this.model.otr.their_priv_pk.fingerprint()
]
));
if (result === true) {
this.model.save({'otr_status': VERIFIED});
} else {
this.model.save({'otr_status': UNVERIFIED});
}
} else if (scheme === 'smp') {
alert(__('You will be prompted to provide a security question and then an answer to that question.\n\nYour contact will then be prompted the same question and if they type the exact same answer (case sensitive), their identity will be verified.'));
question = prompt(__('What is your security question?'));
if (question) {
answer = prompt(__('What is the answer to the security question?'));
this.model.otr.smpSecret(answer, question);
}
} else {
this.showHelpMessages([__('Invalid authentication scheme provided')], 'error');
}
},
toggleOTRMenu: function (ev) {
ev.stopPropagation();
this.$el.find('.toggle-otr ul').slideToggle(200);
},
getOTRTooltip: function () {
var data = this.model.toJSON();
if (data.otr_status === UNENCRYPTED) {
return __('Your messages are not encrypted. Click here to enable OTR encryption.');
} else if (data.otr_status === UNVERIFIED) {
return __('Your messages are encrypted, but your contact has not been verified.');
} else if (data.otr_status === VERIFIED) {
return __('Your messages are encrypted and your contact verified.');
} else if (data.otr_status === FINISHED) {
return __('Your contact has closed their end of the private session, you should do the same');
}
},
renderToolbar: function (options) {
var converse = this._super.converse;
if (!converse.show_toolbar) {
return;
}
var data = this.model.toJSON();
options = _.extend(options || {}, {
FINISHED: FINISHED,
UNENCRYPTED: UNENCRYPTED,
UNVERIFIED: UNVERIFIED,
VERIFIED: VERIFIED,
// FIXME: Leaky abstraction MUC
allow_otr: converse.allow_otr && !this.is_chatroom,
label_end_encrypted_conversation: __('End encrypted conversation'),
label_refresh_encrypted_conversation: __('Refresh encrypted conversation'),
label_start_encrypted_conversation: __('Start encrypted conversation'),
label_verify_with_fingerprints: __('Verify with fingerprints'),
label_verify_with_smp: __('Verify with SMP'),
label_whats_this: __("What\'s this?"),
otr_status_class: OTR_CLASS_MAPPING[data.otr_status],
otr_tooltip: this.getOTRTooltip(),
otr_translated_status: OTR_TRANSLATED_MAPPING[data.otr_status],
});
this._super.renderToolbar.call(this, options);
this.$el.find('.chat-toolbar').append(
converse.templates.toolbar_otr(
_.extend(this.model.toJSON(), options || {})
));
return this;
}
}
},
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
var converse = this.converse;
// Translation aware constants
// ---------------------------
// We can only call the __ translation method *after* converse.js
// has been initialized and with it the i18n machinery. That's why
// we do it here in the "initialize" method and not at the top of
// the module.
OTR_TRANSLATED_MAPPING[UNENCRYPTED] = __('unencrypted');
OTR_TRANSLATED_MAPPING[UNVERIFIED] = __('unverified');
OTR_TRANSLATED_MAPPING[VERIFIED] = __('verified');
OTR_TRANSLATED_MAPPING[FINISHED] = __('finished');
// For translations
__ = utils.__.bind(converse);
// Configuration values for this plugin
var settings = {
allow_otr: true,
cache_otr_key: false,
use_otr_by_default: false
};
_.extend(converse.default_settings, settings);
_.extend(converse, settings);
_.extend(converse, _.pick(converse.user_settings, Object.keys(settings)));
// Only allow OTR if we have the capability
converse.allow_otr = converse.allow_otr && HAS_CRYPTO;
// Only use OTR by default if allow OTR is enabled to begin with
converse.use_otr_by_default = converse.use_otr_by_default && converse.allow_otr;
// Backbone Models and Views
// -------------------------
converse.OTR = Backbone.Model.extend({
// A model for managing OTR settings.
getSessionPassphrase: function () {
if (converse.authentication === 'prebind') {
var key = b64_sha1(converse.connection.jid),
pass = window.sessionStorage[key];
if (typeof pass === 'undefined') {
pass = Math.floor(Math.random()*4294967295).toString();
window.sessionStorage[key] = pass;
}
return pass;
} else {
return converse.connection.pass;
}
},
generatePrivateKey: function (instance_tag) {
var key = new otr.DSA();
var jid = converse.connection.jid;
if (converse.cache_otr_key) {
var cipher = CryptoJS.lib.PasswordBasedCipher;
var pass = this.getSessionPassphrase();
if (typeof pass !== "undefined") {
// Encrypt the key and set in sessionStorage. Also store instance tag.
window.sessionStorage[b64_sha1(jid+'priv_key')] =
cipher.encrypt(CryptoJS.algo.AES, key.packPrivate(), pass).toString();
window.sessionStorage[b64_sha1(jid+'instance_tag')] = instance_tag;
window.sessionStorage[b64_sha1(jid+'pass_check')] =
cipher.encrypt(CryptoJS.algo.AES, 'match', pass).toString();
}
}
return key;
}
});
}
});
}));
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global Backbone, define */
/* This is a Converse.js plugin which add support for in-band registration
* as specified in XEP-0077.
*/
(function (root, factory) {
define("converse-register", [
"converse-core",
"converse-api",
"converse-controlbox"
], factory);
}(this, function (converse, converse_api) {
"use strict";
// Strophe methods for building stanzas
var Strophe = converse_api.env.Strophe,
utils = converse_api.env.utils,
$iq = converse_api.env.$iq;
// Other necessary globals
var $ = converse_api.env.jQuery,
_ = converse_api.env._;
// For translations
var __ = utils.__.bind(converse);
// Add Strophe Namespaces
Strophe.addNamespace('REGISTER', 'jabber:iq:register');
// Add Strophe Statuses
var i = 0;
Object.keys(Strophe.Status).forEach(function (key) {
i = Math.max(i, Strophe.Status[key]);
});
Strophe.Status.REGIFAIL = i + 1;
Strophe.Status.REGISTERED = i + 2;
Strophe.Status.CONFLICT = i + 3;
Strophe.Status.NOTACCEPTABLE = i + 5;
converse_api.plugins.add('register', {
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
ControlBoxView: {
renderLoginPanel: function () {
/* Also render a registration panel, when rendering the
* login panel.
*/
this._super.renderLoginPanel.apply(this, arguments);
var converse = this._super.converse;
if (converse.allow_registration) {
this.registerpanel = new converse.RegisterPanel({
'$parent': this.$el.find('.controlbox-panes'),
'model': this
});
this.registerpanel.render().$el.hide();
}
return this;
}
}
},
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
var converse = this.converse;
this.updateSettings({
allow_registration: true,
domain_placeholder: __(" e.g. conversejs.org"), // Placeholder text shown in the domain input on the registration form
providers_link: 'https://xmpp.net/directory.php', // Link to XMPP providers shown on registration page
});
converse.RegisterPanel = Backbone.View.extend({
tagName: 'div',
id: "register",
className: 'controlbox-pane',
events: {
'submit form#converse-register': 'onProviderChosen'
},
initialize: function (cfg) {
this.reset();
this.$parent = cfg.$parent;
this.$tabs = cfg.$parent.parent().find('#controlbox-tabs');
this.registerHooks();
},
render: function () {
this.$parent.append(this.$el.html(
converse.templates.register_panel({
'label_domain': __("Your XMPP provider's domain name:"),
'label_register': __('Fetch registration form'),
'help_providers': __('Tip: A list of public XMPP providers is available'),
'help_providers_link': __('here'),
'href_providers': converse.providers_link,
'domain_placeholder': converse.domain_placeholder
})
));
this.$tabs.append(converse.templates.register_tab({label_register: __('Register')}));
return this;
},
registerHooks: function () {
/* Hook into Strophe's _connect_cb, so that we can send an IQ
* requesting the registration fields.
*/
var conn = converse.connection;
var connect_cb = conn._connect_cb.bind(conn);
conn._connect_cb = function (req, callback, raw) {
if (!this._registering) {
connect_cb(req, callback, raw);
} else {
if (this.getRegistrationFields(req, callback, raw)) {
this._registering = false;
}
}
}.bind(this);
},
getRegistrationFields: function (req, _callback, raw) {
/* Send an IQ stanza to the XMPP server asking for the
* registration fields.
* Parameters:
* (Strophe.Request) req - The current request
* (Function) callback
*/
converse.log("sendQueryStanza was called");
var conn = converse.connection;
conn.connected = true;
var body = conn._proto._reqToData(req);
if (!body) { return; }
if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) {
return false;
}
var register = body.getElementsByTagName("register");
var mechanisms = body.getElementsByTagName("mechanism");
if (register.length === 0 && mechanisms.length === 0) {
conn._proto._no_auth_received(_callback);
return false;
}
if (register.length === 0) {
conn._changeConnectStatus(
Strophe.Status.REGIFAIL,
__('Sorry, the given provider does not support in band account registration. Please try with a different provider.')
);
return true;
}
// Send an IQ stanza to get all required data fields
conn._addSysHandler(this.onRegistrationFields.bind(this), null, "iq", null, null);
conn.send($iq({type: "get"}).c("query", {xmlns: Strophe.NS.REGISTER}).tree());
return true;
},
onRegistrationFields: function (stanza) {
/* Handler for Registration Fields Request.
*
* Parameters:
* (XMLElement) elem - The query stanza.
*/
if (stanza.getElementsByTagName("query").length !== 1) {
converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
return false;
}
this.setFields(stanza);
this.renderRegistrationForm(stanza);
return false;
},
reset: function (settings) {
var defaults = {
fields: {},
urls: [],
title: "",
instructions: "",
registered: false,
_registering: false,
domain: null,
form_type: null
};
_.extend(this, defaults);
if (settings) {
_.extend(this, _.pick(settings, Object.keys(defaults)));
}
},
onProviderChosen: function (ev) {
/* Callback method that gets called when the user has chosen an
* XMPP provider.
*
* Parameters:
* (Submit Event) ev - Form submission event.
*/
if (ev && ev.preventDefault) { ev.preventDefault(); }
var $form = $(ev.target),
$domain_input = $form.find('input[name=domain]'),
domain = $domain_input.val();
if (!domain) {
$domain_input.addClass('error');
return;
}
$form.find('input[type=submit]').hide()
.after(converse.templates.registration_request({
cancel: __('Cancel'),
info_message: __('Requesting a registration form from the XMPP server')
}));
$form.find('button.cancel').on('click', this.cancelRegistration.bind(this));
this.reset({
domain: Strophe.getDomainFromJid(domain),
_registering: true
});
converse.connection.connect(this.domain, "", this.onRegistering.bind(this));
return false;
},
giveFeedback: function (message, klass) {
this.$('.reg-feedback').attr('class', 'reg-feedback').text(message);
if (klass) {
$('.reg-feedback').addClass(klass);
}
},
onRegistering: function (status, error) {
var that;
converse.log('onRegistering');
if (_.contains([
Strophe.Status.DISCONNECTED,
Strophe.Status.CONNFAIL,
Strophe.Status.REGIFAIL,
Strophe.Status.NOTACCEPTABLE,
Strophe.Status.CONFLICT
], status)) {
converse.log('Problem during registration: Strophe.Status is: '+status);
this.cancelRegistration();
if (error) {
this.giveFeedback(error, 'error');
} else {
this.giveFeedback(__(
'Something went wrong while establishing a connection with "%1$s". Are you sure it exists?',
this.domain
), 'error');
}
} else if (status === Strophe.Status.REGISTERED) {
converse.log("Registered successfully.");
converse.connection.reset();
that = this;
this.$('form').hide(function () {
$(this).replaceWith('');
if (that.fields.password && that.fields.username) {
// automatically log the user in
converse.connection.connect(
that.fields.username.toLowerCase()+'@'+that.domain.toLowerCase(),
that.fields.password,
converse.onConnectStatusChanged
);
converse.chatboxviews.get('controlbox')
.switchTab({target: that.$tabs.find('.current')})
.giveFeedback(__('Now logging you in'));
} else {
converse.chatboxviews.get('controlbox')
.renderLoginPanel()
.giveFeedback(__('Registered successfully'));
}
that.reset();
});
}
},
renderRegistrationForm: function (stanza) {
/* Renders the registration form based on the XForm fields
* received from the XMPP server.
*
* Parameters:
* (XMLElement) stanza - The IQ stanza received from the XMPP server.
*/
var $form= this.$('form'),
$stanza = $(stanza),
$fields, $input;
$form.empty().append(converse.templates.registration_form({
'domain': this.domain,
'title': this.title,
'instructions': this.instructions
}));
if (this.form_type === 'xform') {
$fields = $stanza.find('field');
_.each($fields, function (field) {
$form.append(utils.xForm2webForm.bind(this, $(field), $stanza));
}.bind(this));
} else {
// Show fields
_.each(Object.keys(this.fields), function (key) {
if (key === "username") {
$input = converse.templates.form_username({
domain: ' @'+this.domain,
name: key,
type: "text",
label: key,
value: '',
required: 1
});
} else {
$form.append('');
$input = $('');
if (key === 'password' || key === 'email') {
$input.attr('type', key);
}
}
$form.append($input);
}.bind(this));
// Show urls
_.each(this.urls, function (url) {
$form.append($('').attr('href', url).text(url));
}.bind(this));
}
if (this.fields) {
$form.append('');
$form.on('submit', this.submitRegistrationForm.bind(this));
$form.append('');
$form.find('input[type=button]').on('click', this.cancelRegistration.bind(this));
} else {
$form.append('');
$form.find('input[type=button]').on('click', this.cancelRegistration.bind(this));
}
},
reportErrors: function (stanza) {
/* Report back to the user any error messages received from the
* XMPP server after attempted registration.
*
* Parameters:
* (XMLElement) stanza - The IQ stanza received from the
* XMPP server.
*/
var $form= this.$('form'), flash;
var $errmsgs = $(stanza).find('error text');
var $flash = $form.find('.form-errors');
if (!$flash.length) {
flash = '';
if ($form.find('p.instructions').length) {
$form.find('p.instructions').append(flash);
} else {
$form.prepend(flash);
}
$flash = $form.find('.form-errors');
} else {
$flash.empty();
}
$errmsgs.each(function (idx, txt) {
$flash.append($('
').text($(txt).text()));
});
if (!$errmsgs.length) {
$flash.append($('
').text(
__('The provider rejected your registration attempt. '+
'Please check the values you entered for correctness.')));
}
$flash.show();
},
cancelRegistration: function (ev) {
/* Handler, when the user cancels the registration form.
*/
if (ev && ev.preventDefault) { ev.preventDefault(); }
converse.connection.reset();
this.render();
},
submitRegistrationForm : function (ev) {
/* Handler, when the user submits the registration form.
* Provides form error feedback or starts the registration
* process.
*
* Parameters:
* (Event) ev - the submit event.
*/
if (ev && ev.preventDefault) { ev.preventDefault(); }
var $empty_inputs = this.$('input.required:emptyVal');
if ($empty_inputs.length) {
$empty_inputs.addClass('error');
return;
}
var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
iq = $iq({type: "set"}).c("query", {xmlns:Strophe.NS.REGISTER});
if (this.form_type === 'xform') {
iq.c("x", {xmlns: Strophe.NS.XFORM, type: 'submit'});
$inputs.each(function () {
iq.cnode(utils.webForm2xForm(this)).up();
});
} else {
$inputs.each(function () {
var $input = $(this);
iq.c($input.attr('name'), {}, $input.val());
});
}
converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, "iq", null, null);
converse.connection.send(iq);
this.setFields(iq.tree());
},
setFields: function (stanza) {
/* Stores the values that will be sent to the XMPP server
* during attempted registration.
*
* Parameters:
* (XMLElement) stanza - the IQ stanza that will be sent to the XMPP server.
*/
var $query = $(stanza).find('query'), $xform;
if ($query.length > 0) {
$xform = $query.find('x[xmlns="'+Strophe.NS.XFORM+'"]');
if ($xform.length > 0) {
this._setFieldsFromXForm($xform);
} else {
this._setFieldsFromLegacy($query);
}
}
},
_setFieldsFromLegacy: function ($query) {
$query.children().each(function (idx, field) {
var $field = $(field);
if (field.tagName.toLowerCase() === 'instructions') {
this.instructions = Strophe.getText(field);
return;
} else if (field.tagName.toLowerCase() === 'x') {
if ($field.attr('xmlns') === 'jabber:x:oob') {
$field.find('url').each(function (idx, url) {
this.urls.push($(url).text());
}.bind(this));
}
return;
}
this.fields[field.tagName.toLowerCase()] = Strophe.getText(field);
}.bind(this));
this.form_type = 'legacy';
},
_setFieldsFromXForm: function ($xform) {
this.title = $xform.find('title').text();
this.instructions = $xform.find('instructions').text();
$xform.find('field').each(function (idx, field) {
var _var = field.getAttribute('var');
if (_var) {
this.fields[_var.toLowerCase()] = $(field).children('value').text();
} else {
// TODO: other option seems to be type="fixed"
converse.log("WARNING: Found field we couldn't parse");
}
}.bind(this));
this.form_type = 'xform';
},
_onRegisterIQ: function (stanza) {
/* Callback method that gets called when a return IQ stanza
* is received from the XMPP server, after attempting to
* register a new user.
*
* Parameters:
* (XMLElement) stanza - The IQ stanza.
*/
var error = null,
query = stanza.getElementsByTagName("query");
if (query.length > 0) {
query = query[0];
}
if (stanza.getAttribute("type") === "error") {
converse.log("Registration failed.");
error = stanza.getElementsByTagName("error");
if (error.length !== 1) {
converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown");
return false;
}
error = error[0].firstChild.tagName.toLowerCase();
if (error === 'conflict') {
converse.connection._changeConnectStatus(Strophe.Status.CONFLICT, error);
} else if (error === 'not-acceptable') {
converse.connection._changeConnectStatus(Strophe.Status.NOTACCEPTABLE, error);
} else {
converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, error);
}
this.reportErrors(stanza);
} else {
converse.connection._changeConnectStatus(Strophe.Status.REGISTERED, null);
}
return false;
},
remove: function () {
this.$tabs.empty();
this.$el.parent().empty();
}
});
}
});
}));
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
/* This is a Converse.js plugin which add support for application-level pings
* as specified in XEP-0199 XMPP Ping.
*/
(function (root, factory) {
define("converse-ping", [
"converse-core",
"converse-api",
"strophe.ping"
], factory);
}(this, function (converse, converse_api) {
"use strict";
// Strophe methods for building stanzas
var Strophe = converse_api.env.Strophe;
// Other necessary globals
var _ = converse_api.env._;
converse_api.plugins.add('ping', {
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
var converse = this.converse;
this.updateSettings({
ping_interval: 180 //in seconds
});
converse.ping = function (jid, success, error, timeout) {
// XXX: We could first check here if the server advertised that
// it supports PING.
// However, some servers don't advertise while still keeping the
// connection option due to pings.
//
// var feature = converse.features.findWhere({'var': Strophe.NS.PING});
converse.lastStanzaDate = new Date();
if (typeof jid === 'undefined' || jid === null) {
jid = Strophe.getDomainFromJid(converse.bare_jid);
}
if (typeof timeout === 'undefined' ) { timeout = null; }
if (typeof success === 'undefined' ) { success = null; }
if (typeof error === 'undefined' ) { error = null; }
if (converse.connection) {
converse.connection.ping.ping(jid, success, error, timeout);
return true;
}
return false;
};
converse.pong = function (ping) {
converse.lastStanzaDate = new Date();
converse.connection.ping.pong(ping);
return true;
};
converse.registerPongHandler = function () {
converse.connection.disco.addFeature(Strophe.NS.PING);
converse.connection.ping.addPingHandler(converse.pong);
};
converse.registerPingHandler = function () {
converse.registerPongHandler();
if (converse.ping_interval > 0) {
converse.connection.addHandler(function () {
/* Handler on each stanza, saves the received date
* in order to ping only when needed.
*/
converse.lastStanzaDate = new Date();
return true;
});
converse.connection.addTimedHandler(1000, function () {
var now = new Date();
if (!converse.lastStanzaDate) {
converse.lastStanzaDate = now;
}
if ((now - converse.lastStanzaDate)/1000 > converse.ping_interval) {
return converse.ping();
}
return true;
});
}
};
_.extend(converse_api, {
/* We extend the default converse.js API to add a method specific
* to this plugin.
*/
'ping': function (jid) {
converse.ping(jid);
}
});
var onConnected = function () {
// Wrapper so that we can spy on registerPingHandler in tests
converse.registerPingHandler();
};
converse.on('connected', onConnected);
converse.on('reconnected', onConnected);
}
});
}));
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
(function (root, factory) {
define("converse-notification", ["converse-core", "converse-api"], factory);
}(this, function (converse, converse_api) {
"use strict";
var $ = converse_api.env.jQuery,
utils = converse_api.env.utils,
Strophe = converse_api.env.Strophe,
_ = converse_api.env._;
// For translations
var __ = utils.__.bind(converse);
var ___ = utils.___;
var supports_html5_notification = "Notification" in window;
converse_api.plugins.add('notification', {
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
var converse = this.converse;
this.updateSettings({
show_desktop_notifications: true,
chatstate_notification_blacklist: [],
// ^ a list of JIDs to ignore concerning chat state notifications
play_sounds: false,
sounds_path: '/sounds/',
notification_icon: '/logo/conversejs.png'
});
converse.isOnlyChatStateNotification = function ($msg) {
// See XEP-0085 Chat State Notification
return (
$msg.find('body').length === 0 && (
$msg.find(converse.ACTIVE).length !== 0 ||
$msg.find(converse.COMPOSING).length !== 0 ||
$msg.find(converse.INACTIVE).length !== 0 ||
$msg.find(converse.PAUSED).length !== 0 ||
$msg.find(converse.GONE).length !== 0
)
);
};
converse.shouldNotifyOfGroupMessage = function ($message) {
/* Is this a group message worthy of notification?
*/
var jid = $message.attr('from'),
resource = Strophe.getResourceFromJid(jid),
sender = resource && Strophe.unescapeNode(resource) || '';
if (sender === '' || $message.find('delay').length > 0) {
return false;
}
var room = converse.chatboxes.get(Strophe.getBareJidFromJid(jid));
var body = $message.children('body').text();
if (sender === room.get('nick') || !(new RegExp("\\b"+room.get('nick')+"\\b")).test(body)) {
return false;
}
return true;
};
converse.shouldNotifyOfMessage = function (message) {
/* Is this a message worthy of notification?
*/
var $message = $(message),
$forwarded = $message.find('forwarded');
if ($forwarded.length) {
return false;
} else if ($message.attr('type') === 'groupchat') {
return converse.shouldNotifyOfGroupMessage($message);
} else if (utils.isHeadlineMessage(message)) {
// We want to show notifications for headline messages.
return true;
}
var is_me = Strophe.getBareJidFromJid($message.attr('from')) === converse.bare_jid;
return !converse.isOnlyChatStateNotification($message) && !is_me;
};
converse.playSoundNotification = function ($message) {
/* Plays a sound to notify that a new message was recieved.
*/
// XXX Eventually this can be refactored to use Notification's sound
// feature, but no browser currently supports it.
// https://developer.mozilla.org/en-US/docs/Web/API/notification/sound
var audio;
if (converse.play_sounds && typeof Audio !== "undefined") {
audio = new Audio(converse.sounds_path+"msg_received.ogg");
if (audio.canPlayType('/audio/ogg')) {
audio.play();
} else {
audio = new Audio(converse.sounds_path+"msg_received.mp3");
audio.play();
}
}
};
converse.areDesktopNotificationsEnabled = function (ignore_blur) {
var enabled = supports_html5_notification &&
converse.show_desktop_notifications &&
Notification.permission === "granted";
if (ignore_blur) {
return enabled;
} else {
return enabled && converse.windowState === 'blur';
}
};
converse.showMessageNotification = function ($message) {
/* Shows an HTML5 Notification to indicate that a new chat
* message was received.
*/
var n, title, contact_jid, roster_item,
from_jid = $message.attr('from');
if ($message.attr('type') === 'headline' || from_jid.indexOf('@') === -1) {
// XXX: 2nd check is workaround for Prosody which doesn't
// give type "headline"
title = __(___("Notification from %1$s"), from_jid);
} else {
if ($message.attr('type') === 'groupchat') {
title = __(___("%1$s says"), Strophe.getResourceFromJid(from_jid));
} else {
if (typeof converse.roster === 'undefined') {
converse.log("Could not send notification, because roster is undefined", "error");
return;
}
contact_jid = Strophe.getBareJidFromJid($message.attr('from'));
roster_item = converse.roster.get(contact_jid);
title = __(___("%1$s says"), roster_item.get('fullname'));
}
}
n = new Notification(title, {
body: $message.children('body').text(),
lang: converse.i18n.locale_data.converse[""].lang,
icon: converse.notification_icon
});
setTimeout(n.close.bind(n), 5000);
};
converse.showChatStateNotification = function (contact) {
/* Creates an HTML5 Notification to inform of a change in a
* contact's chat state.
*/
if (_.contains(converse.chatstate_notification_blacklist, contact.jid)) {
// Don't notify if the user is being ignored.
return;
}
var chat_state = contact.chat_status,
message = null;
if (chat_state === 'offline') {
message = __('has gone offline');
} else if (chat_state === 'away') {
message = __('has gone away');
} else if ((chat_state === 'dnd')) {
message = __('is busy');
} else if (chat_state === 'online') {
message = __('has come online');
}
if (message === null) {
return;
}
var n = new Notification(contact.fullname, {
body: message,
lang: converse.i18n.locale_data.converse[""].lang,
icon: 'logo/conversejs.png'
});
setTimeout(n.close.bind(n), 5000);
};
converse.showContactRequestNotification = function (contact) {
var n = new Notification(contact.fullname, {
body: __('wants to be your contact'),
lang: converse.i18n.locale_data.converse[""].lang,
icon: 'logo/conversejs.png'
});
setTimeout(n.close.bind(n), 5000);
};
converse.showErrorNotification = function (data) {
var n = new Notification(__('An error has occured'), {
body: data.message,
lang: converse.i18n.locale_data.converse[""].lang,
icon: 'logo/conversejs.png'
});
setTimeout(n.close.bind(n), 5000);
};
converse.handleChatStateNotification = function (evt, contact) {
/* Event handler for on('contactStatusChanged').
* Will show an HTML5 notification to indicate that the chat
* status has changed.
*/
if (converse.areDesktopNotificationsEnabled()) {
converse.showChatStateNotification(contact);
}
};
converse.handleMessageNotification = function (evt, message) {
/* Event handler for the on('message') event. Will call methods
* to play sounds and show HTML5 notifications.
*/
var $message = $(message);
if (!converse.shouldNotifyOfMessage(message)) {
return false;
}
converse.playSoundNotification($message);
if (converse.areDesktopNotificationsEnabled()) {
converse.showMessageNotification($message);
}
};
converse.handleContactRequestNotification = function (evt, contact) {
if (converse.areDesktopNotificationsEnabled(true)) {
converse.showContactRequestNotification(contact);
}
};
converse.handleFeedback = function (evt, data) {
if (data.klass === 'error' && converse.areDesktopNotificationsEnabled(true)) {
converse.showErrorNotification(data);
}
};
converse.requestPermission = function (evt) {
if (supports_html5_notification &&
! _.contains(['denied', 'granted'], Notification.permission)) {
// Ask user to enable HTML5 notifications
Notification.requestPermission();
}
};
converse.on('contactRequest', converse.handleContactRequestNotification);
converse.on('contactStatusChanged', converse.handleChatStateNotification);
converse.on('message', converse.handleMessageNotification);
converse.on('feedback', converse.handleFeedback);
converse.on('connected', converse.requestPermission);
}
});
}));
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global Backbone, define, window */
(function (root, factory) {
define("converse-minimize", [
"converse-core",
"converse-api",
"converse-controlbox",
"converse-chatview",
"converse-muc"
], factory);
}(this, function (converse, converse_api) {
"use strict";
var $ = converse_api.env.jQuery,
_ = converse_api.env._,
b64_sha1 = converse_api.env.b64_sha1,
moment = converse_api.env.moment,
utils = converse_api.env.utils,
__ = utils.__.bind(converse);
converse_api.plugins.add('minimize', {
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
_tearDown: function () {
this._super._tearDown.apply(this, arguments);
if (this.minimized_chats) {
this.minimized_chats.undelegateEvents().model.reset();
this.minimized_chats.removeAll(); // Remove sub-views
this.minimized_chats.tearDown().remove(); // Remove overview
delete this.minimized_chats;
}
return this;
},
onConnected: function () {
converse.minimized_chats = new converse.MinimizedChats({
model: converse.chatboxes
});
return this._super.onConnected.apply(this, arguments);
},
registerGlobalEventHandlers: function () {
$(window).on("resize", _.debounce(function (ev) {
if (converse.connection.connected) {
converse.chatboxviews.trimChats();
}
}, 200));
return this._super.registerGlobalEventHandlers.apply(this, arguments);
},
wrappedChatBox: function (chatbox) {
/* Wrap a chatbox for outside consumption (i.e. so that it can be
* returned via the API.
*/
if (!chatbox) { return; }
var box = this._super.wrappedChatBox.apply(this, arguments);
box.maximize = chatbox.maximize.bind(chatbox);
box.minimize = chatbox.minimize.bind(chatbox);
return box;
},
ChatBox: {
initialize: function () {
this._super.initialize.apply(this, arguments);
if (this.get('id') === 'controlbox') {
return;
}
this.save({
'minimized': this.get('minimized') || false,
'time_minimized': this.get('time_minimized') || moment(),
});
},
maximize: function () {
this.save({
'minimized': false,
'time_opened': moment().valueOf()
});
},
minimize: function () {
this.save({
'minimized': true,
'time_minimized': moment().format()
});
},
},
ChatBoxView: {
events: {
'click .toggle-chatbox-button': 'minimize',
},
initialize: function () {
this.model.on('change:minimized', this.onMinimizedChanged, this);
return this._super.initialize.apply(this, arguments);
},
afterShown: function () {
this._super.afterShown.apply(this, arguments);
if (!this.model.get('minimized')) {
converse.chatboxviews.trimChats(this);
}
},
shouldShowOnTextMessage: function () {
return !this.model.get('minimized') &&
this._super.shouldShowOnTextMessage.apply(this, arguments);
},
setChatBoxHeight: function (height) {
if (!this.model.get('minimized')) {
return this._super.setChatBoxHeight.apply(this, arguments);
}
},
setChatBoxWidth: function (width) {
if (!this.model.get('minimized')) {
return this._super.setChatBoxWidth.apply(this, arguments);
}
},
onMinimizedChanged: function (item) {
if (item.get('minimized')) {
this.minimize();
} else {
this.maximize();
}
},
onMaximized: function () {
converse.chatboxviews.trimChats(this);
utils.refreshWebkit();
this.$content.scrollTop(this.model.get('scroll'));
this.setChatState(converse.ACTIVE).focus();
this.scrollDown();
converse.emit('chatBoxMaximized', this);
},
onMinimized: function () {
utils.refreshWebkit();
converse.emit('chatBoxMinimized', this);
},
maximize: function () {
// Restores a minimized chat box
var chatboxviews = converse.chatboxviews;
this.$el.insertAfter(chatboxviews.get("controlbox").$el)
.show('fast', this.onMaximized.bind(this));
return 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()});
this.setChatState(converse.INACTIVE).model.minimize();
this.$el.hide('fast', this.onMinimized.bind(this));
},
},
ChatRoomView: {
events: {
'click .toggle-chatbox-button': 'minimize',
},
initialize: function () {
this.model.on('change:minimized', function (item) {
if (item.get('minimized')) {
this.hide();
} else {
this.maximize();
}
}, this);
var result = this._super.initialize.apply(this, arguments);
if (this.model.get('minimized')) {
this.hide();
}
return result;
}
},
ChatBoxes: {
chatBoxMayBeShown: function (chatbox) {
return this._super.chatBoxMayBeShown.apply(this, arguments) &&
!chatbox.get('minimized');
},
},
ChatBoxViews: {
showChat: function (attrs) {
/* Find the chat box and show it. If it doesn't exist, create it.
*/
var chatbox = this._super.showChat.apply(this, arguments);
if (chatbox.get('minimized')) {
chatbox.maximize();
}
return chatbox;
},
getChatBoxWidth: function (view) {
if (!view.model.get('minimized') && view.$el.is(':visible')) {
return view.$el.outerWidth(true);
}
return 0;
},
getShownChats: function () {
return this.filter(function (view) {
return (!view.model.get('minimized') && view.$el.is(':visible'));
});
},
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 minimizes the oldest chat box
* to create space.
*/
var shown_chats = this.getShownChats();
if (converse.no_trimming || shown_chats.length <= 1) {
return;
}
if (this.getChatBoxWidth(shown_chats[0]) === $('body').outerWidth(true)) {
// If the chats shown are the same width as the body,
// then we're in responsive mode and the chats are
// fullscreen. In this case we don't trim.
return;
}
var oldest_chat, boxes_width, view,
$minimized = converse.minimized_chats.$el,
minimized_width = _.contains(this.model.pluck('minimized'), true) ? $minimized.outerWidth(true) : 0,
new_id = newchat ? newchat.model.get('id') : null;
boxes_width = _.reduce(this.xget(new_id), function (memo, view) {
return memo + this.getChatBoxWidth(view);
}.bind(this), newchat ? newchat.$el.outerWidth(true) : 0);
if ((minimized_width + boxes_width) > $('body').outerWidth(true)) {
oldest_chat = this.getOldestMaximizedChat([new_id]);
if (oldest_chat) {
// We hide the chat immediately, because waiting
// for the event to fire (and letting the
// ChatBoxView hide it then) causes race
// conditions.
view = this.get(oldest_chat.get('id'));
if (view) {
view.$el.hide();
}
oldest_chat.minimize();
}
}
},
getOldestMaximizedChat: function (exclude_ids) {
// Get oldest view (if its id is not excluded)
exclude_ids.push('controlbox');
var i = 0;
var model = this.model.sort().at(i);
while (_.contains(exclude_ids, model.get('id')) ||
model.get('minimized') === true) {
i++;
model = this.model.at(i);
if (!model) {
return null;
}
}
return model;
}
}
},
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
this.updateSettings({
no_trimming: false, // Set to true for phantomjs tests (where browser apparently has no width)
});
converse.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);
// OTR stuff, doesn't require this module to depend on OTR.
this.model.on('showReceivedOTRMessage', this.updateUnreadMessagesCounter, this);
this.model.on('showSentOTRMessage', this.updateUnreadMessagesCounter, this);
},
render: function () {
var data = _.extend(
this.model.toJSON(),
{ 'tooltip': __('Click to restore this chat') }
);
if (this.model.get('type') === '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();
var view = converse.chatboxviews.get(this.model.get('id'));
if (view) {
// This will call model.destroy(), removing it from the
// collection and will also emit 'chatBoxClosed'
view.close();
} else {
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)
});
converse.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();
converse.chatboxviews.trimChats.bind(converse.chatboxviews);
} else if (this.keys().length === 1 && !this.$el.is(':visible')) {
this.$el.show('fast', converse.chatboxviews.trimChats.bind(converse.chatboxviews));
}
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') {
// The ControlBox has it's own minimize toggle
return;
}
if (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
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define, window */
(function (root, factory) {
define("converse-dragresize", [
"converse-core",
"converse-api",
"converse-chatview",
"converse-muc", // XXX: would like to remove this
"converse-controlbox"
], factory);
}(this, function (converse, converse_api) {
"use strict";
var $ = converse_api.env.jQuery,
_ = converse_api.env._;
converse_api.plugins.add('dragresize', {
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
registerGlobalEventHandlers: function () {
$(document).on('mousemove', function (ev) {
if (!this.resizing || !this.allow_dragresize) { return true; }
ev.preventDefault();
this.resizing.chatbox.resizeChatBox(ev);
}.bind(this));
$(document).on('mouseup', function (ev) {
if (!this.resizing || !this.allow_dragresize) { return true; }
ev.preventDefault();
var height = this.applyDragResistance(
this.resizing.chatbox.height,
this.resizing.chatbox.model.get('default_height')
);
var width = this.applyDragResistance(
this.resizing.chatbox.width,
this.resizing.chatbox.model.get('default_width')
);
if (this.connection.connected) {
this.resizing.chatbox.model.save({'height': height});
this.resizing.chatbox.model.save({'width': width});
} else {
this.resizing.chatbox.model.set({'height': height});
this.resizing.chatbox.model.set({'width': width});
}
this.resizing = null;
}.bind(this));
return this._super.registerGlobalEventHandlers.apply(this, arguments);
},
ChatBox: {
initialize: function () {
var result = this._super.initialize.apply(this, arguments),
height = this.get('height'), width = this.get('width'),
save = this.get('id') === 'controlbox' ? this.set.bind(this) : this.save.bind(this);
save({
'height': converse.applyDragResistance(height, this.get('default_height')),
'width': converse.applyDragResistance(width, this.get('default_width')),
});
return result;
}
},
ChatBoxView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
initialize: function () {
$(window).on('resize', _.debounce(this.setDimensions.bind(this), 100));
this._super.initialize.apply(this, arguments);
},
render: function () {
var result = this._super.render.apply(this, arguments);
this.setWidth();
return result;
},
setWidth: function () {
// If a custom width is applied (due to drag-resizing),
// then we need to set the width of the .chatbox element as well.
if (this.model.get('width')) {
this.$el.css('width', this.model.get('width'));
}
},
_show: function () {
this.initDragResize().setDimensions();
this._super._show.apply(this, arguments);
},
initDragResize: function () {
/* Determine and store the default box size.
* We need this information for the drag-resizing feature.
*/
var $flyout = this.$el.find('.box-flyout');
if (typeof this.model.get('height') === 'undefined') {
var height = $flyout.height();
var width = $flyout.width();
this.model.set('height', height);
this.model.set('default_height', height);
this.model.set('width', width);
this.model.set('default_width', width);
}
var min_width = $flyout.css('min-width');
var min_height = $flyout.css('min-height');
this.model.set('min_width', min_width.endsWith('px') ? Number(min_width.replace(/px$/, '')) :0);
this.model.set('min_height', min_height.endsWith('px') ? Number(min_height.replace(/px$/, '')) :0);
// Initialize last known mouse position
this.prev_pageY = 0;
this.prev_pageX = 0;
if (converse.connection.connected) {
this.height = this.model.get('height');
this.width = this.model.get('width');
}
return this;
},
setDimensions: function () {
// Make sure the chat box has the right height and width.
this.adjustToViewport();
this.setChatBoxHeight(this.model.get('height'));
this.setChatBoxWidth(this.model.get('width'));
},
setChatBoxHeight: function (height) {
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 (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;
},
adjustToViewport: function () {
/* Event handler called when viewport gets resized. We remove
* custom width/height from chat boxes.
*/
var viewport_width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
var viewport_height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
if (viewport_width <= 480) {
this.model.set('height', undefined);
this.model.set('width', undefined);
} else if (viewport_width <= this.model.get('width')) {
this.model.set('width', undefined);
} else if (viewport_height <= this.model.get('height')) {
this.model.set('height', undefined);
}
},
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';
},
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);
}
}
}
},
ControlBoxView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
initialize: function () {
$(window).on('resize', _.debounce(this.setDimensions.bind(this), 100));
this._super.initialize.apply(this, arguments);
},
renderLoginPanel: function () {
var result = this._super.renderLoginPanel.apply(this, arguments);
this.initDragResize().setDimensions();
return result;
},
renderContactsPanel: function () {
var result = this._super.renderContactsPanel.apply(this, arguments);
this.initDragResize().setDimensions();
return result;
}
},
ChatRoomView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
initialize: function () {
$(window).on('resize', _.debounce(this.setDimensions.bind(this), 100));
this._super.initialize.apply(this, arguments);
},
render: function () {
var result = this._super.render.apply(this, arguments);
this.setWidth();
return result;
}
}
},
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
var converse = this.converse;
this.updateSettings({
allow_dragresize: true,
});
converse.applyDragResistance = function (value, default_value) {
/* This method applies some resistance around the
* default_value. If value is close enough to
* default_value, then default_value is returned instead.
*/
if (typeof value === 'undefined') {
return undefined;
} else if (typeof default_value === 'undefined') {
return value;
}
var resistance = 10;
if ((value !== default_value) &&
(Math.abs(value- default_value) < resistance)) {
return default_value;
}
return value;
};
}
});
}));
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2016, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define, window */
(function (root, factory) {
define("converse-headline", [
"converse-core",
"converse-api",
"converse-chatview"
], factory);
}(this, function (converse, converse_api) {
"use strict";
var $ = converse_api.env.jQuery,
_ = converse_api.env._,
utils = converse_api.env.utils,
__ = utils.__.bind(converse);
var onHeadlineMessage = function (message) {
/* Handler method for all incoming messages of type "headline".
*/
var $message = $(message),
from_jid = $message.attr('from');
if (utils.isHeadlineMessage(message)) {
converse.chatboxes.create({
'id': from_jid,
'jid': from_jid,
'fullname': from_jid,
'type': 'headline'
}).createMessage($message);
converse.emit('message', message);
}
return true;
};
converse_api.plugins.add('headline', {
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
ChatBoxViews: {
onChatBoxAdded: function (item) {
var view = this.get(item.get('id'));
if (!view && item.get('type') === 'headline') {
view = new converse.HeadlinesBoxView({model: item});
this.add(item.get('id'), view);
return view;
} else {
return this._super.onChatBoxAdded.apply(this, arguments);
}
}
}
},
initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
converse.HeadlinesBoxView = converse.ChatBoxView.extend({
className: 'chatbox headlines',
events: {
'click .close-chatbox-button': 'close',
'click .toggle-chatbox-button': 'minimize',
'keypress textarea.chat-textarea': 'keyPressed',
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
initialize: function () {
this.disable_mam = true; // Don't do MAM queries for this box
$(window).on('resize', _.debounce(this.setDimensions.bind(this), 100));
this.model.messages.on('add', this.onMessageAdded, this);
this.model.on('show', this.show, this);
this.model.on('destroy', this.hide, this);
this.model.on('change:minimized', this.onMinimizedChanged, this);
this.render().fetchMessages().insertIntoPage().hide();
converse.emit('chatBoxInitialized', this);
},
render: function () {
this.$el.attr('id', this.model.get('box_id'))
.html(converse.templates.chatbox(
_.extend(this.model.toJSON(), {
show_toolbar: converse.show_toolbar,
show_textarea: false,
title: this.model.get('fullname'),
info_close: __('Close this box'),
info_minimize: __('Minimize this box'),
label_personal_message: ''
}
)
)
);
this.setWidth();
this.$content = this.$el.find('.chat-content');
converse.emit('chatBoxOpened', this);
window.setTimeout(utils.refreshWebkit, 50);
return this;
}
});
var registerHeadlineHandler = function () {
converse.connection.addHandler(
onHeadlineMessage, null, 'message');
};
converse.on('connected', registerHeadlineHandler);
converse.on('reconnected', registerHeadlineHandler);
}
});
}));
var config;
if (typeof(require) === 'undefined') {
/* XXX: Hack to work around r.js's stupid parsing.
* We want to save the configuration in a variable so that we can reuse it in
* tests/main.js.
*/
require = { // jshint ignore:line
config: function (c) {
config = c;
}
};
}
require.config({
baseUrl: '.',
paths: {
"backbone": "components/backbone/backbone",
"backbone.browserStorage": "components/backbone.browserStorage/backbone.browserStorage",
"backbone.overview": "components/backbone.overview/backbone.overview",
"eventemitter": "components/otr/build/dep/eventemitter",
"jquery": "components/jquery/dist/jquery",
"jquery-private": "src/jquery-private",
"jquery.browser": "components/jquery.browser/dist/jquery.browser",
"jquery.easing": "components/jquery-easing-original/index", // XXX: Only required for https://conversejs.org website
"moment": "components/momentjs/moment",
"strophe": "components/strophejs/src/wrapper",
"strophe-base64": "components/strophejs/src/base64",
"strophe-bosh": "components/strophejs/src/bosh",
"strophe-core": "components/strophejs/src/core",
"strophe-md5": "components/strophejs/src/md5",
"strophe-polyfill": "components/strophejs/src/polyfills",
"strophe-sha1": "components/strophejs/src/sha1",
"strophe-utils": "components/strophejs/src/utils",
"strophe-websocket": "components/strophejs/src/websocket",
"strophe.disco": "components/strophejs-plugins/disco/strophe.disco",
"strophe.ping": "src/strophe.ping",
"strophe.rsm": "components/strophejs-plugins/rsm/strophe.rsm",
"strophe.vcard": "src/strophe.vcard",
"text": 'components/requirejs-text/text',
"tpl": 'components/requirejs-tpl-jcbrand/tpl',
"typeahead": "components/typeahead.js/index",
"underscore": "components/underscore/underscore",
"utils": "src/utils",
"polyfill": "src/polyfill",
// Converse
"converse-api": "src/converse-api",
"converse-chatview": "src/converse-chatview",
"converse-controlbox": "src/converse-controlbox",
"converse-core": "src/converse-core",
"converse-dragresize": "src/converse-dragresize",
"converse-headline": "src/converse-headline",
"converse-mam": "src/converse-mam",
"converse-minimize": "src/converse-minimize",
"converse-muc": "src/converse-muc",
"converse-notification": "src/converse-notification",
"converse-otr": "src/converse-otr",
"converse-ping": "src/converse-ping",
"converse-register": "src/converse-register",
"converse-rosterview": "src/converse-rosterview",
"converse-templates": "src/converse-templates",
"converse-vcard": "src/converse-vcard",
// Off-the-record-encryption
"bigint": "src/bigint",
"crypto": "src/crypto",
"crypto.aes": "components/otr/vendor/cryptojs/aes",
"crypto.cipher-core": "components/otr/vendor/cryptojs/cipher-core",
"crypto.core": "components/otr/vendor/cryptojs/core",
"crypto.enc-base64": "components/otr/vendor/cryptojs/enc-base64",
"crypto.evpkdf": "components/crypto-js-evanvosberg/src/evpkdf",
"crypto.hmac": "components/otr/vendor/cryptojs/hmac",
"crypto.md5": "components/crypto-js-evanvosberg/src/md5",
"crypto.mode-ctr": "components/otr/vendor/cryptojs/mode-ctr",
"crypto.pad-nopadding": "components/otr/vendor/cryptojs/pad-nopadding",
"crypto.sha1": "components/otr/vendor/cryptojs/sha1",
"crypto.sha256": "components/otr/vendor/cryptojs/sha256",
"salsa20": "components/otr/build/dep/salsa20",
"otr": "src/otr",
// Locales paths
"locales": "src/locales",
"jed": "components/jed/jed",
"af": "locale/af/LC_MESSAGES/converse.json",
"ca": "locale/ca/LC_MESSAGES/converse.json",
"de": "locale/de/LC_MESSAGES/converse.json",
"en": "locale/en/LC_MESSAGES/converse.json",
"es": "locale/es/LC_MESSAGES/converse.json",
"fr": "locale/fr/LC_MESSAGES/converse.json",
"he": "locale/he/LC_MESSAGES/converse.json",
"hu": "locale/hu/LC_MESSAGES/converse.json",
"id": "locale/id/LC_MESSAGES/converse.json",
"it": "locale/it/LC_MESSAGES/converse.json",
"ja": "locale/ja/LC_MESSAGES/converse.json",
"nb": "locale/nb/LC_MESSAGES/converse.json",
"nl": "locale/nl/LC_MESSAGES/converse.json",
"pl": "locale/pl/LC_MESSAGES/converse.json",
"pt_BR": "locale/pt_BR/LC_MESSAGES/converse.json",
"ru": "locale/ru/LC_MESSAGES/converse.json",
"uk": "locale/uk/LC_MESSAGES/converse.json",
"zh": "locale/zh/LC_MESSAGES/converse.json",
"moment_with_locales": "src/moment_locales",
'moment_af': "components/momentjs/locale/af",
'moment_de': "components/momentjs/locale/de",
'moment_es': "components/momentjs/locale/es",
'moment_fr': "components/momentjs/locale/fr",
'moment_he': "components/momentjs/locale/he",
'moment_hu': "components/momentjs/locale/hu",
'moment_id': "components/momentjs/locale/id",
'moment_it': "components/momentjs/locale/it",
'moment_ja': "components/momentjs/locale/ja",
'moment_nb': "components/momentjs/locale/nb",
'moment_nl': "components/momentjs/locale/nl",
'moment_pl': "components/momentjs/locale/pl",
'moment_pt-br': "components/momentjs/locale/pt-br",
'moment_ru': "components/momentjs/locale/ru",
'moment_uk': "components/momentjs/locale/uk",
'moment_zh': "components/momentjs/locale/zh-cn",
// Templates
"action": "src/templates/action",
"add_contact_dropdown": "src/templates/add_contact_dropdown",
"add_contact_form": "src/templates/add_contact_form",
"change_status_message": "src/templates/change_status_message",
"chat_status": "src/templates/chat_status",
"chatarea": "src/templates/chatarea",
"chatbox": "src/templates/chatbox",
"chatroom": "src/templates/chatroom",
"chatroom_form": "src/templates/chatroom_form",
"chatroom_password_form": "src/templates/chatroom_password_form",
"chatroom_sidebar": "src/templates/chatroom_sidebar",
"chatrooms_tab": "src/templates/chatrooms_tab",
"chats_panel": "src/templates/chats_panel",
"choose_status": "src/templates/choose_status",
"contacts_panel": "src/templates/contacts_panel",
"contacts_tab": "src/templates/contacts_tab",
"controlbox": "src/templates/controlbox",
"controlbox_toggle": "src/templates/controlbox_toggle",
"field": "src/templates/field",
"form_captcha": "src/templates/form_captcha",
"form_checkbox": "src/templates/form_checkbox",
"form_input": "src/templates/form_input",
"form_select": "src/templates/form_select",
"form_textarea": "src/templates/form_textarea",
"form_username": "src/templates/form_username",
"group_header": "src/templates/group_header",
"info": "src/templates/info",
"login_panel": "src/templates/login_panel",
"login_tab": "src/templates/login_tab",
"message": "src/templates/message",
"new_day": "src/templates/new_day",
"occupant": "src/templates/occupant",
"pending_contact": "src/templates/pending_contact",
"pending_contacts": "src/templates/pending_contacts",
"register_panel": "src/templates/register_panel",
"register_tab": "src/templates/register_tab",
"registration_form": "src/templates/registration_form",
"registration_request": "src/templates/registration_request",
"requesting_contact": "src/templates/requesting_contact",
"requesting_contacts": "src/templates/requesting_contacts",
"room_description": "src/templates/room_description",
"room_item": "src/templates/room_item",
"room_panel": "src/templates/room_panel",
"roster": "src/templates/roster",
"roster_item": "src/templates/roster_item",
"search_contact": "src/templates/search_contact",
"select_option": "src/templates/select_option",
"status_option": "src/templates/status_option",
"toggle_chats": "src/templates/toggle_chats",
"toolbar": "src/templates/toolbar",
"toolbar_otr": "src/templates/toolbar_otr",
"trimmed_chat": "src/templates/trimmed_chat",
"vcard": "src/templates/vcard",
"chatbox_minimize": "src/templates/chatbox_minimize"
},
map: {
// '*' means all modules will get 'jquery-private'
// for their 'jquery' dependency.
'*': { 'jquery': 'jquery-private' },
// 'jquery-private' wants the real jQuery module
// though. If this line was not here, there would
// be an unresolvable cyclic dependency.
'jquery-private': { 'jquery': 'jquery' }
},
tpl: {
// Configuration for requirejs-tpl
// Use Mustache style syntax for variable interpolation
templateSettings: {
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
}
},
// define module dependencies for modules not using define
shim: {
'crypto.aes': { deps: ['crypto.cipher-core'] },
'crypto.cipher-core': { deps: ['crypto.enc-base64', 'crypto.evpkdf'] },
'crypto.enc-base64': { deps: ['crypto.core'] },
'crypto.evpkdf': { deps: ['crypto.md5'] },
'crypto.hmac': { deps: ['crypto.core'] },
'crypto.md5': { deps: ['crypto.core'] },
'crypto.mode-ctr': { deps: ['crypto.cipher-core'] },
'crypto.pad-nopadding': { deps: ['crypto.cipher-core'] },
'crypto.sha1': { deps: ['crypto.core'] },
'crypto.sha256': { deps: ['crypto.core'] },
'bigint': { deps: ['crypto'] },
'strophe.ping': { deps: ['strophe'] },
'strophe.register': { deps: ['strophe'] },
'strophe.vcard': { deps: ['strophe'] }
}
});
/* Converse.js components configuration
*
* This file is used to tell require.js which components (or plugins) to load
* when it generates a build.
*/
if (typeof define !== 'undefined') {
/* When running tests, define is not defined. */
define("converse", [
"jquery",
"converse-api",
/* START: Removable components
* --------------------
* Any of the following components may be removed if they're not needed.
*/
"locales", // Translations for converse.js. This line can be removed
// to remove *all* translations, or you can modify the
// file src/locales.js to include only those
// translations that you care about.
"converse-chatview", // Renders standalone chat boxes for single user chat
"converse-mam", // XEP-0313 Message Archive Management
"converse-muc", // XEP-0045 Multi-user chat
"converse-vcard", // XEP-0054 VCard-temp
"converse-otr", // Off-the-record encryption for one-on-one messages
"converse-controlbox", // The control box
"converse-register", // XEP-0077 In-band registration
"converse-ping", // XEP-0199 XMPP Ping
"converse-notification",// HTML5 Notifications
"converse-minimize", // Allows chat boxes to be minimized
"converse-dragresize", // Allows chat boxes to be resized by dragging them
"converse-headline", // Support for headline messages
/* END: Removable components */
], function($, converse_api) {
window.converse = converse_api;
$(window).trigger('converse-loaded', converse_api);
return converse_api;
});
}
;
require(["converse"]);
/*global jQuery */
define('jquery', [], function () { return jQuery; });
define('jquery-private', [], function () { return jQuery; });
/*global jQuery, _, moment, Strophe, $build, $iq, $msg, $pres, SHA1, Base64, MD5, DSA, OTR */
define('jquery.browser', [], function () { return jQuery; });
define('typeahead', [], function () { return jQuery; });
define('underscore', [], function () { return _; });
define('moment_with_locales', [], function () { return moment; });
define('strophe', [], function () {
return {
'Strophe': Strophe,
'$build': $build,
'$iq': $iq,
'$msg': $msg,
'$pres': $pres,
'SHA1': SHA1,
'Base64': Base64,
'MD5': MD5,
'b64_hmac_sha1': SHA1.b64_hmac_sha1,
'b64_sha1': SHA1.b64_sha1,
'str_hmac_sha1': SHA1.str_hmac_sha1,
'str_sha1': SHA1.str_sha1
};
});
var strophePlugin = function () { return Strophe; };
var emptyFunction = function () { };
define('strophe.disco', ['strophe'], strophePlugin);
define('strophe.ping', ['strophe'], strophePlugin);
define('strophe.rsm', ['strophe'], strophePlugin);
define('strophe.vcard', ['strophe'], strophePlugin);
define('backbone', [], emptyFunction);
define('backbone.browserStorage', ['backbone'], emptyFunction);
define('backbone.overview', ['backbone'], emptyFunction);
define('otr', [], function () { return { 'DSA': DSA, 'OTR': OTR };});
define("locales", [], emptyFunction);