2020-01-26 16:21:20 +01:00
|
|
|
/**
|
2021-09-09 11:19:34 +02:00
|
|
|
* @copyright The Converse.js contributors
|
2020-01-26 16:21:20 +01:00
|
|
|
* @license Mozilla Public License (MPLv2)
|
|
|
|
* @description This is the core utilities module.
|
|
|
|
*/
|
2021-07-15 18:45:16 +02:00
|
|
|
import DOMPurify from 'dompurify';
|
|
|
|
import _converse from '@converse/headless/shared/_converse.js';
|
2021-04-21 10:27:27 +02:00
|
|
|
import compact from "lodash-es/compact";
|
|
|
|
import isElement from "lodash-es/isElement";
|
|
|
|
import isObject from "lodash-es/isObject";
|
|
|
|
import last from "lodash-es/last";
|
2021-07-15 18:45:16 +02:00
|
|
|
import log from '@converse/headless/log.js';
|
2018-10-23 03:41:38 +02:00
|
|
|
import sizzle from "sizzle";
|
2021-04-21 10:27:27 +02:00
|
|
|
import { Model } from '@converse/skeletor/src/model.js';
|
2021-07-15 18:45:16 +02:00
|
|
|
import { Strophe } from 'strophe.js/src/strophe.js';
|
2021-04-28 18:35:08 +02:00
|
|
|
import { getOpenPromise } from '@converse/openpromise';
|
2021-12-17 20:54:10 +01:00
|
|
|
import { setUserJID, } from '@converse/headless/utils/init.js';
|
2021-09-29 11:24:38 +02:00
|
|
|
import { settings_api } from '@converse/headless/shared/settings/api.js';
|
2018-10-23 03:41:38 +02:00
|
|
|
|
2021-09-09 11:19:34 +02:00
|
|
|
export function isEmptyMessage (attrs) {
|
|
|
|
if (attrs instanceof Model) {
|
|
|
|
attrs = attrs.attributes;
|
|
|
|
}
|
|
|
|
return !attrs['oob_url'] &&
|
|
|
|
!attrs['file'] &&
|
|
|
|
!(attrs['is_encrypted'] && attrs['plaintext']) &&
|
|
|
|
!attrs['message'];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-09-29 11:24:38 +02:00
|
|
|
/* We distinguish between UniView and MultiView instances.
|
|
|
|
*
|
|
|
|
* UniView means that only one chat is visible, even though there might be multiple ongoing chats.
|
|
|
|
* MultiView means that multiple chats may be visible simultaneously.
|
|
|
|
*/
|
|
|
|
export function isUniView () {
|
|
|
|
return ['mobile', 'fullscreen', 'embedded'].includes(settings_api.get("view_mode"));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* The utils object
|
|
|
|
* @namespace u
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
const u = {};
|
|
|
|
|
2019-07-11 23:41:59 +02:00
|
|
|
|
2018-05-29 12:00:23 +02:00
|
|
|
u.isTagEqual = function (stanza, name) {
|
|
|
|
if (stanza.nodeTree) {
|
|
|
|
return u.isTagEqual(stanza.nodeTree, name);
|
|
|
|
} else if (!(stanza instanceof Element)) {
|
|
|
|
throw Error(
|
|
|
|
"isTagEqual called with value which isn't "+
|
|
|
|
"an element or Strophe.Builder instance");
|
|
|
|
} else {
|
|
|
|
return Strophe.isTagEqual(stanza, name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-02 11:12:46 +02:00
|
|
|
const parser = new DOMParser();
|
|
|
|
const parserErrorNS = parser.parseFromString('invalid', 'text/xml')
|
|
|
|
.getElementsByTagName("parsererror")[0].namespaceURI;
|
|
|
|
|
2020-10-07 15:31:13 +02:00
|
|
|
u.getJIDFromURI = function (jid) {
|
|
|
|
return jid.startsWith('xmpp:') && jid.endsWith('?join')
|
|
|
|
? jid.replace(/^xmpp:/, '').replace(/\?join$/, '')
|
|
|
|
: jid;
|
|
|
|
}
|
|
|
|
|
2019-02-12 14:21:45 +01:00
|
|
|
u.toStanza = function (string) {
|
2019-06-02 11:12:46 +02:00
|
|
|
const node = parser.parseFromString(string, "text/xml");
|
|
|
|
if (node.getElementsByTagNameNS(parserErrorNS, 'parsererror').length) {
|
|
|
|
throw new Error(`Parser Error: ${string}`);
|
|
|
|
}
|
|
|
|
return node.firstElementChild;
|
2019-02-12 14:21:45 +01:00
|
|
|
}
|
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
u.getLongestSubstring = function (string, candidates) {
|
|
|
|
function reducer (accumulator, current_value) {
|
|
|
|
if (string.startsWith(current_value)) {
|
|
|
|
if (current_value.length > accumulator.length) {
|
|
|
|
return current_value;
|
2018-08-15 17:22:24 +02:00
|
|
|
} else {
|
|
|
|
return accumulator;
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
} else {
|
|
|
|
return accumulator;
|
2018-08-15 17:22:24 +02:00
|
|
|
}
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
return candidates.reduce(reducer, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
u.prefixMentions = function (message) {
|
|
|
|
/* Given a message object, return its text with @ chars
|
|
|
|
* inserted before the mentioned nicknames.
|
|
|
|
*/
|
|
|
|
let text = message.get('message');
|
|
|
|
(message.get('references') || [])
|
|
|
|
.sort((a, b) => b.begin - a.begin)
|
|
|
|
.forEach(ref => {
|
|
|
|
text = `${text.slice(0, ref.begin)}@${text.slice(ref.begin)}`
|
|
|
|
});
|
|
|
|
return text;
|
|
|
|
};
|
|
|
|
|
|
|
|
u.isValidJID = function (jid) {
|
2020-10-01 11:13:13 +02:00
|
|
|
if (typeof jid === 'string') {
|
2020-03-06 15:19:48 +01:00
|
|
|
return compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
|
2019-06-03 10:20:52 +02:00
|
|
|
}
|
|
|
|
return false;
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
u.isValidMUCJID = function (jid) {
|
|
|
|
return !jid.startsWith('@') && !jid.endsWith('@');
|
|
|
|
};
|
|
|
|
|
|
|
|
u.isSameBareJID = function (jid1, jid2) {
|
2020-10-01 11:13:13 +02:00
|
|
|
if (typeof jid1 !== 'string' || typeof jid2 !== 'string') {
|
2019-06-05 10:01:55 +02:00
|
|
|
return false;
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
|
|
|
|
Strophe.getBareJidFromJid(jid2).toLowerCase();
|
|
|
|
};
|
|
|
|
|
2018-11-18 19:14:22 +01:00
|
|
|
|
|
|
|
u.isSameDomain = function (jid1, jid2) {
|
2020-10-01 11:13:13 +02:00
|
|
|
if (typeof jid1 !== 'string' || typeof jid2 !== 'string') {
|
2018-11-18 19:14:22 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return Strophe.getDomainFromJid(jid1).toLowerCase() ===
|
|
|
|
Strophe.getDomainFromJid(jid2).toLowerCase();
|
|
|
|
};
|
|
|
|
|
2018-10-23 03:41:38 +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 &&
|
|
|
|
sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, message).length
|
|
|
|
);
|
2019-09-19 16:54:55 +02:00
|
|
|
} else if (message instanceof Model) {
|
2019-01-31 16:11:17 +01:00
|
|
|
message = message.attributes;
|
2017-12-02 14:12:17 +01:00
|
|
|
}
|
2019-01-31 16:11:17 +01:00
|
|
|
return !(message['is_delayed'] && message['is_archived']);
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
2017-12-02 14:12:17 +01:00
|
|
|
|
2020-02-13 15:12:03 +01:00
|
|
|
u.shouldCreateMessage = function (attrs) {
|
2020-03-18 19:32:03 +01:00
|
|
|
return attrs['retracted'] || // Retraction received *before* the message
|
2021-09-09 11:19:34 +02:00
|
|
|
!isEmptyMessage(attrs);
|
2020-02-13 15:12:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
u.shouldCreateGroupchatMessage = function (attrs) {
|
|
|
|
return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone);
|
2020-02-10 11:23:55 +01:00
|
|
|
}
|
2020-02-13 15:12:03 +01:00
|
|
|
|
2019-10-13 14:52:43 +02:00
|
|
|
u.isChatRoom = function (model) {
|
|
|
|
return model && (model.get('type') === 'chatroom');
|
|
|
|
}
|
|
|
|
|
2019-07-04 14:12:12 +02:00
|
|
|
u.isErrorObject = function (o) {
|
|
|
|
return o instanceof Error;
|
|
|
|
}
|
|
|
|
|
2019-08-10 12:26:07 +02:00
|
|
|
u.isErrorStanza = function (stanza) {
|
2020-03-06 15:19:48 +01:00
|
|
|
if (!isElement(stanza)) {
|
2019-08-10 12:26:07 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return stanza.getAttribute('type') === 'error';
|
|
|
|
}
|
2019-08-08 15:12:18 +02:00
|
|
|
|
|
|
|
u.isForbiddenError = function (stanza) {
|
2020-03-06 15:19:48 +01:00
|
|
|
if (!isElement(stanza)) {
|
2019-08-08 15:12:18 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return sizzle(`error[type="auth"] forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0;
|
|
|
|
}
|
|
|
|
|
2019-07-29 14:32:57 +02:00
|
|
|
u.isServiceUnavailableError = function (stanza) {
|
2020-03-06 15:19:48 +01:00
|
|
|
if (!isElement(stanza)) {
|
2019-07-29 14:32:57 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return sizzle(`error[type="cancel"] service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0;
|
|
|
|
}
|
|
|
|
|
2020-03-30 16:29:09 +02:00
|
|
|
/**
|
|
|
|
* Merge the second object into the first one.
|
|
|
|
* @private
|
2020-09-25 21:20:29 +02:00
|
|
|
* @method u#merge
|
2020-03-30 16:29:09 +02:00
|
|
|
* @param { Object } first
|
|
|
|
* @param { Object } second
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
u.merge = function merge (first, second) {
|
2020-03-30 16:29:09 +02:00
|
|
|
for (const k in second) {
|
2020-03-06 15:19:48 +01:00
|
|
|
if (isObject(first[k])) {
|
2018-10-23 03:41:38 +02:00
|
|
|
merge(first[k], second[k]);
|
|
|
|
} else {
|
|
|
|
first[k] = second[k];
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
u.getOuterWidth = function (el, include_margin=false) {
|
2019-07-12 12:41:21 +02:00
|
|
|
let width = el.offsetWidth;
|
2018-10-23 03:41:38 +02:00
|
|
|
if (!include_margin) {
|
2018-01-03 14:58:05 +01:00
|
|
|
return width;
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-07-12 12:41:21 +02:00
|
|
|
const style = window.getComputedStyle(el);
|
|
|
|
width += parseInt(style.marginLeft ? style.marginLeft : 0, 10) +
|
|
|
|
parseInt(style.marginRight ? style.marginRight : 0, 10);
|
2018-10-23 03:41:38 +02:00
|
|
|
return width;
|
|
|
|
};
|
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Converts an HTML string into a DOM element.
|
|
|
|
* Expects that the HTML string has only one top-level element,
|
|
|
|
* i.e. not multiple ones.
|
|
|
|
* @private
|
|
|
|
* @method u#stringToElement
|
|
|
|
* @param { String } s - The HTML string
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
u.stringToElement = function (s) {
|
|
|
|
var div = document.createElement('div');
|
|
|
|
div.innerHTML = s;
|
|
|
|
return div.firstElementChild;
|
|
|
|
};
|
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Checks whether the DOM element matches the given selector.
|
|
|
|
* @private
|
|
|
|
* @method u#matchesSelector
|
|
|
|
* @param { DOMElement } el - The DOM element
|
|
|
|
* @param { String } selector - The selector
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
u.matchesSelector = function (el, selector) {
|
2018-12-17 14:38:00 +01:00
|
|
|
const match = (
|
2018-10-23 03:41:38 +02:00
|
|
|
el.matches ||
|
|
|
|
el.matchesSelector ||
|
|
|
|
el.msMatchesSelector ||
|
|
|
|
el.mozMatchesSelector ||
|
|
|
|
el.webkitMatchesSelector ||
|
|
|
|
el.oMatchesSelector
|
2018-12-17 14:38:00 +01:00
|
|
|
);
|
|
|
|
return match ? match.call(el, selector) : false;
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Returns a list of children of the DOM element that match the selector.
|
|
|
|
* @private
|
|
|
|
* @method u#queryChildren
|
|
|
|
* @param { DOMElement } el - the DOM element
|
|
|
|
* @param { String } selector - the selector they should be matched against
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
u.queryChildren = function (el, selector) {
|
2019-09-07 22:00:28 +02:00
|
|
|
return Array.from(el.childNodes).filter(el => u.matchesSelector(el, selector));
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
u.contains = function (attr, query) {
|
2020-03-06 15:19:48 +01:00
|
|
|
const checker = (item, key) => item.get(key).toLowerCase().includes(query.toLowerCase());
|
2018-10-23 03:41:38 +02:00
|
|
|
return function (item) {
|
|
|
|
if (typeof attr === 'object') {
|
2020-03-06 15:19:48 +01:00
|
|
|
return Object.keys(attr).reduce((acc, k) => acc || checker(item, k), false);
|
2018-10-23 03:41:38 +02:00
|
|
|
} else if (typeof attr === 'string') {
|
2020-03-06 15:19:48 +01:00
|
|
|
return checker(item, attr);
|
2017-06-19 11:08:57 +02:00
|
|
|
} else {
|
2018-10-23 03:41:38 +02:00
|
|
|
throw new TypeError('contains: wrong attribute type. Must be string or array.');
|
2018-02-19 10:35:42 +01:00
|
|
|
}
|
2017-11-10 22:50:39 +01:00
|
|
|
};
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
2017-11-10 22:50:39 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
u.isOfType = function (type, item) {
|
|
|
|
return item.get('type') == type;
|
|
|
|
};
|
2018-03-31 18:29:01 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
u.isInstance = function (type, item) {
|
|
|
|
return item instanceof type;
|
|
|
|
};
|
2018-03-31 18:29:01 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
u.getAttribute = function (key, item) {
|
|
|
|
return item.get(key);
|
|
|
|
};
|
2018-05-12 16:45:40 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
u.contains.not = function (attr, query) {
|
|
|
|
return function (item) {
|
|
|
|
return !(u.contains(attr, query)(item));
|
2018-05-12 16:45:40 +02:00
|
|
|
};
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
2018-05-12 16:45:40 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
u.rootContains = function (root, el) {
|
|
|
|
// The document element does not have the contains method in IE.
|
|
|
|
if (root === document && !root.contains) {
|
|
|
|
return document.head.contains(el) || document.body.contains(el);
|
|
|
|
}
|
|
|
|
return root.contains ? root.contains(el) : window.HTMLElement.prototype.contains.call(root, el);
|
|
|
|
};
|
|
|
|
|
|
|
|
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
|
|
|
|
};
|
|
|
|
|
|
|
|
u.isPersistableModel = function (model) {
|
|
|
|
return model.collection && model.collection.browserStorage;
|
|
|
|
};
|
|
|
|
|
2021-04-28 18:35:08 +02:00
|
|
|
u.getResolveablePromise = getOpenPromise;
|
|
|
|
u.getOpenPromise = getOpenPromise;
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
u.interpolate = function (string, o) {
|
|
|
|
return string.replace(/{{{([^{}]*)}}}/g,
|
|
|
|
(a, b) => {
|
|
|
|
var r = o[b];
|
|
|
|
return typeof r === 'string' || typeof r === 'number' ? r : a;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Call the callback once all the events have been triggered
|
|
|
|
* @private
|
|
|
|
* @method u#onMultipleEvents
|
|
|
|
* @param { Array } events: An array of objects, with keys `object` and
|
2019-09-07 22:00:28 +02:00
|
|
|
* `event`, representing the event name and the object it's triggered upon.
|
2019-03-29 23:47:56 +01:00
|
|
|
* @param { Function } callback: The function to call once all events have
|
|
|
|
* been triggered.
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
u.onMultipleEvents = function (events=[], callback) {
|
|
|
|
let triggered = [];
|
|
|
|
|
|
|
|
function handler (result) {
|
|
|
|
triggered.push(result)
|
|
|
|
if (events.length === triggered.length) {
|
|
|
|
callback(triggered);
|
|
|
|
triggered = [];
|
2018-07-22 16:12:36 +02:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-09-07 22:00:28 +02:00
|
|
|
events.forEach(e => e.object.on(e.event, handler));
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
2018-05-20 15:10:37 +02:00
|
|
|
|
2021-06-03 16:29:31 +02:00
|
|
|
|
|
|
|
export function safeSave (model, attributes, options) {
|
2018-10-23 03:41:38 +02:00
|
|
|
if (u.isPersistableModel(model)) {
|
2019-10-24 14:29:15 +02:00
|
|
|
model.save(attributes, options);
|
2018-10-23 03:41:38 +02:00
|
|
|
} else {
|
2019-10-24 14:29:15 +02:00
|
|
|
model.set(attributes, options);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2021-06-03 16:29:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
u.safeSave = safeSave;
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
u.siblingIndex = function (el) {
|
|
|
|
/* eslint-disable no-cond-assign */
|
|
|
|
for (var i = 0; el = el.previousElementSibling; i++);
|
|
|
|
return i;
|
|
|
|
};
|
|
|
|
|
2019-08-19 15:02:54 +02:00
|
|
|
/**
|
|
|
|
* Returns the current word being written in the input element
|
|
|
|
* @method u#getCurrentWord
|
|
|
|
* @param {HTMLElement} input - The HTMLElement in which text is being entered
|
|
|
|
* @param {integer} [index] - An optional rightmost boundary index. If given, the text
|
|
|
|
* value of the input element will only be considered up until this index.
|
|
|
|
* @param {string} [delineator] - An optional string delineator to
|
|
|
|
* differentiate between words.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
u.getCurrentWord = function (input, index, delineator) {
|
2019-03-04 10:21:04 +01:00
|
|
|
if (!index) {
|
|
|
|
index = input.selectionEnd || undefined;
|
|
|
|
}
|
2020-10-07 11:05:18 +02:00
|
|
|
let [word] = input.value.slice(0, index).split(/\s/).slice(-1);
|
2019-08-19 15:02:54 +02:00
|
|
|
if (delineator) {
|
|
|
|
[word] = word.split(delineator).slice(-1);
|
|
|
|
}
|
|
|
|
return word;
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
|
|
|
|
2020-11-27 14:47:51 +01:00
|
|
|
u.isMentionBoundary = (s) => s !== '@' && RegExp(`(\\p{Z}|\\p{P})`, 'u').test(s);
|
|
|
|
|
|
|
|
u.replaceCurrentWord = function (input, new_value) {
|
|
|
|
const caret = input.selectionEnd || undefined;
|
|
|
|
const current_word = last(input.value.slice(0, caret).split(/\s/));
|
|
|
|
const value = input.value;
|
|
|
|
const mention_boundary = u.isMentionBoundary(current_word[0]) ? current_word[0] : '';
|
2020-10-07 18:51:11 +02:00
|
|
|
input.value = value.slice(0, caret - current_word.length) + mention_boundary + `${new_value} ` + value.slice(caret);
|
|
|
|
const selection_end = caret - current_word.length + new_value.length + 1;
|
|
|
|
input.selectionEnd = mention_boundary ? selection_end + 1 : selection_end;
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true) {
|
|
|
|
const evt = document.createEvent(type);
|
|
|
|
evt.initEvent(name, bubbles, cancelable);
|
|
|
|
el.dispatchEvent(evt);
|
|
|
|
};
|
|
|
|
|
|
|
|
u.getSelectValues = function (select) {
|
|
|
|
const result = [];
|
|
|
|
const options = select && select.options;
|
|
|
|
for (var i=0, iLen=options.length; i<iLen; i++) {
|
|
|
|
const opt = options[i];
|
|
|
|
if (opt.selected) {
|
|
|
|
result.push(opt.value || opt.text);
|
2018-05-20 15:10:37 +02:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
u.getRandomInt = function (max) {
|
|
|
|
return Math.floor(Math.random() * Math.floor(max));
|
|
|
|
};
|
2018-08-07 09:45:55 +02:00
|
|
|
|
2019-05-26 11:20:31 +02:00
|
|
|
u.placeCaretAtEnd = function (textarea) {
|
2018-10-23 03:41:38 +02:00
|
|
|
if (textarea !== document.activeElement) {
|
|
|
|
textarea.focus();
|
|
|
|
}
|
|
|
|
// Double the length because Opera is inconsistent about whether a carriage return is one character or two.
|
|
|
|
const len = textarea.value.length * 2;
|
|
|
|
// Timeout seems to be required for Blink
|
|
|
|
setTimeout(() => textarea.setSelectionRange(len, len), 1);
|
|
|
|
// Scroll to the bottom, in case we're in a tall textarea
|
|
|
|
// (Necessary for Firefox and Chrome)
|
|
|
|
this.scrollTop = 999999;
|
|
|
|
};
|
|
|
|
|
2022-01-25 13:04:14 +01:00
|
|
|
export function getUniqueId (suffix) {
|
2019-10-11 15:32:38 +02:00
|
|
|
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
2020-06-03 10:56:34 +02:00
|
|
|
const r = Math.random() * 16 | 0;
|
|
|
|
const v = c === 'x' ? r : r & 0x3 | 0x8;
|
2018-10-23 03:41:38 +02:00
|
|
|
return v.toString(16);
|
|
|
|
});
|
2019-10-11 15:32:38 +02:00
|
|
|
if (typeof(suffix) === "string" || typeof(suffix) === "number") {
|
2020-06-03 10:56:34 +02:00
|
|
|
return uuid + ":" + suffix;
|
2019-10-11 15:32:38 +02:00
|
|
|
} else {
|
2020-06-03 10:56:34 +02:00
|
|
|
return uuid;
|
2019-10-11 15:32:38 +02:00
|
|
|
}
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
|
2021-09-29 11:24:38 +02:00
|
|
|
u.httpToGeoUri = function(text) {
|
2021-03-24 17:05:07 +01:00
|
|
|
const replacement = 'geo:$1,$2';
|
2021-09-29 11:24:38 +02:00
|
|
|
return text.replace(settings_api.get("geouri_regex"), replacement);
|
2021-03-24 17:05:07 +01:00
|
|
|
};
|
|
|
|
|
2019-07-11 22:50:30 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Clears the specified timeout and interval.
|
2019-08-19 15:02:54 +02:00
|
|
|
* @method u#clearTimers
|
2019-07-11 22:50:30 +02:00
|
|
|
* @param {number} timeout - Id if the timeout to clear.
|
|
|
|
* @param {number} interval - Id of the interval to clear.
|
|
|
|
* @private
|
|
|
|
* @copyright Simen Bekkhus 2016
|
|
|
|
* @license MIT
|
|
|
|
*/
|
|
|
|
function clearTimers(timeout, interval) {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
clearInterval(interval);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a {@link Promise} that resolves if the passed in function returns a truthy value.
|
|
|
|
* Rejects if it throws or does not return truthy within the given max_wait.
|
2019-08-19 15:02:54 +02:00
|
|
|
* @method u#waitUntil
|
2019-07-11 22:50:30 +02:00
|
|
|
* @param {Function} func - The function called every check_delay,
|
2019-08-19 15:02:54 +02:00
|
|
|
* and the result of which is the resolved value of the promise.
|
2019-07-11 22:50:30 +02:00
|
|
|
* @param {number} [max_wait=300] - The time to wait before rejecting the promise.
|
|
|
|
* @param {number} [check_delay=3] - The time to wait before each invocation of {func}.
|
|
|
|
* @returns {Promise} A promise resolved with the value of func,
|
2019-08-19 15:02:54 +02:00
|
|
|
* or rejected with the exception thrown by it or it times out.
|
2019-07-11 22:50:30 +02:00
|
|
|
* @copyright Simen Bekkhus 2016
|
|
|
|
* @license MIT
|
|
|
|
*/
|
|
|
|
u.waitUntil = function (func, max_wait=300, check_delay=3) {
|
|
|
|
// Run the function once without setting up any listeners in case it's already true
|
|
|
|
try {
|
|
|
|
const result = func();
|
|
|
|
if (result) {
|
|
|
|
return Promise.resolve(result);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
return Promise.reject(e);
|
|
|
|
}
|
|
|
|
|
2021-04-28 18:35:08 +02:00
|
|
|
const promise = getOpenPromise();
|
2020-06-05 15:37:32 +02:00
|
|
|
const timeout_err = new Error();
|
2019-07-11 22:50:30 +02:00
|
|
|
|
|
|
|
function checker () {
|
|
|
|
try {
|
|
|
|
const result = func();
|
|
|
|
if (result) {
|
|
|
|
clearTimers(max_wait_timeout, interval);
|
|
|
|
promise.resolve(result);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
clearTimers(max_wait_timeout, interval);
|
|
|
|
promise.reject(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const interval = setInterval(checker, check_delay);
|
2020-06-05 15:37:32 +02:00
|
|
|
|
|
|
|
function handler () {
|
2019-07-11 22:50:30 +02:00
|
|
|
clearTimers(max_wait_timeout, interval);
|
2020-06-05 15:37:32 +02:00
|
|
|
const err_msg = `Wait until promise timed out: \n\n${timeout_err.stack}`;
|
|
|
|
console.trace();
|
2019-11-06 11:01:34 +01:00
|
|
|
log.error(err_msg);
|
2019-07-11 23:41:59 +02:00
|
|
|
promise.reject(new Error(err_msg));
|
2020-06-05 15:37:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const max_wait_timeout = setTimeout(handler, max_wait);
|
2019-07-11 22:50:30 +02:00
|
|
|
|
|
|
|
return promise;
|
|
|
|
};
|
|
|
|
|
2021-07-15 18:45:16 +02:00
|
|
|
export function setUnloadEvent () {
|
|
|
|
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/
|
|
|
|
_converse.unloadevent = 'pagehide';
|
|
|
|
} else if ('onbeforeunload' in window) {
|
|
|
|
_converse.unloadevent = 'beforeunload';
|
|
|
|
} else if ('onunload' in window) {
|
|
|
|
_converse.unloadevent = 'unload';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getLoginCredentialsFromBrowser () {
|
|
|
|
try {
|
|
|
|
const creds = await navigator.credentials.get({'password': true});
|
|
|
|
if (creds && creds.type == 'password' && u.isValidJID(creds.id)) {
|
2021-12-17 20:54:10 +01:00
|
|
|
await setUserJID(creds.id);
|
2021-07-15 18:45:16 +02:00
|
|
|
return {'jid': creds.id, 'password': creds.password};
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
log.error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function replacePromise (name) {
|
|
|
|
const existing_promise = _converse.promises[name];
|
|
|
|
if (!existing_promise) {
|
|
|
|
throw new Error(`Tried to replace non-existing promise: ${name}`);
|
|
|
|
}
|
|
|
|
if (existing_promise.replace) {
|
|
|
|
const promise = getOpenPromise();
|
|
|
|
promise.replace = existing_promise.replace;
|
|
|
|
_converse.promises[name] = promise;
|
|
|
|
} else {
|
|
|
|
log.debug(`Not replacing promise "${name}"`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const element = document.createElement('div');
|
|
|
|
|
|
|
|
export function decodeHTMLEntities (str) {
|
|
|
|
if (str && typeof str === 'string') {
|
|
|
|
element.innerHTML = DOMPurify.sanitize(str);
|
|
|
|
str = element.textContent;
|
|
|
|
element.textContent = '';
|
|
|
|
}
|
|
|
|
return str;
|
|
|
|
}
|
|
|
|
|
2021-09-09 11:19:34 +02:00
|
|
|
export default Object.assign({
|
2022-01-25 13:04:14 +01:00
|
|
|
isEmptyMessage,
|
|
|
|
getUniqueId
|
2021-09-09 11:19:34 +02:00
|
|
|
}, u);
|