diff --git a/src/headless/plugins/emoji/index.js b/src/headless/plugins/emoji/index.js
index ec9c24dbe..2a7e5de37 100644
--- a/src/headless/plugins/emoji/index.js
+++ b/src/headless/plugins/emoji/index.js
@@ -3,13 +3,10 @@
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
-import { ASCII_REPLACE_REGEX, CODEPOINTS_REGEX } from './regexes.js';
import { Model } from '@converse/skeletor/src/model.js';
import { _converse, api, converse } from "../../core.js";
import { getOpenPromise } from '@converse/openpromise';
-import { html } from 'lit';
-const u = converse.env.utils;
converse.emojis = {
'initialized': false,
@@ -17,202 +14,6 @@ converse.emojis = {
};
-const ASCII_LIST = {
- '*\\0/*':'1f646', '*\\O/*':'1f646', '-___-':'1f611', ':\'-)':'1f602', '\':-)':'1f605', '\':-D':'1f605', '>:-)':'1f606', '\':-(':'1f613',
- '>:-(':'1f620', ':\'-(':'1f622', 'O:-)':'1f607', '0:-3':'1f607', '0:-)':'1f607', '0;^)':'1f607', 'O;-)':'1f607', '0;-)':'1f607', 'O:-3':'1f607',
- '-__-':'1f611', ':-Þ':'1f61b', '3':'1f494', ':\')':'1f602', ':-D':'1f603', '\':)':'1f605', '\'=)':'1f605', '\':D':'1f605', '\'=D':'1f605',
- '>:)':'1f606', '>;)':'1f606', '>=)':'1f606', ';-)':'1f609', '*-)':'1f609', ';-]':'1f609', ';^)':'1f609', '\':(':'1f613', '\'=(':'1f613',
- ':-*':'1f618', ':^*':'1f618', '>:P':'1f61c', 'X-P':'1f61c', '>:[':'1f61e', ':-(':'1f61e', ':-[':'1f61e', '>:(':'1f620', ':\'(':'1f622',
- ';-(':'1f622', '>.<':'1f623', '#-)':'1f635', '%-)':'1f635', 'X-)':'1f635', '\\0/':'1f646', '\\O/':'1f646', '0:3':'1f607', '0:)':'1f607',
- 'O:)':'1f607', 'O=)':'1f607', 'O:3':'1f607', 'B-)':'1f60e', '8-)':'1f60e', 'B-D':'1f60e', '8-D':'1f60e', '-_-':'1f611', '>:\\':'1f615',
- '>:/':'1f615', ':-/':'1f615', ':-.':'1f615', ':-P':'1f61b', ':Þ':'1f61b', ':-b':'1f61b', ':-O':'1f62e', 'O_O':'1f62e', '>:O':'1f62e',
- ':-X':'1f636', ':-#':'1f636', ':-)':'1f642', '(y)':'1f44d', '<3':'2764', ':D':'1f603', '=D':'1f603', ';)':'1f609', '*)':'1f609',
- ';]':'1f609', ';D':'1f609', ':*':'1f618', '=*':'1f618', ':(':'1f61e', ':[':'1f61e', '=(':'1f61e', ':@':'1f620', ';(':'1f622', 'D:':'1f628',
- ':$':'1f633', '=$':'1f633', '#)':'1f635', '%)':'1f635', 'X)':'1f635', 'B)':'1f60e', '8)':'1f60e', ':/':'1f615', ':\\':'1f615', '=/':'1f615',
- '=\\':'1f615', ':L':'1f615', '=L':'1f615', ':P':'1f61b', '=P':'1f61b', ':b':'1f61b', ':O':'1f62e', ':X':'1f636', ':#':'1f636', '=X':'1f636',
- '=#':'1f636', ':)':'1f642', '=]':'1f642', '=)':'1f642', ':]':'1f642'
-};
-
-function toCodePoint(unicode_surrogates) {
- const r = [];
- let p = 0;
- let i = 0;
- while (i < unicode_surrogates.length) {
- const c = unicode_surrogates.charCodeAt(i++);
- if (p) {
- r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16));
- p = 0;
- } else if (0xD800 <= c && c <= 0xDBFF) {
- p = c;
- } else {
- r.push(c.toString(16));
- }
- }
- return r.join('-');
-}
-
-
-function fromCodePoint (codepoint) {
- let code = typeof codepoint === 'string' ? parseInt(codepoint, 16) : codepoint;
- if (code < 0x10000) {
- return String.fromCharCode(code);
- }
- code -= 0x10000;
- return String.fromCharCode(
- 0xD800 + (code >> 10),
- 0xDC00 + (code & 0x3FF)
- );
-}
-
-
-function convert (unicode) {
- // Converts unicode code points and code pairs to their respective characters
- if (unicode.indexOf("-") > -1) {
- const parts = [],
- s = unicode.split('-');
- for (let i = 0; i < s.length; i++) {
- let part = parseInt(s[i], 16);
- if (part >= 0x10000 && part <= 0x10FFFF) {
- const hi = Math.floor((part - 0x10000) / 0x400) + 0xD800;
- const lo = ((part - 0x10000) % 0x400) + 0xDC00;
- part = (String.fromCharCode(hi) + String.fromCharCode(lo));
- } else {
- part = String.fromCharCode(part);
- }
- parts.push(part);
- }
- return parts.join('');
- }
- return fromCodePoint(unicode);
-}
-
-function unique (arr) {
- return [...new Set(arr)];
-}
-
-function getTonedEmojis () {
- if (!converse.emojis.toned) {
- converse.emojis.toned = unique(
- Object.values(converse.emojis.json.people)
- .filter(person => person.sn.includes('_tone'))
- .map(person => person.sn.replace(/_tone[1-5]/, ''))
- );
- }
- return converse.emojis.toned;
-}
-
-
-export function convertASCII2Emoji (str) {
- // Replace ASCII smileys
- return str.replace(ASCII_REPLACE_REGEX, (entire, m1, m2, m3) => {
- if( (typeof m3 === 'undefined') || (m3 === '') || (!(u.unescapeHTML(m3) in ASCII_LIST)) ) {
- // if the ascii doesnt exist just return the entire match
- return entire;
- }
- m3 = u.unescapeHTML(m3);
- const unicode = ASCII_LIST[m3].toUpperCase();
- return m2+convert(unicode);
- });
-}
-
-
-export function getEmojiMarkup (data, options={unicode_only: false, add_title_wrapper: false}) {
- const emoji = data.emoji;
- const shortname = data.shortname;
- if (emoji) {
- if (options.unicode_only) {
- return emoji;
- } else if (api.settings.get('use_system_emojis')) {
- if (options.add_title_wrapper) {
- return shortname ? html`${emoji}` : emoji;
- } else {
- return emoji;
- }
- } else {
- const path = api.settings.get('emoji_image_path');
- return html`
`;
- }
- } else if (options.unicode_only) {
- return shortname;
- } else {
- return html`
`;
- }
-}
-
-
-export function getShortnameReferences (text) {
- if (!converse.emojis.initialized) {
- throw new Error(
- 'getShortnameReferences called before emojis are initialized. '+
- 'To avoid this problem, first await the converse.emojis.initilaized_promise.'
- );
- }
- const references = [...text.matchAll(converse.emojis.shortnames_regex)].filter(ref => ref[0].length > 0);
- return references.map(ref => {
- const cp = converse.emojis.by_sn[ref[0]].cp;
- return {
- cp,
- 'begin': ref.index,
- 'end': ref.index+ref[0].length,
- 'shortname': ref[0],
- 'emoji': cp ? convert(cp) : null
- }
- });
-}
-
-
-function parseStringForEmojis(str, callback) {
- const UFE0Fg = /\uFE0F/g;
- const U200D = String.fromCharCode(0x200D);
- return String(str).replace(CODEPOINTS_REGEX, (emoji, _, offset) => {
- const icon_id = toCodePoint(emoji.indexOf(U200D) < 0 ? emoji.replace(UFE0Fg, '') : emoji);
- if (icon_id) callback(icon_id, emoji, offset);
- });
-}
-
-
-export function getCodePointReferences (text) {
- const references = [];
- parseStringForEmojis(text, (icon_id, emoji, offset) => {
- references.push({
- 'begin': offset,
- 'cp': icon_id,
- 'emoji': emoji,
- 'end': offset + emoji.length,
- 'shortname': u.getEmojisByAtrribute('cp')[icon_id]?.sn || ''
- });
- });
- return references;
-}
-
-
-function addEmojisMarkup (text, options) {
- let list = [text];
- [...getShortnameReferences(text), ...getCodePointReferences(text)]
- .sort((a, b) => b.begin - a.begin)
- .forEach(ref => {
- const text = list.shift();
- const emoji = getEmojiMarkup(ref, options);
- if (typeof emoji === 'string') {
- list = [text.slice(0, ref.begin) + emoji + text.slice(ref.end), ...list];
- } else {
- list = [text.slice(0, ref.begin), emoji, text.slice(ref.end), ...list];
- }
- });
- return list;
-}
-
-
converse.plugins.add('converse-emoji', {
initialize () {
@@ -255,7 +56,6 @@ converse.plugins.add('converse-emoji', {
}
});
-
/**
* Model for storing data related to the Emoji picker widget
* @class
@@ -270,98 +70,6 @@ converse.plugins.add('converse-emoji', {
}
});
- /************************ BEGIN Utils ************************/
- // Closured cache
- const emojis_by_attribute = {};
-
- Object.assign(u, {
- /**
- * Returns an emoji represented by the passed in shortname.
- * Scans the passed in text for shortnames and replaces them with
- * emoji unicode glyphs or alternatively if it's a custom emoji
- * without unicode representation then a lit TemplateResult
- * which represents image tag markup is returned.
- *
- * The shortname needs to be defined in `emojis.json`
- * and needs to have either a `cp` attribute for the codepoint, or
- * an `url` attribute which points to the source for the image.
- *
- * @method u.shortnamesToEmojis
- * @param { String } str - String containg the shortname(s)
- * @param { Object } options
- * @param { Boolean } options.unicode_only - Whether emojis are rendered as
- * unicode codepoints. If so, the returned result will be an array
- * with containing one string, because the emojis themselves will
- * also be strings. If set to false, emojis will be represented by
- * lit TemplateResult objects.
- * @param { Boolean } options.add_title_wrapper - Whether unicode
- * codepoints should be wrapped with a `` element with a
- * title, so that the shortname is shown upon hovering with the
- * mouse.
- * @returns {Array} An array of at least one string, or otherwise
- * strings and lit TemplateResult objects.
- */
- shortnamesToEmojis (str, options={unicode_only: false, add_title_wrapper: false}) {
- str = convertASCII2Emoji(str);
- return addEmojisMarkup(str, options);
- },
-
- /**
- * Replaces all shortnames in the passed in string with their
- * unicode (emoji) representation.
- * @method u.shortnamesToUnicode
- * @param { String } str - String containing the shortname(s)
- * @returns { String }
- */
- shortnamesToUnicode (str) {
- return u.shortnamesToEmojis(str, {'unicode_only': true})[0];
- },
-
- /**
- * Determines whether the passed in string is just a single emoji shortname;
- * @method u.isOnlyEmojis
- * @param { String } shortname - A string which migh be just an emoji shortname
- * @returns { Boolean }
- */
- isOnlyEmojis (text) {
- const words = text.trim().split(/\s+/);
- if (words.length === 0 || words.length > 3) {
- return false;
- }
- const emojis = words.filter(text => {
- const refs = getCodePointReferences(u.shortnamesToUnicode(text));
- return refs.length === 1 && (text === refs[0]['shortname'] || text === refs[0]['emoji']);
- });
- return emojis.length === words.length;
- },
-
- /**
- * @method u.getEmojisByAtrribute
- * @param { String } attr - The attribute according to which the
- * returned map should be keyed.
- * @returns { Object } - Map of emojis with the passed in attribute values
- * as keys and a list of emojis for a particular category as values.
- */
- getEmojisByAtrribute (attr) {
- if (emojis_by_attribute[attr]) {
- return emojis_by_attribute[attr];
- }
- if (attr === 'category') {
- return converse.emojis.json;
- }
- const all_variants = converse.emojis.list
- .map(e => e[attr])
- .filter((c, i, arr) => arr.indexOf(c) == i);
-
- emojis_by_attribute[attr] = {};
- all_variants.forEach(v => (emojis_by_attribute[attr][v] = converse.emojis.list.find(i => i[attr] === v)));
- return emojis_by_attribute[attr];
- }
- });
- /************************ END Utils ************************/
-
-
- /************************ BEGIN API ************************/
// We extend the default converse.js API to add methods specific to MUC groupchats.
Object.assign(api, {
/**
@@ -385,7 +93,6 @@ converse.plugins.add('converse-emoji', {
converse.emojis.shortnames = converse.emojis.list.map(m => m.sn);
const getShortNames = () => converse.emojis.shortnames.map(s => s.replace(/[+]/g, "\\$&")).join('|');
converse.emojis.shortnames_regex = new RegExp(getShortNames(), "gi");
- converse.emojis.toned = getTonedEmojis();
converse.emojis.initialized_promise.resolve();
}
return converse.emojis.initialized_promise;
diff --git a/src/headless/plugins/emoji/utils.js b/src/headless/plugins/emoji/utils.js
new file mode 100644
index 000000000..dbbff30cd
--- /dev/null
+++ b/src/headless/plugins/emoji/utils.js
@@ -0,0 +1,210 @@
+import { ASCII_REPLACE_REGEX, CODEPOINTS_REGEX } from './regexes.js';
+import { converse } from "../../core.js";
+
+const { u } = converse.env;
+
+// Closured cache
+const emojis_by_attribute = {};
+
+
+const ASCII_LIST = {
+ '*\\0/*':'1f646', '*\\O/*':'1f646', '-___-':'1f611', ':\'-)':'1f602', '\':-)':'1f605', '\':-D':'1f605', '>:-)':'1f606', '\':-(':'1f613',
+ '>:-(':'1f620', ':\'-(':'1f622', 'O:-)':'1f607', '0:-3':'1f607', '0:-)':'1f607', '0;^)':'1f607', 'O;-)':'1f607', '0;-)':'1f607', 'O:-3':'1f607',
+ '-__-':'1f611', ':-Þ':'1f61b', '3':'1f494', ':\')':'1f602', ':-D':'1f603', '\':)':'1f605', '\'=)':'1f605', '\':D':'1f605', '\'=D':'1f605',
+ '>:)':'1f606', '>;)':'1f606', '>=)':'1f606', ';-)':'1f609', '*-)':'1f609', ';-]':'1f609', ';^)':'1f609', '\':(':'1f613', '\'=(':'1f613',
+ ':-*':'1f618', ':^*':'1f618', '>:P':'1f61c', 'X-P':'1f61c', '>:[':'1f61e', ':-(':'1f61e', ':-[':'1f61e', '>:(':'1f620', ':\'(':'1f622',
+ ';-(':'1f622', '>.<':'1f623', '#-)':'1f635', '%-)':'1f635', 'X-)':'1f635', '\\0/':'1f646', '\\O/':'1f646', '0:3':'1f607', '0:)':'1f607',
+ 'O:)':'1f607', 'O=)':'1f607', 'O:3':'1f607', 'B-)':'1f60e', '8-)':'1f60e', 'B-D':'1f60e', '8-D':'1f60e', '-_-':'1f611', '>:\\':'1f615',
+ '>:/':'1f615', ':-/':'1f615', ':-.':'1f615', ':-P':'1f61b', ':Þ':'1f61b', ':-b':'1f61b', ':-O':'1f62e', 'O_O':'1f62e', '>:O':'1f62e',
+ ':-X':'1f636', ':-#':'1f636', ':-)':'1f642', '(y)':'1f44d', '<3':'2764', ':D':'1f603', '=D':'1f603', ';)':'1f609', '*)':'1f609',
+ ';]':'1f609', ';D':'1f609', ':*':'1f618', '=*':'1f618', ':(':'1f61e', ':[':'1f61e', '=(':'1f61e', ':@':'1f620', ';(':'1f622', 'D:':'1f628',
+ ':$':'1f633', '=$':'1f633', '#)':'1f635', '%)':'1f635', 'X)':'1f635', 'B)':'1f60e', '8)':'1f60e', ':/':'1f615', ':\\':'1f615', '=/':'1f615',
+ '=\\':'1f615', ':L':'1f615', '=L':'1f615', ':P':'1f61b', '=P':'1f61b', ':b':'1f61b', ':O':'1f62e', ':X':'1f636', ':#':'1f636', '=X':'1f636',
+ '=#':'1f636', ':)':'1f642', '=]':'1f642', '=)':'1f642', ':]':'1f642'
+};
+
+
+function toCodePoint(unicode_surrogates) {
+ const r = [];
+ let p = 0;
+ let i = 0;
+ while (i < unicode_surrogates.length) {
+ const c = unicode_surrogates.charCodeAt(i++);
+ if (p) {
+ r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16));
+ p = 0;
+ } else if (0xD800 <= c && c <= 0xDBFF) {
+ p = c;
+ } else {
+ r.push(c.toString(16));
+ }
+ }
+ return r.join('-');
+}
+
+
+function fromCodePoint (codepoint) {
+ let code = typeof codepoint === 'string' ? parseInt(codepoint, 16) : codepoint;
+ if (code < 0x10000) {
+ return String.fromCharCode(code);
+ }
+ code -= 0x10000;
+ return String.fromCharCode(
+ 0xD800 + (code >> 10),
+ 0xDC00 + (code & 0x3FF)
+ );
+}
+
+
+function convert (unicode) {
+ // Converts unicode code points and code pairs to their respective characters
+ if (unicode.indexOf("-") > -1) {
+ const parts = [],
+ s = unicode.split('-');
+ for (let i = 0; i < s.length; i++) {
+ let part = parseInt(s[i], 16);
+ if (part >= 0x10000 && part <= 0x10FFFF) {
+ const hi = Math.floor((part - 0x10000) / 0x400) + 0xD800;
+ const lo = ((part - 0x10000) % 0x400) + 0xDC00;
+ part = (String.fromCharCode(hi) + String.fromCharCode(lo));
+ } else {
+ part = String.fromCharCode(part);
+ }
+ parts.push(part);
+ }
+ return parts.join('');
+ }
+ return fromCodePoint(unicode);
+}
+
+export function convertASCII2Emoji (str) {
+ // Replace ASCII smileys
+ return str.replace(ASCII_REPLACE_REGEX, (entire, _, m2, m3) => {
+ if( (typeof m3 === 'undefined') || (m3 === '') || (!(u.unescapeHTML(m3) in ASCII_LIST)) ) {
+ // if the ascii doesnt exist just return the entire match
+ return entire;
+ }
+ m3 = u.unescapeHTML(m3);
+ const unicode = ASCII_LIST[m3].toUpperCase();
+ return m2+convert(unicode);
+ });
+}
+
+export function getShortnameReferences (text) {
+ if (!converse.emojis.initialized) {
+ throw new Error(
+ 'getShortnameReferences called before emojis are initialized. '+
+ 'To avoid this problem, first await the converse.emojis.initilaized_promise.'
+ );
+ }
+ const references = [...text.matchAll(converse.emojis.shortnames_regex)].filter(ref => ref[0].length > 0);
+ return references.map(ref => {
+ const cp = converse.emojis.by_sn[ref[0]].cp;
+ return {
+ cp,
+ 'begin': ref.index,
+ 'end': ref.index+ref[0].length,
+ 'shortname': ref[0],
+ 'emoji': cp ? convert(cp) : null
+ }
+ });
+}
+
+
+function parseStringForEmojis(str, callback) {
+ const UFE0Fg = /\uFE0F/g;
+ const U200D = String.fromCharCode(0x200D);
+ return String(str).replace(CODEPOINTS_REGEX, (emoji, _, offset) => {
+ const icon_id = toCodePoint(emoji.indexOf(U200D) < 0 ? emoji.replace(UFE0Fg, '') : emoji);
+ if (icon_id) callback(icon_id, emoji, offset);
+ });
+}
+
+
+export function getCodePointReferences (text) {
+ const references = [];
+ parseStringForEmojis(text, (icon_id, emoji, offset) => {
+ references.push({
+ 'begin': offset,
+ 'cp': icon_id,
+ 'emoji': emoji,
+ 'end': offset + emoji.length,
+ 'shortname': getEmojisByAtrribute('cp')[icon_id]?.sn || ''
+ });
+ });
+ return references;
+}
+
+function addEmojisMarkup (text) {
+ let list = [text];
+ [...getShortnameReferences(text), ...getCodePointReferences(text)]
+ .sort((a, b) => b.begin - a.begin)
+ .forEach(ref => {
+ const text = list.shift();
+ const emoji = ref.emoji || ref.shortname;
+ list = [text.slice(0, ref.begin) + emoji + text.slice(ref.end), ...list];
+ });
+ return list;
+}
+
+/**
+ * Replaces all shortnames in the passed in string with their
+ * unicode (emoji) representation.
+ * @namespace u
+ * @method u.shortnamesToUnicode
+ * @param { String } str - String containing the shortname(s)
+ * @returns { String }
+ */
+function shortnamesToUnicode (str) {
+ return addEmojisMarkup(convertASCII2Emoji(str)).pop();
+}
+
+/**
+ * Determines whether the passed in string is just a single emoji shortname;
+ * @namespace u
+ * @method u.isOnlyEmojis
+ * @param { String } shortname - A string which migh be just an emoji shortname
+ * @returns { Boolean }
+ */
+function isOnlyEmojis (text) {
+ const words = text.trim().split(/\s+/);
+ if (words.length === 0 || words.length > 3) {
+ return false;
+ }
+ const emojis = words.filter(text => {
+ const refs = getCodePointReferences(u.shortnamesToUnicode(text));
+ return refs.length === 1 && (text === refs[0]['shortname'] || text === refs[0]['emoji']);
+ });
+ return emojis.length === words.length;
+}
+
+/**
+ * @namespace u
+ * @method u.getEmojisByAtrribute
+ * @param { 'category'|'cp'|'sn' } attr
+ * The attribute according to which the returned map should be keyed.
+ * @returns { Object }
+ * Map of emojis with the passed in `attr` used as key and a list of emojis as values.
+ */
+function getEmojisByAtrribute (attr) {
+ if (emojis_by_attribute[attr]) {
+ return emojis_by_attribute[attr];
+ }
+ if (attr === 'category') {
+ return converse.emojis.json;
+ }
+ const all_variants = converse.emojis.list
+ .map(e => e[attr])
+ .filter((c, i, arr) => arr.indexOf(c) == i);
+
+ emojis_by_attribute[attr] = {};
+ all_variants.forEach(v => (emojis_by_attribute[attr][v] = converse.emojis.list.find(i => i[attr] === v)));
+ return emojis_by_attribute[attr];
+}
+
+Object.assign(u, {
+ getEmojisByAtrribute,
+ isOnlyEmojis,
+ shortnamesToUnicode,
+});
+
diff --git a/src/plugins/chatview/tests/emojis.js b/src/plugins/chatview/tests/emojis.js
index ec2229b63..88d3ecd7a 100644
--- a/src/plugins/chatview/tests/emojis.js
+++ b/src/plugins/chatview/tests/emojis.js
@@ -133,7 +133,7 @@ describe("Emojis", function () {
const view = _converse.chatboxviews.get(contact_jid);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
await u.waitUntil(() => view.querySelector('.chat-msg__text').innerHTML.replace(//g, '') ===
- '
');
+ '
');
const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
let message = view.querySelector(last_msg_sel);
@@ -191,7 +191,7 @@ describe("Emojis", function () {
const picker = await u.waitUntil(() => view.querySelector('converse-emoji-picker'), 1000);
const custom_category = picker.querySelector('.pick-category[data-category="custom"]');
expect(custom_category.innerHTML.replace(//g, '').trim()).toBe(
- '
');
+ '
');
const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = 'Running tests for :converse:';
@@ -204,7 +204,7 @@ describe("Emojis", function () {
await new Promise(resolve => view.model.messages.once('rendered', resolve));
const body = view.querySelector('converse-chat-message-body');
await u.waitUntil(() => body.innerHTML.replace(//g, '').trim() ===
- 'Running tests for
');
+ 'Running tests for
');
}));
});
});
diff --git a/src/plugins/chatview/tests/http-file-upload.js b/src/plugins/chatview/tests/http-file-upload.js
index 4d3267ad7..a47fcbd73 100644
--- a/src/plugins/chatview/tests/http-file-upload.js
+++ b/src/plugins/chatview/tests/http-file-upload.js
@@ -273,7 +273,7 @@ describe("XEP-0363: HTTP File Upload", function () {
// Check that the image renders
expect(img_link_el.outerHTML.replace(//g, '').trim()).toEqual(
``+
- `
`);
+ `
`);
XMLHttpRequest.prototype.send = send_backup;
}));
diff --git a/src/plugins/chatview/tests/message-images.js b/src/plugins/chatview/tests/message-images.js
index c43cf3c16..517fd126a 100644
--- a/src/plugins/chatview/tests/message-images.js
+++ b/src/plugins/chatview/tests/message-images.js
@@ -18,7 +18,7 @@ describe("A Chat Message", function () {
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
expect(msg.innerHTML.replace(//g, '').trim()).toEqual(
``+
- `
`+
+ `
`+
``);
message += "?param1=val1¶m2=val2";
@@ -28,7 +28,7 @@ describe("A Chat Message", function () {
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
expect(msg.innerHTML.replace(//g, '').trim()).toEqual(
``+
- `
`+
+ `
`+
``);
// Test now with two images in one message
diff --git a/src/plugins/muc-views/tests/http-file-upload.js b/src/plugins/muc-views/tests/http-file-upload.js
index a0f67ee33..ddbc7eab1 100644
--- a/src/plugins/muc-views/tests/http-file-upload.js
+++ b/src/plugins/muc-views/tests/http-file-upload.js
@@ -139,7 +139,7 @@ describe("XEP-0363: HTTP File Upload", function () {
// Check that the image renders
expect(img_link_el.outerHTML.replace(//g, '').trim()).toEqual(
``+
- `
`);
+ `
`);
expect(view.querySelector('.chat-msg .chat-msg__media')).toBe(null);
XMLHttpRequest.prototype.send = send_backup;
diff --git a/src/shared/chat/emoji-picker-content.js b/src/shared/chat/emoji-picker-content.js
index aaccd63ca..b025c2328 100644
--- a/src/shared/chat/emoji-picker-content.js
+++ b/src/shared/chat/emoji-picker-content.js
@@ -2,6 +2,7 @@ import { CustomElement } from 'shared/components/element.js';
import { _converse, converse, api } from "@converse/headless/core";
import { html } from "lit";
import { tpl_all_emojis, tpl_search_results } from "./templates/emoji-picker.js";
+import { getTonedEmojis } from './utils.js';
const { sizzle } = converse.env;
@@ -90,7 +91,7 @@ export default class EmojiPickerContent extends CustomElement {
return true;
}
} else {
- if (this.current_skintone && converse.emojis.toned.includes(shortname)) {
+ if (this.current_skintone && getTonedEmojis().includes(shortname)) {
return true;
}
}
diff --git a/src/shared/chat/emoji-picker.js b/src/shared/chat/emoji-picker.js
index f382ab6e0..c672d809c 100644
--- a/src/shared/chat/emoji-picker.js
+++ b/src/shared/chat/emoji-picker.js
@@ -5,6 +5,7 @@ import debounce from 'lodash-es/debounce';
import { CustomElement } from 'shared/components/element.js';
import { KEYCODES } from '@converse/headless/shared/constants.js';
import { _converse, api, converse } from "@converse/headless/core";
+import { getTonedEmojis } from './utils.js';
import { tpl_emoji_picker } from "./templates/emoji-picker.js";
import './styles/emoji.scss';
@@ -221,7 +222,7 @@ export default class EmojiPicker extends CustomElement {
}
getTonedShortname (shortname) {
- if (converse.emojis.toned.includes(shortname) && this.current_skintone) {
+ if (getTonedEmojis().includes(shortname) && this.current_skintone) {
return `${shortname.slice(0, shortname.length-1)}_${this.current_skintone}:`
}
return shortname;
diff --git a/src/shared/chat/utils.js b/src/shared/chat/utils.js
index c82f5c0d8..4d3ff4936 100644
--- a/src/shared/chat/utils.js
+++ b/src/shared/chat/utils.js
@@ -1,8 +1,14 @@
import debounce from 'lodash/debounce';
import tpl_new_day from "./templates/new-day.js";
import { _converse, api, converse } from '@converse/headless/core';
+import { html } from 'lit';
+import {
+ convertASCII2Emoji,
+ getShortnameReferences,
+ getCodePointReferences
+} from '@converse/headless/plugins/emoji/utils.js';
-const { dayjs } = converse.env;
+const { dayjs, u } = converse.env;
export function onScrolledDown (model) {
if (!model.isHidden()) {
@@ -94,3 +100,102 @@ export function getHats (message) {
}
return [];
}
+
+function unique (arr) {
+ return [...new Set(arr)];
+}
+
+export function getTonedEmojis () {
+ if (!converse.emojis.toned) {
+ converse.emojis.toned = unique(
+ Object.values(converse.emojis.json.people)
+ .filter(person => person.sn.includes('_tone'))
+ .map(person => person.sn.replace(/_tone[1-5]/, ''))
+ );
+ }
+ return converse.emojis.toned;
+}
+
+export function getEmojiMarkup (data, options={unicode_only: false, add_title_wrapper: false}) {
+ const emoji = data.emoji;
+ const shortname = data.shortname;
+ if (emoji) {
+ if (options.unicode_only) {
+ return emoji;
+ } else if (api.settings.get('use_system_emojis')) {
+ if (options.add_title_wrapper) {
+ return shortname ? html`${emoji}` : emoji;
+ } else {
+ return emoji;
+ }
+ } else {
+ const path = api.settings.get('emoji_image_path');
+ return html`
`;
+ }
+ } else if (options.unicode_only) {
+ return shortname;
+ } else {
+ return html`
`;
+ }
+}
+
+export function addEmojisMarkup (text, options) {
+ let list = [text];
+ [...getShortnameReferences(text), ...getCodePointReferences(text)]
+ .sort((a, b) => b.begin - a.begin)
+ .forEach(ref => {
+ const text = list.shift();
+ const emoji = getEmojiMarkup(ref, options);
+ if (typeof emoji === 'string') {
+ list = [text.slice(0, ref.begin) + emoji + text.slice(ref.end), ...list];
+ } else {
+ list = [text.slice(0, ref.begin), emoji, text.slice(ref.end), ...list];
+ }
+ });
+ return list;
+}
+
+/**
+ * Returns an emoji represented by the passed in shortname.
+ * Scans the passed in text for shortnames and replaces them with
+ * emoji unicode glyphs or alternatively if it's a custom emoji
+ * without unicode representation then a lit TemplateResult
+ * which represents image tag markup is returned.
+ *
+ * The shortname needs to be defined in `emojis.json`
+ * and needs to have either a `cp` attribute for the codepoint, or
+ * an `url` attribute which points to the source for the image.
+ *
+ * @namespace u
+ * @method u.shortnamesToEmojis
+ * @param { String } str - String containg the shortname(s)
+ * @param { Object } options
+ * @param { Boolean } options.unicode_only - Whether emojis are rendered as
+ * unicode codepoints. If so, the returned result will be an array
+ * with containing one string, because the emojis themselves will
+ * also be strings. If set to false, emojis will be represented by
+ * lit TemplateResult objects.
+ * @param { Boolean } options.add_title_wrapper - Whether unicode
+ * codepoints should be wrapped with a `` element with a
+ * title, so that the shortname is shown upon hovering with the
+ * mouse.
+ * @returns {Array} An array of at least one string, or otherwise
+ * strings and lit TemplateResult objects.
+ */
+export function shortnamesToEmojis (str, options={unicode_only: false, add_title_wrapper: false}) {
+ str = convertASCII2Emoji(str);
+ return addEmojisMarkup(str, options);
+}
+
+
+Object.assign(u, { shortnamesToEmojis });
diff --git a/src/shared/rich-text.js b/src/shared/rich-text.js
index 7293835ae..d260e1e1c 100644
--- a/src/shared/rich-text.js
+++ b/src/shared/rich-text.js
@@ -4,15 +4,15 @@ import tpl_image from 'templates/image.js';
import tpl_video from 'templates/video.js';
import { api } from '@converse/headless/core';
import { containsDirectives, getDirectiveAndLength, getDirectiveTemplate, isQuoteDirective } from './styling.js';
+import { getEmojiMarkup } from './chat/utils.js';
import { getHyperlinkTemplate } from 'utils/html.js';
import { getMediaURLs } from '@converse/headless/shared/chat/utils.js';
import { getMediaURLsMetadata } from '@converse/headless/shared/parsers.js';
import {
convertASCII2Emoji,
getCodePointReferences,
- getEmojiMarkup,
getShortnameReferences
-} from '@converse/headless/plugins/emoji/index.js';
+} from '@converse/headless/plugins/emoji/utils.js';
import {
filterQueryParamsFromURL,
isAudioURL,
@@ -22,6 +22,7 @@ import {
shouldRenderMediaFromURL,
} from '@converse/headless/utils/url.js';
+
import { html } from 'lit';
const isString = s => typeof s === 'string';