xmpp.chapril.org-conversejs/src/utils.js

638 lines
24 KiB
JavaScript
Raw Normal View History

/*global define, escape, locales, Jed */
2015-01-16 22:07:27 +01:00
(function (root, factory) {
define([
"jquery.noconflict",
2017-06-14 18:41:45 +02:00
"sizzle",
2017-07-10 21:14:48 +02:00
"es6-promise",
"jquery.browser",
"lodash.noconflict",
"locales",
"moment_with_locales",
2017-06-14 18:41:45 +02:00
"strophe",
"tpl!field",
"tpl!select_option",
"tpl!form_select",
"tpl!form_textarea",
"tpl!form_checkbox",
"tpl!form_username",
"tpl!form_input",
"tpl!form_captcha"
], factory);
}(this, function (
2017-07-10 21:14:48 +02:00
$, sizzle,
Promise,
dummy, _,
locales,
moment,
2017-06-14 18:41:45 +02:00
Strophe,
tpl_field,
tpl_select_option,
tpl_form_select,
tpl_form_textarea,
tpl_form_checkbox,
tpl_form_username,
tpl_form_input,
tpl_form_captcha
) {
"use strict";
locales = locales || {};
2017-06-14 18:41:45 +02:00
Strophe = Strophe.Strophe;
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'
};
var afterAnimationEnd = function (el, callback) {
el.classList.remove('visible');
if (_.isFunction(callback)) {
callback();
}
};
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;
};
2016-05-28 13:13:49 +02:00
var isImage = function (url) {
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
};
$.fn.hasScrollBar = function() {
if (!$.contains(document, this.get(0))) {
return false;
}
if(this.parent().height() < this.get(0).scrollHeight) {
return true;
}
return false;
};
var throttledHTML = _.throttle(function (el, html) {
el.innerHTML = html;
}, 500);
2016-05-28 13:13:49 +02:00
$.fn.addHyperlinks = function () {
if (this.length > 0) {
this.each(function (i, obj) {
2016-05-28 13:28:32 +02:00
var prot, escaped_url;
var x = obj.innerHTML;
2016-05-28 13:28:32 +02:00
var list = x.match(/\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<]{2,200}\b/g );
if (list) {
for (i=0; i<list.length; i++) {
prot = list[i].indexOf('http://') === 0 || list[i].indexOf('https://') === 0 ? '' : 'http://';
escaped_url = encodeURI(decodeURI(list[i])).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
x = x.replace(list[i], '<a target="_blank" rel="noopener" href="' + prot + escaped_url + '">'+ list[i] + '</a>' );
}
}
obj.innerHTML = x;
_.forEach(list, function (url) {
isImage(unescapeHTML(url)).then(function (img) {
img.className = 'chat-image';
2017-06-16 11:31:57 +02:00
var a = obj.querySelector('a');
if (!_.isNull(a)) {
throttledHTML(a, img.outerHTML);
}
2016-05-28 13:28:32 +02:00
});
2016-05-28 13:13:49 +02:00
});
});
}
return this;
};
var utils = {
// Translation machinery
// ---------------------
__: function (str) {
if (!utils.isConverseLocale(this.locale) || this.locale === 'en') {
return Jed.sprintf.apply(Jed, arguments);
}
2015-01-16 22:07:27 +01:00
if (typeof this.jed === "undefined") {
this.jed = new Jed(window.JSON.parse(locales[this.locale]));
2015-01-16 22:07:27 +01:00
}
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;
},
2016-07-18 13:41:31 +02:00
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;
}
}
},
2017-06-16 11:31:57 +02:00
hideElement: function (el) {
el.classList.add('hidden');
},
toggleElement: function (el) {
if (_.includes(el.classList, 'hidden')) {
// XXX: use fadeIn?
el.classList.remove('hidden');
} else {
this.hideElement (el);
}
},
slideDown: function (el, interval=0.6) {
return new Promise((resolve, reject) => {
if (_.isNil(el)) {
const err = "Undefined or null element passed into slideDown"
console.warn(err);
reject(new Error(err));
}
let intval = el.getAttribute('data-slider-intval');
if (intval) {
window.clearInterval(intval);
}
let h = 0;
const end_height = el.getAttribute('data-slider-height');
intval = window.setInterval(function () {
h++;
el.style.height = h + 'px';
if (h >= end_height) {
window.clearInterval(intval);
el.style.height = '';
el.style.overflow = '';
el.removeAttribute('data-slider-intval');
el.removeAttribute('data-slider-height');
resolve();
}
}, interval);
el.setAttribute('data-slider-intval', intval);
});
},
slideUp: function (el, interval=0.6) {
return new Promise((resolve, reject) => {
if (_.isNil(el)) {
const err = "Undefined or null element passed into slideUp";
console.warn(err);
reject(new Error(err));
}
let intval = el.getAttribute('data-slider-intval');
if (intval) {
window.clearInterval(intval);
}
let h = el.offsetHeight;
el.setAttribute('data-slider-height', h);
el.style.overflow = 'hidden';
intval = window.setInterval(function () {
el.style.height = h + 'px';
h--;
if (h < 0) {
window.clearInterval(intval);
el.removeAttribute('data-slider-intval');
resolve();
}
}, interval);
el.setAttribute('data-slider-intval', intval);
});
},
fadeIn: function (el, callback) {
if (_.isNil(el)) {
console.warn("Undefined or null element passed into fadeIn");
}
if ($.fx.off) {
el.classList.remove('hidden');
if (_.isFunction(callback)) {
callback();
}
return;
}
if (_.includes(el.classList, 'hidden')) {
/* XXX: This doesn't appear to be working...
el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnd, el, callback), false);
el.addEventListener("animationend", _.partial(afterAnimationEnd, el, callback), false);
*/
setTimeout(_.partial(afterAnimationEnd, el, callback), 351);
el.classList.add('visible');
el.classList.remove('hidden');
} else {
afterAnimationEnd(el, callback);
}
2016-07-18 13:41:31 +02:00
},
isSameBareJID: function (jid1, jid2) {
return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
Strophe.getBareJidFromJid(jid2).toLowerCase();
},
isNewMessage: function (message) {
2017-06-14 18:41:45 +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-06-14 18:41:45 +02:00
},
isOTRMessage: function (message) {
2017-02-24 16:10:22 +01:00
var body = message.querySelector('body'),
text = (!_.isNull(body) ? body.textContent: undefined);
return text && !!text.match(/^\?OTR/);
},
isHeadlineMessage: function (message) {
2017-02-24 16:10:22 +01:00
var from_jid = message.getAttribute('from');
2017-02-25 09:33:34 +01:00
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;
},
merge: function merge (first, second) {
/* Merge the second object into the first one.
*/
for (var k in second) {
if (_.isObject(first[k])) {
merge(first[k], second[k]);
} else {
first[k] = second[k];
}
}
},
applyUserSettings: function applyUserSettings (context, settings, user_settings) {
/* Configuration settings might be nested objects. We only want to
* add settings which are whitelisted.
*/
for (var k in settings) {
if (_.isUndefined(user_settings[k])) {
continue;
}
if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
applyUserSettings(context[k], settings[k], user_settings[k]);
} else {
context[k] = user_settings[k];
}
}
},
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 && window.requestAnimationFrame) {
window.requestAnimationFrame(function () {
var conversejs = document.getElementById('conversejs');
conversejs.style.display = 'none';
var tmp = conversejs.offsetHeight; // jshint ignore:line
conversejs.style.display = 'block';
});
}
},
webForm2xForm: function (field) {
/* Takes an HTML DOM and turns it into an XForm field.
2015-01-16 22:07:27 +01:00
*
* 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<lines.length; vk++) {
var val = $.trim(lines[vk]);
if (val === '')
continue;
value.push(val);
}
} else {
value = $input.val();
}
return $(tpl_field({
name: $input.attr('name'),
value: value
}))[0];
},
contains: function (attr, query) {
return function (item) {
if (typeof attr === 'object') {
var value = false;
_.forEach(attr, function (a) {
value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
});
return value;
} else if (typeof attr === 'string') {
return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
} else {
throw new TypeError('contains: wrong attribute type. Must be string or array.');
}
};
},
2014-11-24 20:35:00 +01:00
xForm2webForm: function ($field, $stanza) {
/* Takes a field in XMPP XForm (XEP-004: Data Forms) format
2015-01-16 22:07:27 +01:00
* and turns it into a HTML DOM field.
*
* Parameters:
* (XMLElement) field - the field to convert
*/
2014-11-24 20:35:00 +01:00
// FIXME: take <required> into consideration
var options = [], j, $options, $values, value, values;
2015-10-25 18:49:35 +01:00
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(tpl_select_option({
value: value,
label: $($options[j]).attr('label'),
selected: _.startsWith(values, value),
2014-11-24 20:35:00 +01:00
required: $field.find('required').length
}));
}
return tpl_form_select({
name: $field.attr('var'),
label: $field.attr('label'),
options: options.join(''),
2015-10-25 18:49:35 +01:00
multiple: ($field.attr('type') === 'list-multi'),
2014-11-24 20:35:00 +01:00
required: $field.find('required').length
});
2015-10-25 18:49:35 +01:00
} else if ($field.attr('type') === 'fixed') {
2014-11-24 20:35:00 +01:00
return $('<p class="form-help">').text($field.find('value').text());
2015-10-25 18:49:35 +01:00
} else if ($field.attr('type') === 'jid-multi') {
return tpl_form_textarea({
name: $field.attr('var'),
label: $field.attr('label') || '',
2014-11-24 20:35:00 +01:00
value: $field.find('value').text(),
required: $field.find('required').length
});
2015-10-25 18:49:35 +01:00
} else if ($field.attr('type') === 'boolean') {
return tpl_form_checkbox({
name: $field.attr('var'),
type: XFORM_TYPE_MAP[$field.attr('type')],
label: $field.attr('label') || '',
2014-11-24 20:35:00 +01:00
checked: $field.find('value').text() === "1" && 'checked="1"' || '',
required: $field.find('required').length
});
} else if ($field.attr('type') && $field.attr('var') === 'username') {
return tpl_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
});
2014-11-24 20:35:00 +01:00
} else if ($field.attr('type')) {
return tpl_form_input({
name: $field.attr('var'),
type: XFORM_TYPE_MAP[$field.attr('type')],
label: $field.attr('label') || '',
2014-11-24 20:35:00 +01:00
value: $field.find('value').text(),
required: $field.find('required').length
});
2014-11-24 20:35:00 +01:00
} else {
if ($field.attr('var') === 'ocr') { // Captcha
return _.reduce(_.map($field.find('uri'),
$.proxy(function (uri) {
return tpl_form_captcha({
2014-11-24 20:35:00 +01:00
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.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 = utils.isLocaleAvailable(window.navigator.userLanguage, library_check);
}
if (window.navigator.languages && !locale) {
for (i=0; i<window.navigator.languages.length && !locale; i++) {
locale = utils.isLocaleAvailable(window.navigator.languages[i], library_check);
}
}
if (window.navigator.browserLanguage && !locale) {
locale = utils.isLocaleAvailable(window.navigator.browserLanguage, library_check);
}
if (window.navigator.language && !locale) {
locale = utils.isLocaleAvailable(window.navigator.language, library_check);
}
if (window.navigator.systemLanguage && !locale) {
locale = utils.isLocaleAvailable(window.navigator.systemLanguage, library_check);
}
return locale || 'en';
};
utils.isConverseLocale = function (locale) {
if (!_.isString(locale)) { return false; }
return _.includes(_.keys(locales || {}), locale);
};
utils.isMomentLocale = function (locale) {
if (!_.isString(locale)) { return false; }
return moment.locale() !== moment.locale(locale);
};
utils.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);
}
}
return utils.detectLocale(isSupportedByLibrary) || 'en';
};
utils.isOfType = function (type, item) {
return item.get('type') == type;
};
utils.isInstance = function (type, item) {
return item instanceof type;
};
utils.getAttribute = function (key, item) {
return item.get(key);
};
utils.contains.not = function (attr, query) {
return function (item) {
return !(utils.contains(attr, query)(item));
};
};
utils.createElementsFromString = function (element, html) {
// http://stackoverflow.com/questions/9334645/create-node-from-markup-string
var frag = document.createDocumentFragment(),
tmp = document.createElement('body'), child;
tmp.innerHTML = html;
// Append elements in a loop to a DocumentFragment, so that the browser does
// not re-render the document for each node
while (child = tmp.firstChild) { // eslint-disable-line no-cond-assign
frag.appendChild(child);
}
element.appendChild(frag); // Now, append all elements at once
frag = tmp = null;
};
utils.addEmoji = function (_converse, emojione, text) {
return emojione.shortnameToUnicode(text);
}
2017-06-16 11:31:57 +02:00
utils.marshallEmojis = function (emojione) {
/* Return a dict of emojis with the categories as keys and
* lists of emojis in that category as values.
*/
if (_.isUndefined(this.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:'
];
const excluded_categories = ['modifier'];
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));
}
emojis_by_category[cat] = list;
});
this.emojis_by_category = emojis_by_category;
}
return this.emojis_by_category;
}
2017-06-24 11:00:44 +02:00
utils.isPersistableModel = function (model) {
return model.collection && model.collection.browserStorage;
2017-07-10 21:14:48 +02:00
};
utils.getWrappedPromise = function () {
const wrapper = {};
wrapper.promise = new Promise((resolve, reject) => {
wrapper.resolve = resolve;
wrapper.reject = reject;
})
return wrapper;
};
utils.safeSave = function (model, attributes) {
if (utils.isPersistableModel(model)) {
model.save(attributes);
} else {
model.set(attributes);
}
}
return utils;
2015-01-16 22:07:27 +01:00
}));