2017-08-15 21:23:30 +02:00
|
|
|
// Converse.js (A browser based XMPP chat client)
|
|
|
|
// http://conversejs.org
|
|
|
|
//
|
|
|
|
// This is the utilities module.
|
|
|
|
//
|
|
|
|
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
|
|
|
|
// Licensed under the Mozilla Public License (MPLv2)
|
|
|
|
//
|
2017-01-26 15:49:02 +01:00
|
|
|
/*global define, escape, locales, Jed */
|
2015-01-16 22:07:27 +01:00
|
|
|
(function (root, factory) {
|
2016-02-23 08:13:30 +01:00
|
|
|
define([
|
2017-06-14 18:41:45 +02:00
|
|
|
"sizzle",
|
2017-07-10 21:14:48 +02:00
|
|
|
"es6-promise",
|
2016-02-23 08:13:30 +01:00
|
|
|
"jquery.browser",
|
2017-04-21 15:52:16 +02:00
|
|
|
"lodash.noconflict",
|
2017-04-19 14:18:30 +02:00
|
|
|
"locales",
|
|
|
|
"moment_with_locales",
|
2017-06-14 18:41:45 +02:00
|
|
|
"strophe",
|
2016-09-23 10:54:55 +02:00
|
|
|
"tpl!field",
|
|
|
|
"tpl!select_option",
|
|
|
|
"tpl!form_select",
|
|
|
|
"tpl!form_textarea",
|
|
|
|
"tpl!form_checkbox",
|
|
|
|
"tpl!form_username",
|
|
|
|
"tpl!form_input",
|
|
|
|
"tpl!form_captcha"
|
2016-02-23 08:13:30 +01:00
|
|
|
], factory);
|
2016-09-23 10:54:55 +02:00
|
|
|
}(this, function (
|
2017-08-15 16:36:43 +02:00
|
|
|
sizzle,
|
2017-07-10 21:14:48 +02:00
|
|
|
Promise,
|
2017-08-15 16:36:43 +02:00
|
|
|
jQBrowser,
|
|
|
|
_,
|
2017-06-14 19:18:14 +02:00
|
|
|
locales,
|
|
|
|
moment,
|
2017-06-14 18:41:45 +02:00
|
|
|
Strophe,
|
2016-09-23 10:54:55 +02:00
|
|
|
tpl_field,
|
|
|
|
tpl_select_option,
|
|
|
|
tpl_form_select,
|
|
|
|
tpl_form_textarea,
|
|
|
|
tpl_form_checkbox,
|
|
|
|
tpl_form_username,
|
|
|
|
tpl_form_input,
|
|
|
|
tpl_form_captcha
|
|
|
|
) {
|
2014-11-16 12:47:30 +01:00
|
|
|
"use strict";
|
2017-04-19 14:37:59 +02:00
|
|
|
locales = locales || {};
|
2017-07-16 01:24:33 +02:00
|
|
|
const b64_sha1 = Strophe.SHA1.b64_sha1;
|
2017-06-14 18:41:45 +02:00
|
|
|
Strophe = Strophe.Strophe;
|
2014-11-16 12:47:30 +01:00
|
|
|
|
2017-07-16 01:24:33 +02:00
|
|
|
const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b/g;
|
|
|
|
|
2014-11-16 12:47:30 +01:00
|
|
|
var XFORM_TYPE_MAP = {
|
|
|
|
'text-private': 'password',
|
2015-10-29 08:12:13 +01:00
|
|
|
'text-single': 'text',
|
2014-11-16 12:47:30 +01:00
|
|
|
'fixed': 'label',
|
|
|
|
'boolean': 'checkbox',
|
|
|
|
'hidden': 'hidden',
|
|
|
|
'jid-multi': 'textarea',
|
|
|
|
'list-single': 'dropdown',
|
|
|
|
'list-multi': 'dropdown'
|
|
|
|
};
|
|
|
|
|
2017-02-02 16:07:00 +01:00
|
|
|
var afterAnimationEnd = function (el, callback) {
|
|
|
|
el.classList.remove('visible');
|
|
|
|
if (_.isFunction(callback)) {
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-04-04 16:05:50 +02:00
|
|
|
var unescapeHTML = function (htmlEscapedText) {
|
|
|
|
/* Helper method that replace HTML-escaped symbols with equivalent characters
|
|
|
|
* (e.g. transform occurrences of '&' to '&')
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (String) htmlEscapedText: a String containing the HTML-escaped symbols.
|
|
|
|
*/
|
|
|
|
var div = document.createElement('div');
|
|
|
|
div.innerHTML = htmlEscapedText;
|
|
|
|
return div.innerText;
|
2017-06-12 20:30:58 +02:00
|
|
|
};
|
2017-04-04 16:05:50 +02:00
|
|
|
|
2016-05-28 13:13:49 +02:00
|
|
|
var isImage = function (url) {
|
2017-07-11 17:21:13 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
var img = new Image();
|
|
|
|
var timer = window.setTimeout(function () {
|
|
|
|
reject(new Error("Could not determine whether it's an image"));
|
|
|
|
img = null;
|
|
|
|
}, 3000);
|
|
|
|
img.onerror = img.onabort = function () {
|
|
|
|
clearTimeout(timer);
|
|
|
|
reject(new Error("Could not determine whether it's an image"));
|
|
|
|
};
|
|
|
|
img.onload = function () {
|
|
|
|
clearTimeout(timer);
|
|
|
|
resolve(img);
|
|
|
|
};
|
|
|
|
img.src = url;
|
|
|
|
});
|
2016-05-28 13:13:49 +02:00
|
|
|
};
|
|
|
|
|
2017-07-15 11:03:22 +02:00
|
|
|
function calculateSlideStep (height) {
|
|
|
|
if (height > 100) {
|
|
|
|
return 10;
|
|
|
|
} else if (height > 50) {
|
|
|
|
return 5;
|
|
|
|
} else {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
var u = {};
|
2017-07-15 11:03:22 +02:00
|
|
|
|
|
|
|
// Translation machinery
|
|
|
|
// ---------------------
|
2017-08-15 21:23:30 +02:00
|
|
|
u.__ = function (str) {
|
|
|
|
if (!u.isConverseLocale(this.locale) || this.locale === 'en') {
|
2017-07-15 11:03:22 +02:00
|
|
|
return Jed.sprintf.apply(Jed, arguments);
|
|
|
|
}
|
|
|
|
if (typeof this.jed === "undefined") {
|
|
|
|
this.jed = new Jed(window.JSON.parse(locales[this.locale]));
|
|
|
|
}
|
|
|
|
var t = this.jed.translate(str);
|
|
|
|
if (arguments.length>1) {
|
|
|
|
return t.fetch.apply(t, [].slice.call(arguments,1));
|
|
|
|
} else {
|
|
|
|
return t.fetch();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.___ = function (str) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* 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;
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isLocaleAvailable = function (locale, available) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* 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;
|
2016-07-18 13:41:31 +02:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
|
|
|
};
|
2016-07-18 13:41:31 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.addHyperlinks = function (text) {
|
2017-07-16 01:24:33 +02:00
|
|
|
const list = text.match(URL_REGEX) || [];
|
|
|
|
var links = [];
|
2017-07-15 17:33:25 +02:00
|
|
|
_.each(list, (match) => {
|
|
|
|
const prot = match.indexOf('http://') === 0 || match.indexOf('https://') === 0 ? '' : 'http://';
|
|
|
|
const url = prot + encodeURI(decodeURI(match)).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
|
2017-07-16 01:24:33 +02:00
|
|
|
const a = '<a target="_blank" rel="noopener" href="' + url + '">'+ _.escape(match) + '</a>';
|
|
|
|
// We first insert a hash of the code that will be inserted, and
|
|
|
|
// then later replace that with the code itself. That way we avoid
|
|
|
|
// issues when some matches are substrings of others.
|
|
|
|
links.push(a);
|
|
|
|
text = text.replace(match, b64_sha1(a));
|
2017-07-15 17:33:25 +02:00
|
|
|
});
|
2017-07-16 01:24:33 +02:00
|
|
|
while (links.length) {
|
|
|
|
const a = links.pop();
|
|
|
|
text = text.replace(b64_sha1(a), a);
|
|
|
|
}
|
2017-07-15 20:13:44 +02:00
|
|
|
return text;
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.renderImageURLs = function (obj) {
|
2017-07-16 01:24:33 +02:00
|
|
|
const list = obj.textContent.match(URL_REGEX) || [];
|
2017-07-15 17:33:25 +02:00
|
|
|
_.forEach(list, function (url) {
|
2017-07-16 01:24:33 +02:00
|
|
|
isImage(url).then(function (img) {
|
2017-07-15 17:33:25 +02:00
|
|
|
img.className = 'chat-image';
|
2017-07-16 01:24:33 +02:00
|
|
|
var anchors = sizzle(`a[href="${url}"]`, obj);
|
|
|
|
_.each(anchors, (a) => { a.innerHTML = img.outerHTML; });
|
2017-07-15 17:33:25 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
return obj;
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.slideInAllElements = function (elements) {
|
2017-07-15 11:03:22 +02:00
|
|
|
return Promise.all(
|
|
|
|
_.map(
|
|
|
|
elements,
|
2017-08-15 21:23:30 +02:00
|
|
|
_.partial(u.slideIn, _, 600)
|
2017-07-15 11:03:22 +02:00
|
|
|
));
|
|
|
|
};
|
2017-06-16 11:31:57 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.slideToggleElement = function (el) {
|
2017-07-17 21:53:33 +02:00
|
|
|
if (_.includes(el.classList, 'collapsed')) {
|
2017-08-15 21:23:30 +02:00
|
|
|
return u.slideOut(el);
|
2017-07-15 11:03:22 +02:00
|
|
|
} else {
|
2017-08-15 21:23:30 +02:00
|
|
|
return u.slideIn(el);
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.slideOut = function (el, duration=900) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Shows/expands an element by sliding it out of itself. */
|
2017-07-17 18:07:35 +02:00
|
|
|
|
|
|
|
function calculateEndHeight (el) {
|
|
|
|
return _.reduce(
|
|
|
|
el.children,
|
|
|
|
(result, child) => result + child.offsetHeight, 0
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-07-17 22:01:23 +02:00
|
|
|
function wrapup (el) {
|
|
|
|
el.removeAttribute('data-slider-marker');
|
|
|
|
el.classList.remove('collapsed');
|
|
|
|
el.style.overflow = "";
|
|
|
|
el.style.height = "";
|
|
|
|
}
|
|
|
|
|
2017-07-15 11:03:22 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (_.isNil(el)) {
|
|
|
|
const err = "Undefined or null element passed into slideOut"
|
|
|
|
console.warn(err);
|
|
|
|
reject(new Error(err));
|
2017-07-15 15:15:37 +02:00
|
|
|
return;
|
2017-06-16 11:31:57 +02:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
let interval_marker = el.getAttribute('data-slider-marker');
|
|
|
|
if (interval_marker) {
|
2017-07-15 15:15:37 +02:00
|
|
|
el.removeAttribute('data-slider-marker');
|
2017-07-15 11:03:22 +02:00
|
|
|
window.clearInterval(interval_marker);
|
|
|
|
}
|
2017-07-17 18:07:35 +02:00
|
|
|
const end_height = calculateEndHeight(el);
|
2017-08-15 21:23:30 +02:00
|
|
|
if (window.converse_disable_effects) { // Effects are disabled (for tests)
|
2017-07-15 15:15:37 +02:00
|
|
|
el.style.height = end_height + 'px';
|
2017-07-17 22:01:23 +02:00
|
|
|
wrapup(el);
|
2017-07-15 15:15:37 +02:00
|
|
|
resolve();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-07-15 11:03:22 +02:00
|
|
|
const step = calculateSlideStep(end_height),
|
|
|
|
interval = end_height/duration*step;
|
|
|
|
let h = 0;
|
2017-07-17 21:53:33 +02:00
|
|
|
|
2017-07-15 11:03:22 +02:00
|
|
|
interval_marker = window.setInterval(function () {
|
|
|
|
h += step;
|
|
|
|
if (h < end_height) {
|
2017-07-14 10:01:00 +02:00
|
|
|
el.style.height = h + 'px';
|
2017-07-15 11:03:22 +02:00
|
|
|
} else {
|
2017-07-17 18:07:35 +02:00
|
|
|
// We recalculate the height to work around an apparent
|
|
|
|
// browser bug where browsers don't know the correct
|
|
|
|
// offsetHeight beforehand.
|
|
|
|
el.style.height = calculateEndHeight(el) + 'px';
|
2017-07-15 11:03:22 +02:00
|
|
|
window.clearInterval(interval_marker);
|
2017-07-17 22:01:23 +02:00
|
|
|
wrapup(el);
|
2017-07-15 11:03:22 +02:00
|
|
|
resolve();
|
2017-07-14 10:01:00 +02:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
}, interval);
|
|
|
|
el.setAttribute('data-slider-marker', interval_marker);
|
|
|
|
});
|
|
|
|
};
|
2017-07-14 10:01:00 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.slideIn = function (el, duration=600) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Hides/collapses an element by sliding it into itself. */
|
|
|
|
return new Promise((resolve, reject) => {
|
2017-07-14 10:01:00 +02:00
|
|
|
if (_.isNil(el)) {
|
2017-07-15 11:03:22 +02:00
|
|
|
const err = "Undefined or null element passed into slideIn";
|
|
|
|
console.warn(err);
|
2017-07-15 15:15:37 +02:00
|
|
|
return reject(new Error(err));
|
2017-07-17 21:53:33 +02:00
|
|
|
} else if (_.includes(el.classList, 'collapsed')) {
|
2017-07-15 15:15:37 +02:00
|
|
|
return resolve();
|
2017-08-15 21:23:30 +02:00
|
|
|
} else if (window.converse_disable_effects) { // Effects are disabled (for tests)
|
2017-07-19 09:10:17 +02:00
|
|
|
el.classList.add('collapsed');
|
|
|
|
el.style.height = "";
|
2017-07-15 15:15:37 +02:00
|
|
|
return resolve();
|
2016-11-22 17:42:58 +01:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
let interval_marker = el.getAttribute('data-slider-marker');
|
|
|
|
if (interval_marker) {
|
2017-07-15 15:15:37 +02:00
|
|
|
el.removeAttribute('data-slider-marker');
|
2017-07-15 11:03:22 +02:00
|
|
|
window.clearInterval(interval_marker);
|
2017-02-02 16:07:00 +01:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
let h = el.offsetHeight;
|
|
|
|
const step = calculateSlideStep(h),
|
|
|
|
interval = h/duration*step;
|
|
|
|
|
|
|
|
el.style.overflow = 'hidden';
|
|
|
|
|
|
|
|
interval_marker = window.setInterval(function () {
|
|
|
|
h -= step;
|
|
|
|
if (h > 0) {
|
|
|
|
el.style.height = h + 'px';
|
|
|
|
} else {
|
2017-07-19 09:10:17 +02:00
|
|
|
el.removeAttribute('data-slider-marker');
|
|
|
|
window.clearInterval(interval_marker);
|
2017-07-17 21:53:33 +02:00
|
|
|
el.classList.add('collapsed');
|
|
|
|
el.style.height = "";
|
2017-07-15 11:03:22 +02:00
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
}, interval);
|
|
|
|
el.setAttribute('data-slider-marker', interval_marker);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.fadeIn = function (el, callback) {
|
2017-07-15 11:03:22 +02:00
|
|
|
if (_.isNil(el)) {
|
|
|
|
console.warn("Undefined or null element passed into fadeIn");
|
|
|
|
}
|
2017-08-15 21:23:30 +02:00
|
|
|
if (window.converse_disable_effects) { // Effects are disabled (for tests)
|
2017-07-15 11:03:22 +02:00
|
|
|
el.classList.remove('hidden');
|
|
|
|
if (_.isFunction(callback)) {
|
|
|
|
callback();
|
2017-06-14 19:18:14 +02:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (_.includes(el.classList, 'hidden')) {
|
|
|
|
/* XXX: This doesn't appear to be working...
|
|
|
|
el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnd, el, callback), false);
|
|
|
|
el.addEventListener("animationend", _.partial(afterAnimationEnd, el, callback), false);
|
|
|
|
*/
|
|
|
|
setTimeout(_.partial(afterAnimationEnd, el, callback), 351);
|
|
|
|
el.classList.add('visible');
|
|
|
|
el.classList.remove('hidden');
|
|
|
|
} else {
|
|
|
|
afterAnimationEnd(el, callback);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isSameBareJID = function (jid1, jid2) {
|
2017-07-15 11:03:22 +02:00
|
|
|
return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
|
|
|
|
Strophe.getBareJidFromJid(jid2).toLowerCase();
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isNewMessage = function (message) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Given a stanza, determine whether it's a new
|
|
|
|
* message, i.e. not a MAM archived one.
|
|
|
|
*/
|
|
|
|
if (message instanceof Element) {
|
|
|
|
return !(sizzle('result[xmlns="'+Strophe.NS.MAM+'"]', message).length);
|
|
|
|
} else {
|
|
|
|
return !message.get('archive_id');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isOTRMessage = function (message) {
|
2017-07-15 11:03:22 +02:00
|
|
|
var body = message.querySelector('body'),
|
|
|
|
text = (!_.isNull(body) ? body.textContent: undefined);
|
|
|
|
return text && !!text.match(/^\?OTR/);
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isHeadlineMessage = function (message) {
|
2017-07-15 11:03:22 +02:00
|
|
|
var from_jid = message.getAttribute('from');
|
|
|
|
if (message.getAttribute('type') === 'headline') {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (message.getAttribute('type') !== 'error' &&
|
|
|
|
!_.isNil(from_jid) &&
|
|
|
|
!_.includes(from_jid, '@')) {
|
|
|
|
// Some servers (I'm looking at you Prosody) don't set the message
|
|
|
|
// type to "headline" when sending server messages. For now we
|
|
|
|
// check if an @ signal is included, and if not, we assume it's
|
|
|
|
// a headline message.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.merge = function merge (first, second) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Merge the second object into the first one.
|
|
|
|
*/
|
|
|
|
for (var k in second) {
|
|
|
|
if (_.isObject(first[k])) {
|
|
|
|
merge(first[k], second[k]);
|
|
|
|
} else {
|
|
|
|
first[k] = second[k];
|
2017-02-25 09:33:34 +01:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.applyUserSettings = function applyUserSettings (context, settings, user_settings) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Configuration settings might be nested objects. We only want to
|
|
|
|
* add settings which are whitelisted.
|
|
|
|
*/
|
|
|
|
for (var k in settings) {
|
|
|
|
if (_.isUndefined(user_settings[k])) {
|
|
|
|
continue;
|
2016-03-28 12:49:52 +02:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
|
|
|
|
applyUserSettings(context[k], settings[k], user_settings[k]);
|
|
|
|
} else {
|
|
|
|
context[k] = user_settings[k];
|
2016-10-27 13:30:58 +02:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.refreshWebkit = function () {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* This works around a webkit bug. Refreshes the browser's viewport,
|
|
|
|
* otherwise chatboxes are not moved along when one is closed.
|
|
|
|
*/
|
2017-08-15 16:36:43 +02:00
|
|
|
if (jQBrowser.webkit && window.requestAnimationFrame) {
|
2017-07-15 11:03:22 +02:00
|
|
|
window.requestAnimationFrame(function () {
|
|
|
|
var conversejs = document.getElementById('conversejs');
|
|
|
|
conversejs.style.display = 'none';
|
|
|
|
var tmp = conversejs.offsetHeight; // jshint ignore:line
|
|
|
|
conversejs.style.display = 'block';
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.stringToDOM = function (s) {
|
|
|
|
/* Converts an HTML string into a DOM element.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (String) s - The HTML string
|
|
|
|
*/
|
|
|
|
var div = document.createElement('div');
|
|
|
|
div.innerHTML = s;
|
|
|
|
return div.childNodes;
|
|
|
|
};
|
|
|
|
|
|
|
|
u.matchesSelector = function (el, selector) {
|
|
|
|
/* Checks whether the DOM element matches the given selector.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (DOMElement) el - The DOM element
|
|
|
|
* (String) selector - The selector
|
|
|
|
*/
|
|
|
|
return (
|
|
|
|
el.matches ||
|
|
|
|
el.matchesSelector ||
|
|
|
|
el.msMatchesSelector ||
|
|
|
|
el.mozMatchesSelector ||
|
|
|
|
el.webkitMatchesSelector ||
|
|
|
|
el.oMatchesSelector
|
|
|
|
).call(el, selector);
|
|
|
|
};
|
|
|
|
|
|
|
|
u.queryChildren = function (el, selector) {
|
|
|
|
/* Returns a list of children of the DOM element that match the
|
|
|
|
* selector.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (DOMElement) el - the DOM element
|
|
|
|
* (String) selector - the selector they should be matched
|
|
|
|
* against.
|
|
|
|
*/
|
|
|
|
return _.filter(el.children, _.partial(u.matchesSelector, _, selector));
|
|
|
|
};
|
|
|
|
|
|
|
|
u.webForm2xForm = function (field) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Takes an HTML DOM and turns it into an XForm field.
|
2017-08-15 21:23:30 +02:00
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (DOMElement) field - the field to convert
|
|
|
|
*/
|
|
|
|
let value;
|
|
|
|
if (field.getAttribute('type') === 'checkbox') {
|
|
|
|
value = field.checked && 1 || 0;
|
|
|
|
} else if (field.tagName == "textarea") {
|
|
|
|
value = _.filter(field.value.split('\n'), _.trim);
|
2017-07-15 11:03:22 +02:00
|
|
|
} else {
|
2017-08-15 21:23:30 +02:00
|
|
|
value = field.value;
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
2017-08-15 21:23:30 +02:00
|
|
|
return u.stringToDOM(
|
|
|
|
tpl_field({
|
|
|
|
name: field.getAttribute('name'),
|
|
|
|
value: value
|
|
|
|
})
|
|
|
|
)[0];
|
2017-07-15 11:03:22 +02:00
|
|
|
};
|
2016-02-23 08:13:30 +01:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.contains = function (attr, query) {
|
2017-07-15 11:03:22 +02:00
|
|
|
return function (item) {
|
|
|
|
if (typeof attr === 'object') {
|
|
|
|
var value = false;
|
|
|
|
_.forEach(attr, function (a) {
|
|
|
|
value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
|
|
|
|
});
|
|
|
|
return value;
|
|
|
|
} else if (typeof attr === 'string') {
|
|
|
|
return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
|
2014-11-17 09:44:42 +01:00
|
|
|
} else {
|
2017-07-15 11:03:22 +02:00
|
|
|
throw new TypeError('contains: wrong attribute type. Must be string or array.');
|
2014-11-17 09:44:42 +01:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
};
|
|
|
|
};
|
2014-11-24 20:35:00 +01:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.xForm2webForm = function (field, stanza) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Takes a field in XMPP XForm (XEP-004: Data Forms) format
|
2017-08-15 21:23:30 +02:00
|
|
|
* and turns it into an HTML field.
|
|
|
|
*
|
|
|
|
* Returns either text or a DOM element (which is not ideal, but fine
|
|
|
|
* for now).
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (XMLElement) field - the field to convert
|
|
|
|
*/
|
|
|
|
if (field.getAttribute('type') === 'list-single' ||
|
|
|
|
field.getAttribute('type') === 'list-multi') {
|
|
|
|
|
|
|
|
const values = _.map(
|
|
|
|
u.queryChildren(field, 'value'),
|
|
|
|
_.partial(_.get, _, 'textContent')
|
|
|
|
);
|
|
|
|
const options = _.map(
|
|
|
|
u.queryChildren(field, 'option'),
|
|
|
|
function (option) {
|
|
|
|
const value = _.get(option.querySelector('value'), 'textContent');
|
|
|
|
return tpl_select_option({
|
|
|
|
'value': value,
|
|
|
|
'label': option.getAttribute('label'),
|
|
|
|
'selected': _.startsWith(values, value),
|
|
|
|
'required': _.isNil(field.querySelector('required'))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
);
|
2017-07-15 11:03:22 +02:00
|
|
|
return tpl_form_select({
|
2017-08-15 21:23:30 +02:00
|
|
|
'name': field.getAttribute('var'),
|
|
|
|
'label': field.getAttribute('label'),
|
|
|
|
'options': options.join(''),
|
|
|
|
'multiple': (field.getAttribute('type') === 'list-multi'),
|
|
|
|
'required': _.isNil(field.querySelector('required'))
|
|
|
|
})
|
|
|
|
} else if (field.getAttribute('type') === 'fixed') {
|
|
|
|
const text = _.get(field.querySelector('value'), 'textContent');
|
|
|
|
const el = u.stringToDOM('<p class="form-help">');
|
|
|
|
el.textContent = text;
|
|
|
|
return el;
|
|
|
|
} else if (field.getAttribute('type') === 'jid-multi') {
|
2017-07-15 11:03:22 +02:00
|
|
|
return tpl_form_textarea({
|
2017-08-15 21:23:30 +02:00
|
|
|
'name': field.getAttribute('var'),
|
|
|
|
'label': field.getAttribute('label') || '',
|
|
|
|
'value': _.get(field.querySelector('value'), 'textContent'),
|
|
|
|
'required': _.isNil(field.querySelector('required'))
|
|
|
|
})
|
|
|
|
} else if (field.getAttribute('type') === 'boolean') {
|
2017-07-15 11:03:22 +02:00
|
|
|
return tpl_form_checkbox({
|
2017-08-15 21:23:30 +02:00
|
|
|
'name': field.getAttribute('var'),
|
|
|
|
'type': XFORM_TYPE_MAP[field.getAttribute('type')],
|
|
|
|
'label': field.getAttribute('label') || '',
|
|
|
|
'checked': _.get(field.querySelector('value'), 'textContent') === "1" && 'checked="1"' || '',
|
|
|
|
'required': _.isNil(field.querySelector('required'))
|
|
|
|
})
|
|
|
|
} else if (field.getAttribute('type') && field.getAttribute('var') === 'username') {
|
2017-07-15 11:03:22 +02:00
|
|
|
return tpl_form_username({
|
2017-08-15 21:23:30 +02:00
|
|
|
'domain': ' @'+this.domain,
|
|
|
|
'name': field.getAttribute('var'),
|
|
|
|
'type': XFORM_TYPE_MAP[field.getAttribute('type')],
|
|
|
|
'label': field.getAttribute('label') || '',
|
|
|
|
'value': _.get(field.querySelector('value'), 'textContent'),
|
|
|
|
'required': _.isNil(field.querySelector('required'))
|
|
|
|
})
|
|
|
|
} else if (field.getAttribute('type')) {
|
2017-07-15 11:03:22 +02:00
|
|
|
return tpl_form_input({
|
2017-08-15 21:23:30 +02:00
|
|
|
'name': field.getAttribute('var'),
|
|
|
|
'type': XFORM_TYPE_MAP[field.getAttribute('type')],
|
|
|
|
'label': field.getAttribute('label') || '',
|
|
|
|
'value': _.get(field.querySelector('value'), 'textContent'),
|
|
|
|
'required': _.isNil(field.querySelector('required'))
|
|
|
|
})
|
2017-07-15 11:03:22 +02:00
|
|
|
} else {
|
2017-08-15 21:23:30 +02:00
|
|
|
if (field.getAttribute('var') === 'ocr') { // Captcha
|
|
|
|
const uri = field.querySelector('uri');
|
|
|
|
const el = sizzle('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]', stanza)[0];
|
|
|
|
return tpl_form_captcha({
|
|
|
|
'label': field.getAttribute('label'),
|
|
|
|
'name': field.getAttribute('var'),
|
|
|
|
'data': _.get(el, 'textContent'),
|
|
|
|
'type': uri.getAttribute('type'),
|
|
|
|
'required': _.isNil(field.querySelector('required'))
|
|
|
|
})
|
2014-11-16 12:47:30 +01:00
|
|
|
}
|
2014-10-13 22:02:55 +02:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
2016-02-19 10:36:01 +01:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.detectLocale = function (library_check) {
|
2017-04-19 14:18:30 +02:00
|
|
|
/* 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) {
|
2017-08-15 21:23:30 +02:00
|
|
|
locale = u.isLocaleAvailable(window.navigator.userLanguage, library_check);
|
2017-04-19 14:18:30 +02:00
|
|
|
}
|
|
|
|
if (window.navigator.languages && !locale) {
|
|
|
|
for (i=0; i<window.navigator.languages.length && !locale; i++) {
|
2017-08-15 21:23:30 +02:00
|
|
|
locale = u.isLocaleAvailable(window.navigator.languages[i], library_check);
|
2017-04-19 14:18:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (window.navigator.browserLanguage && !locale) {
|
2017-08-15 21:23:30 +02:00
|
|
|
locale = u.isLocaleAvailable(window.navigator.browserLanguage, library_check);
|
2017-04-19 14:18:30 +02:00
|
|
|
}
|
|
|
|
if (window.navigator.language && !locale) {
|
2017-08-15 21:23:30 +02:00
|
|
|
locale = u.isLocaleAvailable(window.navigator.language, library_check);
|
2017-04-19 14:18:30 +02:00
|
|
|
}
|
|
|
|
if (window.navigator.systemLanguage && !locale) {
|
2017-08-15 21:23:30 +02:00
|
|
|
locale = u.isLocaleAvailable(window.navigator.systemLanguage, library_check);
|
2017-04-19 14:18:30 +02:00
|
|
|
}
|
|
|
|
return locale || 'en';
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isConverseLocale = function (locale) {
|
2017-04-19 14:18:30 +02:00
|
|
|
if (!_.isString(locale)) { return false; }
|
2017-06-12 20:30:58 +02:00
|
|
|
return _.includes(_.keys(locales || {}), locale);
|
2017-04-19 14:18:30 +02:00
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isMomentLocale = function (locale) {
|
2017-04-19 14:18:30 +02:00
|
|
|
if (!_.isString(locale)) { return false; }
|
|
|
|
return moment.locale() !== moment.locale(locale);
|
2017-06-12 20:30:58 +02:00
|
|
|
};
|
2017-04-19 14:18:30 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.getLocale = function (preferred_locale, isSupportedByLibrary) {
|
2017-04-23 19:18:00 +02:00
|
|
|
if (_.isString(preferred_locale)) {
|
|
|
|
if (preferred_locale === 'en' || isSupportedByLibrary(preferred_locale)) {
|
|
|
|
return preferred_locale;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
var obj = window.JSON.parse(preferred_locale);
|
|
|
|
return obj.locale_data.converse[""].lang;
|
|
|
|
} catch (e) {
|
|
|
|
console.log(e);
|
|
|
|
}
|
2017-04-19 14:18:30 +02:00
|
|
|
}
|
2017-08-15 21:23:30 +02:00
|
|
|
return u.detectLocale(isSupportedByLibrary) || 'en';
|
2017-04-19 14:18:30 +02:00
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isOfType = function (type, item) {
|
2017-06-09 23:43:58 +02:00
|
|
|
return item.get('type') == type;
|
2017-06-12 20:30:58 +02:00
|
|
|
};
|
2017-06-09 23:43:58 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isInstance = function (type, item) {
|
2017-06-07 00:55:25 +02:00
|
|
|
return item instanceof type;
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.getAttribute = function (key, item) {
|
2017-06-07 00:55:25 +02:00
|
|
|
return item.get(key);
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.contains.not = function (attr, query) {
|
2016-02-19 10:36:01 +01:00
|
|
|
return function (item) {
|
2017-08-15 21:23:30 +02:00
|
|
|
return !(u.contains(attr, query)(item));
|
2016-02-19 10:36:01 +01:00
|
|
|
};
|
|
|
|
};
|
2017-03-31 03:54:42 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.createFragmentFromText = function (markup) {
|
2017-07-15 16:00:18 +02:00
|
|
|
/* Returns a DocumentFragment containing DOM nodes based on the
|
|
|
|
* passed-in markup text.
|
|
|
|
*/
|
2017-03-31 03:54:42 +02:00
|
|
|
// http://stackoverflow.com/questions/9334645/create-node-from-markup-string
|
|
|
|
var frag = document.createDocumentFragment(),
|
|
|
|
tmp = document.createElement('body'), child;
|
2017-07-15 16:00:18 +02:00
|
|
|
tmp.innerHTML = markup;
|
|
|
|
// Append elements in a loop to a DocumentFragment, so that the
|
|
|
|
// browser does not re-render the document for each node.
|
2017-03-31 03:54:42 +02:00
|
|
|
while (child = tmp.firstChild) { // eslint-disable-line no-cond-assign
|
|
|
|
frag.appendChild(child);
|
|
|
|
}
|
2017-07-15 16:00:18 +02:00
|
|
|
return frag
|
2017-06-12 20:30:58 +02:00
|
|
|
};
|
2017-03-31 03:54:42 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.addEmoji = function (_converse, emojione, text) {
|
2017-07-15 19:17:15 +02:00
|
|
|
if (_converse.use_emojione) {
|
|
|
|
return emojione.toImage(text);
|
|
|
|
} else {
|
|
|
|
return emojione.shortnameToUnicode(text);
|
|
|
|
}
|
2017-06-17 00:49:25 +02:00
|
|
|
}
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.getEmojisByCategory = function (_converse, emojione) {
|
2017-06-16 11:31:57 +02:00
|
|
|
/* Return a dict of emojis with the categories as keys and
|
|
|
|
* lists of emojis in that category as values.
|
|
|
|
*/
|
2017-07-16 12:14:56 +02:00
|
|
|
if (_.isUndefined(_converse.emojis_by_category)) {
|
2017-07-15 07:58:30 +02:00
|
|
|
const emojis = _.values(_.mapValues(emojione.emojioneList, function (value, key, o) {
|
2017-06-16 11:31:57 +02:00
|
|
|
value._shortname = key;
|
|
|
|
return value
|
|
|
|
}));
|
2017-07-15 07:58:30 +02:00
|
|
|
const tones = [':tone1:', ':tone2:', ':tone3:', ':tone4:', ':tone5:'];
|
|
|
|
const excluded = [':kiss_ww:', ':kiss_mm:', ':kiss_woman_man:'];
|
|
|
|
const excluded_substrings = [
|
|
|
|
':woman', ':man', ':women_', ':men_', '_man_', '_woman_', '_woman:', '_man:'
|
|
|
|
];
|
2017-07-15 11:51:15 +02:00
|
|
|
const excluded_categories = ['modifier', 'regional'];
|
2017-07-15 07:58:30 +02:00
|
|
|
const categories = _.difference(
|
|
|
|
_.uniq(_.map(emojis, _.partial(_.get, _, 'category'))),
|
|
|
|
excluded_categories
|
|
|
|
);
|
|
|
|
const emojis_by_category = {};
|
|
|
|
_.forEach(categories, (cat) => {
|
|
|
|
let list = _.sortBy(_.filter(emojis, ['category', cat]), ['uc_base']);
|
|
|
|
list = _.filter(
|
|
|
|
list,
|
|
|
|
(item) => !_.includes(_.concat(tones, excluded), item._shortname) &&
|
|
|
|
!_.some(excluded_substrings, _.partial(_.includes, item._shortname))
|
|
|
|
);
|
2017-06-16 11:31:57 +02:00
|
|
|
if (cat === 'people') {
|
2017-07-15 07:58:30 +02:00
|
|
|
const idx = _.findIndex(list, ['uc_base', '1f600']);
|
2017-06-16 11:31:57 +02:00
|
|
|
list = _.union(_.slice(list, idx), _.slice(list, 0, idx+1));
|
|
|
|
} else if (cat === 'activity') {
|
|
|
|
list = _.union(_.slice(list, 27-1), _.slice(list, 0, 27));
|
|
|
|
} else if (cat === 'objects') {
|
|
|
|
list = _.union(_.slice(list, 24-1), _.slice(list, 0, 24));
|
|
|
|
} else if (cat === 'travel') {
|
|
|
|
list = _.union(_.slice(list, 17-1), _.slice(list, 0, 17));
|
2017-07-15 11:51:15 +02:00
|
|
|
} else if (cat === 'symbols') {
|
|
|
|
list = _.union(_.slice(list, 60-1), _.slice(list, 0, 60));
|
2017-06-16 11:31:57 +02:00
|
|
|
}
|
|
|
|
emojis_by_category[cat] = list;
|
|
|
|
});
|
2017-07-16 12:14:56 +02:00
|
|
|
_converse.emojis_by_category = emojis_by_category;
|
2017-06-16 11:31:57 +02:00
|
|
|
}
|
2017-07-16 12:14:56 +02:00
|
|
|
return _converse.emojis_by_category;
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.getTonedEmojis = function (_converse) {
|
2017-07-16 12:14:56 +02:00
|
|
|
_converse.toned_emojis = _.uniq(
|
|
|
|
_.map(
|
|
|
|
_.filter(
|
2017-08-15 21:23:30 +02:00
|
|
|
u.getEmojisByCategory(_converse).people,
|
2017-07-16 12:14:56 +02:00
|
|
|
(person) => _.includes(person._shortname, '_tone')
|
|
|
|
),
|
|
|
|
(person) => person._shortname.replace(/_tone[1-5]/, '')
|
|
|
|
));
|
|
|
|
return _converse.toned_emojis;
|
|
|
|
};
|
2017-06-24 11:00:44 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isPersistableModel = function (model) {
|
2017-06-19 11:08:57 +02:00
|
|
|
return model.collection && model.collection.browserStorage;
|
2017-07-10 21:14:48 +02:00
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.getWrappedPromise = function () {
|
2017-07-10 21:14:48 +02:00
|
|
|
const wrapper = {};
|
|
|
|
wrapper.promise = new Promise((resolve, reject) => {
|
|
|
|
wrapper.resolve = resolve;
|
|
|
|
wrapper.reject = reject;
|
|
|
|
})
|
|
|
|
return wrapper;
|
|
|
|
};
|
2017-06-19 11:08:57 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.safeSave = function (model, attributes) {
|
|
|
|
if (u.isPersistableModel(model)) {
|
2017-06-23 20:16:16 +02:00
|
|
|
model.save(attributes);
|
2017-06-19 11:08:57 +02:00
|
|
|
} else {
|
2017-06-23 20:16:16 +02:00
|
|
|
model.set(attributes);
|
2017-06-19 11:08:57 +02:00
|
|
|
}
|
|
|
|
}
|
2017-08-15 21:23:30 +02:00
|
|
|
return u;
|
2015-01-16 22:07:27 +01:00
|
|
|
}));
|