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', ':)':'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`${emoji}`; - } - } else if (options.unicode_only) { - return shortname; - } else { - return html`${shortname}`; - } -} - - -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', ':)':'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( - ':xmpp:'); + ':xmpp:'); 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 :converse:'); + 'Running tests for :converse:'); })); }); }); 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`${emoji}`; + } + } else if (options.unicode_only) { + return shortname; + } else { + return html`${shortname}`; + } +} + +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';