2019-07-11 12:30:51 +02:00
|
|
|
// Converse.js
|
|
|
|
// https://conversejs.org
|
|
|
|
//
|
|
|
|
// Copyright (c) 2012-2019, the Converse.js developers
|
|
|
|
// Licensed under the Mozilla Public License (MPLv2)
|
|
|
|
/**
|
|
|
|
* @module converse-emoji
|
|
|
|
*/
|
2019-09-19 16:54:55 +02:00
|
|
|
import { Model } from 'skeletor.js/src/model.js';
|
2019-07-11 12:30:51 +02:00
|
|
|
import * as twemoji from "twemoji";
|
|
|
|
import _ from "./lodash.noconflict";
|
|
|
|
import converse from "./converse-core";
|
|
|
|
|
2019-08-15 13:43:36 +02:00
|
|
|
const u = converse.env.utils;
|
2019-07-11 12:30:51 +02:00
|
|
|
|
|
|
|
const ASCII_LIST = {
|
2020-01-21 12:45:34 +01:00
|
|
|
'*\\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'
|
2019-07-11 12:30:51 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const ASCII_REGEX = '(\\*\\\\0\\/\\*|\\*\\\\O\\/\\*|\\-___\\-|\\:\'\\-\\)|\'\\:\\-\\)|\'\\:\\-D|\\>\\:\\-\\)|>\\:\\-\\)|\'\\:\\-\\(|\\>\\:\\-\\(|>\\:\\-\\(|\\:\'\\-\\(|O\\:\\-\\)|0\\:\\-3|0\\:\\-\\)|0;\\^\\)|O;\\-\\)|0;\\-\\)|O\\:\\-3|\\-__\\-|\\:\\-Þ|\\:\\-Þ|\\<\\/3|<\\/3|\\:\'\\)|\\:\\-D|\'\\:\\)|\'\\=\\)|\'\\:D|\'\\=D|\\>\\:\\)|>\\:\\)|\\>;\\)|>;\\)|\\>\\=\\)|>\\=\\)|;\\-\\)|\\*\\-\\)|;\\-\\]|;\\^\\)|\'\\:\\(|\'\\=\\(|\\:\\-\\*|\\:\\^\\*|\\>\\:P|>\\:P|X\\-P|\\>\\:\\[|>\\:\\[|\\:\\-\\(|\\:\\-\\[|\\>\\:\\(|>\\:\\(|\\:\'\\(|;\\-\\(|\\>\\.\\<|>\\.<|#\\-\\)|%\\-\\)|X\\-\\)|\\\\0\\/|\\\\O\\/|0\\:3|0\\:\\)|O\\:\\)|O\\=\\)|O\\:3|B\\-\\)|8\\-\\)|B\\-D|8\\-D|\\-_\\-|\\>\\:\\\\|>\\:\\\\|\\>\\:\\/|>\\:\\/|\\:\\-\\/|\\:\\-\\.|\\:\\-P|\\:Þ|\\:Þ|\\:\\-b|\\:\\-O|O_O|\\>\\:O|>\\:O|\\:\\-X|\\:\\-#|\\:\\-\\)|\\(y\\)|\\<3|<3|\\:D|\\=D|;\\)|\\*\\)|;\\]|;D|\\:\\*|\\=\\*|\\:\\(|\\:\\[|\\=\\(|\\:@|;\\(|D\\:|\\:\\$|\\=\\$|#\\)|%\\)|X\\)|B\\)|8\\)|\\:\\/|\\:\\\\|\\=\\/|\\=\\\\|\\:L|\\=L|\\:P|\\=P|\\:b|\\:O|\\:X|\\:#|\\=X|\\=#|\\:\\)|\\=\\]|\\=\\)|\\:\\])';
|
|
|
|
const ASCII_REPLACE_REGEX = new RegExp("<object[^>]*>.*?<\/object>|<span[^>]*>.*?<\/span>|<(?:object|embed|svg|img|div|span|p|a)[^>]*>|((\\s|^)"+ASCII_REGEX+"(?=\\s|$|[!,.?]))", "gi");
|
|
|
|
|
|
|
|
|
|
|
|
function convert (unicode) {
|
2019-08-15 13:43:36 +02:00
|
|
|
// Converts unicode code points and code pairs
|
|
|
|
// to their respective characters
|
2019-07-11 12:30:51 +02:00
|
|
|
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 twemoji.default.convert.fromCodePoint(unicode);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
converse.plugins.add('converse-emoji', {
|
|
|
|
|
2020-01-21 14:06:47 +01:00
|
|
|
initialize () {
|
2019-07-11 12:30:51 +02:00
|
|
|
/* The initialize function gets called as soon as the plugin is
|
|
|
|
* loaded by converse.js's plugin machinery.
|
|
|
|
*/
|
|
|
|
const { _converse } = this;
|
2019-10-16 17:17:29 +02:00
|
|
|
const { ___ } = _converse;
|
2019-07-11 12:30:51 +02:00
|
|
|
|
|
|
|
_converse.api.settings.update({
|
|
|
|
'emoji_image_path': twemoji.default.base,
|
2019-08-15 13:43:36 +02:00
|
|
|
'emoji_categories': {
|
|
|
|
"smileys": ":grinning:",
|
|
|
|
"people": ":thumbsup:",
|
|
|
|
"activity": ":soccer:",
|
|
|
|
"travel": ":motorcycle:",
|
|
|
|
"objects": ":bomb:",
|
|
|
|
"nature": ":rainbow:",
|
|
|
|
"food": ":hotdog:",
|
|
|
|
"symbols": ":musical_note:",
|
2019-10-16 17:17:29 +02:00
|
|
|
"flags": ":flag_ac:",
|
2019-11-09 11:53:44 +01:00
|
|
|
"custom": null
|
2019-10-16 17:17:29 +02:00
|
|
|
},
|
|
|
|
// We use the triple-underscore method which doesn't actually
|
|
|
|
// translate but does signify to gettext that these strings should
|
|
|
|
// go into the POT file. The translation then happens in the
|
|
|
|
// template. We do this so that users can pass in their own
|
|
|
|
// strings via converse.initialize, which is before __ is
|
|
|
|
// available.
|
|
|
|
'emoji_category_labels': {
|
|
|
|
"smileys": ___("Smileys and emotions"),
|
|
|
|
"people": ___("People"),
|
|
|
|
"activity": ___("Activities"),
|
|
|
|
"travel": ___("Travel"),
|
|
|
|
"objects": ___("Objects"),
|
|
|
|
"nature": ___("Animals and nature"),
|
|
|
|
"food": ___("Food and drink"),
|
|
|
|
"symbols": ___("Symbols"),
|
|
|
|
"flags": ___("Flags"),
|
|
|
|
"custom": ___("Stickers")
|
2019-08-15 13:43:36 +02:00
|
|
|
}
|
2019-07-11 12:30:51 +02:00
|
|
|
});
|
2019-10-16 17:17:29 +02:00
|
|
|
|
2020-01-21 12:45:34 +01:00
|
|
|
_converse.emojis = {};
|
2019-12-18 16:09:44 +01:00
|
|
|
_converse.api.promises.add('emojisInitialized', false);
|
2019-07-11 12:30:51 +02:00
|
|
|
twemoji.default.base = _converse.emoji_image_path;
|
|
|
|
|
2019-08-15 13:43:36 +02:00
|
|
|
|
2019-08-21 16:32:32 +02:00
|
|
|
/**
|
|
|
|
* Model for storing data related to the Emoji picker widget
|
|
|
|
* @class
|
|
|
|
* @namespace _converse.EmojiPicker
|
|
|
|
* @memberOf _converse
|
|
|
|
*/
|
2019-09-19 16:54:55 +02:00
|
|
|
_converse.EmojiPicker = Model.extend({
|
2019-08-16 10:18:35 +02:00
|
|
|
defaults: {
|
2019-08-21 16:32:32 +02:00
|
|
|
'current_category': 'smileys',
|
2019-08-16 10:18:35 +02:00
|
|
|
'current_skintone': '',
|
|
|
|
'scroll_position': 0
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-07-11 12:30:51 +02:00
|
|
|
function getTonedEmojis () {
|
|
|
|
if (!_converse.toned_emojis) {
|
|
|
|
_converse.toned_emojis = _.uniq(
|
2019-08-22 16:21:27 +02:00
|
|
|
Object.values(_converse.emojis.json.people)
|
2019-08-15 13:43:36 +02:00
|
|
|
.filter(person => person.sn.includes('_tone'))
|
|
|
|
.map(person => person.sn.replace(/_tone[1-5]/, ''))
|
2019-07-11 12:30:51 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
return _converse.toned_emojis;
|
|
|
|
}
|
|
|
|
|
2019-08-15 13:43:36 +02:00
|
|
|
|
|
|
|
/************************ BEGIN Utils ************************/
|
|
|
|
// Closured cache
|
|
|
|
const emojis_by_attribute = {};
|
|
|
|
|
|
|
|
Object.assign(u, {
|
2019-08-21 16:32:32 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Replaces emoji shortnames in the passed-in string with unicode or image-based emojis
|
|
|
|
* (based on the value of `use_system_emojis`).
|
|
|
|
* @method u.addEmoji
|
|
|
|
* @param {string} text = The text
|
|
|
|
* @returns {string} The text with shortnames replaced with emoji
|
|
|
|
* unicodes or images.
|
|
|
|
*/
|
|
|
|
addEmoji (text) {
|
|
|
|
return u.getEmojiRenderer()(text);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Based on the value of `use_system_emojis` will return either
|
|
|
|
* a function that converts emoji shortnames into unicode glyphs
|
|
|
|
* (see {@link u.shortnameToUnicode} or one that converts them into images.
|
|
|
|
* unicode emojis
|
|
|
|
* @method u.getEmojiRenderer
|
|
|
|
* @returns {function}
|
|
|
|
*/
|
|
|
|
getEmojiRenderer () {
|
|
|
|
const how = {
|
2019-10-16 17:17:29 +02:00
|
|
|
'attributes': icon => {
|
2019-08-21 16:32:32 +02:00
|
|
|
const codepoint = twemoji.default.convert.toCodePoint(icon);
|
|
|
|
return {'title': `${u.getEmojisByAtrribute('cp')[codepoint]['sn']} ${icon}`}
|
|
|
|
}
|
|
|
|
};
|
2019-10-16 17:17:29 +02:00
|
|
|
const transform = u.shortnamesToEmojis;
|
|
|
|
return _converse.use_system_emojis ? transform : text => twemoji.default.parse(transform(text), how);
|
2019-08-21 16:32:32 +02:00
|
|
|
},
|
|
|
|
|
2019-08-15 13:43:36 +02:00
|
|
|
/**
|
2019-10-16 17:17:29 +02:00
|
|
|
* 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 markup for an HTML image tag
|
|
|
|
* 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
|
2019-08-15 13:43:36 +02:00
|
|
|
* @param {string} str - String containg the shortname(s)
|
|
|
|
*/
|
2019-10-16 17:17:29 +02:00
|
|
|
shortnamesToEmojis (str, unicode_only=false) {
|
2019-08-15 13:43:36 +02:00
|
|
|
str = str.replace(_converse.emojis.shortnames_regex, shortname => {
|
2019-08-22 16:21:27 +02:00
|
|
|
if ((typeof shortname === 'undefined') || (shortname === '') || (!_converse.emoji_shortnames.includes(shortname))) {
|
2019-08-15 13:43:36 +02:00
|
|
|
// if the shortname doesnt exist just return the entire match
|
|
|
|
return shortname;
|
|
|
|
}
|
2019-10-16 17:17:29 +02:00
|
|
|
const codepoint = _converse.emojis_map[shortname].cp;
|
|
|
|
if (codepoint) {
|
|
|
|
return convert(codepoint.toUpperCase());
|
|
|
|
} else if (unicode_only) {
|
|
|
|
return shortname;
|
|
|
|
} else {
|
|
|
|
return `<img class="emoji" draggable="false" alt="${shortname}" src="${_converse.emojis_map[shortname].url}"/>`;
|
|
|
|
}
|
2019-08-15 13:43:36 +02:00
|
|
|
});
|
|
|
|
// Also replace ASCII smileys
|
|
|
|
str = 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);
|
|
|
|
});
|
|
|
|
return str;
|
|
|
|
},
|
|
|
|
|
2019-10-16 17:17:29 +02:00
|
|
|
/**
|
|
|
|
* Returns unicode represented by the passed in shortname.
|
|
|
|
* @method u.shortnameToUnicode
|
|
|
|
* @param {string} str - String containg the shortname(s)
|
|
|
|
*/
|
|
|
|
shortnameToUnicode (str) {
|
|
|
|
return this.shortnamesToEmojis(str, true);
|
|
|
|
},
|
|
|
|
|
2019-08-15 13:43:36 +02:00
|
|
|
/**
|
|
|
|
* Determines whether the passed in string is just a single emoji shortname;
|
2019-11-18 12:49:44 +01:00
|
|
|
* @method u.isOnlyEmojis
|
2019-08-15 13:43:36 +02:00
|
|
|
* @param {string} shortname - A string which migh be just an emoji shortname
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
2019-11-18 12:49:44 +01:00
|
|
|
isOnlyEmojis (text) {
|
|
|
|
const words = text.trim().split(/\s+/);
|
2019-11-20 14:36:53 +01:00
|
|
|
if (words.length === 0 || words.length > 3) {
|
2019-11-18 12:49:44 +01:00
|
|
|
return false;
|
2019-08-15 13:43:36 +02:00
|
|
|
}
|
2019-11-18 12:49:44 +01:00
|
|
|
const rejects = words.filter(text => {
|
|
|
|
const result = twemoji.default.parse(u.shortnameToUnicode(text));
|
|
|
|
const match = result.match(/<img class="emoji" draggable="false" alt=".*?" src=".*?\.png"\/>/);
|
|
|
|
return !match || match.length !== 1;
|
|
|
|
});
|
|
|
|
return rejects.length === 0;
|
2019-08-15 13:43:36 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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') {
|
2019-08-22 16:21:27 +02:00
|
|
|
return _converse.emojis.json;
|
2019-08-15 13:43:36 +02:00
|
|
|
}
|
|
|
|
const all_variants = _converse.emojis_list
|
|
|
|
.map(e => e[attr])
|
|
|
|
.filter((c, i, arr) => arr.indexOf(c) == i);
|
|
|
|
|
2019-08-23 08:20:33 +02:00
|
|
|
emojis_by_attribute[attr] = {};
|
|
|
|
all_variants.forEach(v => (emojis_by_attribute[attr][v] = _.find(_converse.emojis_list, i => (i[attr] === v))));
|
2019-08-15 13:43:36 +02:00
|
|
|
return emojis_by_attribute[attr];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
/************************ END Utils ************************/
|
|
|
|
|
2020-01-22 09:25:53 +01:00
|
|
|
|
|
|
|
/************************ BEGIN API ************************/
|
|
|
|
// We extend the default converse.js API to add methods specific to MUC groupchats.
|
|
|
|
Object.assign(_converse.api, {
|
2020-01-21 12:45:34 +01:00
|
|
|
/**
|
2020-01-22 09:25:53 +01:00
|
|
|
* The "rooms" namespace groups methods relevant to chatrooms
|
|
|
|
* (aka groupchats).
|
|
|
|
*
|
|
|
|
* @namespace _converse.api.rooms
|
|
|
|
* @memberOf _converse.api
|
2020-01-21 12:45:34 +01:00
|
|
|
*/
|
2020-01-22 09:25:53 +01:00
|
|
|
emojis: {
|
|
|
|
/**
|
|
|
|
* Initializes Emoji support by downloading the emojis JSON (and any applicable images).
|
|
|
|
* @method _converse.api.emojis.initialize
|
|
|
|
* @returns {Promise}
|
|
|
|
*/
|
|
|
|
async initialize () {
|
|
|
|
if (_converse.emojis.initialized) {
|
|
|
|
return _converse.emojis.initialized;
|
|
|
|
}
|
|
|
|
_converse.emojis.initialized = u.getResolveablePromise();
|
|
|
|
const { default: json } = await import(/*webpackChunkName: "emojis" */ './emojis.json');
|
|
|
|
_converse.emojis.json = json;
|
|
|
|
_converse.emojis.categories = Object.keys(_converse.emojis.json);
|
|
|
|
_converse.emojis_map = _converse.emojis.categories.reduce((result, cat) => Object.assign(result, _converse.emojis.json[cat]), {});
|
|
|
|
_converse.emojis_list = Object.values(_converse.emojis_map);
|
|
|
|
_converse.emojis_list.sort((a, b) => a.sn < b.sn ? -1 : (a.sn > b.sn ? 1 : 0));
|
|
|
|
_converse.emoji_shortnames = _converse.emojis_list.map(m => m.sn);
|
2019-08-21 16:32:32 +02:00
|
|
|
|
2020-01-22 09:25:53 +01:00
|
|
|
const getShortNames = () => _converse.emoji_shortnames.map(s => s.replace(/[+]/g, "\\$&")).join('|');
|
|
|
|
_converse.emojis.shortnames_regex = new RegExp("<object[^>]*>.*?<\/object>|<span[^>]*>.*?<\/span>|<(?:object|embed|svg|img|div|span|p|a)[^>]*>|("+getShortNames()+")", "gi");
|
2020-01-21 12:45:34 +01:00
|
|
|
|
2020-01-22 09:25:53 +01:00
|
|
|
_converse.emojis.toned = getTonedEmojis();
|
|
|
|
_converse.emojis.initialized.resolve();
|
|
|
|
/**
|
|
|
|
* Triggered once the JSON file representing emoji data has been
|
|
|
|
* fetched and its save to start calling emoji utility methods.
|
|
|
|
* @event _converse#emojisInitialized
|
|
|
|
*/
|
|
|
|
_converse.api.trigger('emojisInitialized');
|
|
|
|
}
|
|
|
|
}
|
2020-01-21 12:45:34 +01:00
|
|
|
});
|
2019-07-11 12:30:51 +02:00
|
|
|
}
|
|
|
|
});
|