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

542 lines
19 KiB
JavaScript
Raw Normal View History

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)
//
/*global define, escape, window */
2015-01-16 22:07:27 +01:00
(function (root, factory) {
define([
2017-06-14 18:41:45 +02:00
"sizzle",
2017-07-10 21:14:48 +02:00
"es6-promise",
"jquery.browser",
"lodash.noconflict",
2017-06-14 18:41:45 +02:00
"strophe",
], factory);
}(this, function (
sizzle,
2017-07-10 21:14:48 +02:00
Promise,
jQBrowser,
_,
2017-08-16 15:19:41 +02:00
Strophe
) {
"use strict";
const b64_sha1 = Strophe.SHA1.b64_sha1;
2017-06-14 18:41:45 +02:00
Strophe = Strophe.Strophe;
const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b/g;
const logger = _.assign({
'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop,
'error': _.get(console, 'log') ? console.log.bind(console) : _.noop,
'info': _.get(console, 'log') ? console.log.bind(console) : _.noop,
'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
}, console);
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 '&amp;' 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
};
function calculateSlideStep (height) {
if (height > 100) {
return 10;
} else if (height > 50) {
return 5;
} else {
return 1;
}
}
function calculateElementHeight (el) {
/* Return the height of the passed in DOM element,
* based on the heights of its children.
*/
return _.reduce(
el.children,
(result, child) => result + child.offsetHeight, 0
);
}
function slideOutWrapup (el) {
/* Wrapup function for slideOut. */
el.removeAttribute('data-slider-marker');
el.classList.remove('collapsed');
el.style.overflow = "";
el.style.height = "";
}
2017-08-15 21:23:30 +02:00
var u = {};
2017-08-15 21:23:30 +02:00
u.addHyperlinks = function (text) {
const list = text.match(URL_REGEX) || [];
var links = [];
_.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");
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));
});
while (links.length) {
const a = links.pop();
text = text.replace(b64_sha1(a), a);
}
return text;
};
2017-08-15 21:23:30 +02:00
u.renderImageURLs = function (obj) {
const list = obj.textContent.match(URL_REGEX) || [];
_.forEach(list, function (url) {
isImage(url).then(function (img) {
img.className = 'chat-image';
var anchors = sizzle(`a[href="${url}"]`, obj);
_.each(anchors, (a) => { a.innerHTML = img.outerHTML; });
});
});
return obj;
};
2017-08-15 21:23:30 +02:00
u.slideInAllElements = function (elements) {
return Promise.all(
_.map(
elements,
2017-08-15 21:23:30 +02:00
_.partial(u.slideIn, _, 600)
));
};
2017-06-16 11:31:57 +02:00
2017-08-15 21:23:30 +02:00
u.slideToggleElement = function (el) {
if (_.includes(el.classList, 'collapsed')) {
2017-08-15 21:23:30 +02:00
return u.slideOut(el);
} else {
2017-08-15 21:23:30 +02:00
return u.slideIn(el);
}
};
2017-08-15 21:23:30 +02:00
u.slideOut = function (el, duration=900) {
/* Shows/expands an element by sliding it out of itself
*
* Parameters:
* (HTMLElement) el - The HTML string
* (Number) duration - The duration amount in milliseconds
*/
return new Promise((resolve, reject) => {
if (_.isNil(el)) {
const err = "Undefined or null element passed into slideOut"
logger.warn(err);
reject(new Error(err));
2017-07-15 15:15:37 +02:00
return;
2017-06-16 11:31:57 +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');
window.clearInterval(interval_marker);
}
const end_height = calculateElementHeight(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';
slideOutWrapup(el);
2017-07-15 15:15:37 +02:00
resolve();
return;
}
const step = calculateSlideStep(end_height),
interval = end_height/duration*step;
let h = 0;
interval_marker = window.setInterval(function () {
h += step;
if (h < end_height) {
el.style.height = h + 'px';
} else {
// We recalculate the height to work around an apparent
// browser bug where browsers don't know the correct
// offsetHeight beforehand.
el.style.height = calculateElementHeight(el) + 'px';
window.clearInterval(interval_marker);
slideOutWrapup(el);
resolve();
}
}, interval);
el.setAttribute('data-slider-marker', interval_marker);
});
};
2017-08-15 21:23:30 +02:00
u.slideIn = function (el, duration=600) {
/* Hides/collapses an element by sliding it into itself. */
return new Promise((resolve, reject) => {
if (_.isNil(el)) {
const err = "Undefined or null element passed into slideIn";
logger.warn(err);
2017-07-15 15:15:37 +02:00
return reject(new Error(err));
} 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();
}
let interval_marker = el.getAttribute('data-slider-marker');
if (interval_marker) {
2017-07-15 15:15:37 +02:00
el.removeAttribute('data-slider-marker');
window.clearInterval(interval_marker);
}
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);
el.classList.add('collapsed');
el.style.height = "";
resolve();
}
}, interval);
el.setAttribute('data-slider-marker', interval_marker);
});
};
2017-08-15 21:23:30 +02:00
u.fadeIn = function (el, callback) {
if (_.isNil(el)) {
logger.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)
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);
}
};
u.isValidJID = function (jid) {
return _.filter(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
};
2017-08-15 21:23:30 +02:00
u.isSameBareJID = function (jid1, jid2) {
return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
Strophe.getBareJidFromJid(jid2).toLowerCase();
};
2017-08-15 21:23:30 +02:00
u.isNewMessage = function (message) {
/* 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) {
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) {
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) {
/* 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-08-15 21:23:30 +02:00
u.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];
}
}
};
2017-08-15 21:23:30 +02:00
u.refreshWebkit = function () {
/* This works around a webkit bug. Refreshes the browser's viewport,
* otherwise chatboxes are not moved along when one is closed.
*/
if (jQBrowser.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';
});
}
};
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.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
2017-08-15 21:23:30 +02:00
u.isOfType = function (type, item) {
return item.get('type') == type;
};
2017-08-15 21:23:30 +02:00
u.isInstance = function (type, item) {
return item instanceof type;
};
2017-08-15 21:23:30 +02:00
u.getAttribute = function (key, item) {
return item.get(key);
};
2017-08-15 21:23:30 +02:00
u.contains.not = function (attr, query) {
return function (item) {
2017-08-15 21:23:30 +02:00
return !(u.contains(attr, query)(item));
};
};
2017-08-15 21:23:30 +02:00
u.createFragmentFromText = function (markup) {
/* Returns a DocumentFragment containing DOM nodes based on the
* passed-in markup text.
*/
// http://stackoverflow.com/questions/9334645/create-node-from-markup-string
var frag = document.createDocumentFragment(),
tmp = document.createElement('body'), child;
tmp.innerHTML = markup;
// 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);
}
return frag
};
2017-08-15 21:23:30 +02:00
u.addEmoji = function (_converse, emojione, text) {
if (_converse.use_emojione) {
return emojione.toImage(text);
} else {
return emojione.shortnameToUnicode(text);
}
}
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.
*/
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:'
];
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));
} 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;
});
_converse.emojis_by_category = emojis_by_category;
2017-06-16 11:31:57 +02:00
}
return _converse.emojis_by_category;
};
2017-08-15 21:23:30 +02:00
u.getTonedEmojis = function (_converse) {
_converse.toned_emojis = _.uniq(
_.map(
_.filter(
2017-08-15 21:23:30 +02:00
u.getEmojisByCategory(_converse).people,
(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) {
return model.collection && model.collection.browserStorage;
2017-07-10 21:14:48 +02:00
};
u.getResolveablePromise = function () {
/* Returns a promise object on which `resolve` or `reject` can be
* called.
*/
2017-07-10 21:14:48 +02:00
const wrapper = {};
const promise = new Promise((resolve, reject) => {
2017-07-10 21:14:48 +02:00
wrapper.resolve = resolve;
wrapper.reject = reject;
})
_.assign(promise, wrapper);
return promise;
2017-07-10 21:14:48 +02:00
};
2017-08-15 21:23:30 +02:00
u.safeSave = function (model, attributes) {
if (u.isPersistableModel(model)) {
model.save(attributes);
} else {
model.set(attributes);
}
}
u.isVisible = function (el) {
// XXX: Taken from jQuery's "visible" implementation
return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
};
2017-08-15 21:23:30 +02:00
return u;
2015-01-16 22:07:27 +01:00
}));