diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js index 5da8de1cf5c..5755bf1c4c1 100644 --- a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js +++ b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js @@ -1,55 +1,61 @@ // This code is largely borrowed from: -// https://github.com/missive/emoji-mart/blob/bbd4fbe/src/utils/emoji-index.js +// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js import data from './emoji_mart_data_light'; import { getData, getSanitizedData, intersect } from './emoji_utils'; +let originalPool = {}; let index = {}; let emojisList = {}; let emoticonsList = {}; -let previousInclude = []; -let previousExclude = []; for (let emoji in data.emojis) { - let emojiData = data.emojis[emoji], - { short_names, emoticons } = emojiData, - id = short_names[0]; + let emojiData = data.emojis[emoji]; + let { short_names, emoticons } = emojiData; + let id = short_names[0]; + + if (emoticons) { + emoticons.forEach(emoticon => { + if (emoticonsList[emoticon]) { + return; + } - for (let emoticon of (emoticons || [])) { - if (!emoticonsList[emoticon]) { emoticonsList[emoticon] = id; - } + }); } emojisList[id] = getSanitizedData(id); + originalPool[id] = emojiData; +} + +function addCustomToPool(custom, pool) { + custom.forEach((emoji) => { + let emojiId = emoji.id || emoji.short_names[0]; + + if (emojiId && !pool[emojiId]) { + pool[emojiId] = getData(emoji); + emojisList[emojiId] = getSanitizedData(emoji); + } + }); } function search(value, { emojisToShowFilter, maxResults, include, exclude, custom = [] } = {}) { + addCustomToPool(custom, originalPool); + maxResults = maxResults || 75; include = include || []; exclude = exclude || []; - if (custom.length) { - for (const emoji of custom) { - data.emojis[emoji.id] = getData(emoji); - emojisList[emoji.id] = getSanitizedData(emoji); - } - - data.categories.push({ - name: 'Custom', - emojis: custom.map(emoji => emoji.id), - }); - } - - let results = null; - let pool = data.emojis; + let results = null, + pool = originalPool; if (value.length) { if (value === '-' || value === '-1') { return [emojisList['-1']]; } - let values = value.toLowerCase().split(/[\s|,|\-|_]+/); + let values = value.toLowerCase().split(/[\s|,|\-|_]+/), + allResults = []; if (values.length > 2) { values = [values[0], values[1]]; @@ -58,33 +64,32 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo if (include.length || exclude.length) { pool = {}; - if (previousInclude !== include.sort().join(',') || previousExclude !== exclude.sort().join(',')) { - previousInclude = include.sort().join(','); - previousExclude = exclude.sort().join(','); - index = {}; - } - - for (let category of data.categories) { + data.categories.forEach(category => { let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true; let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false; if (!isIncluded || isExcluded) { - continue; + return; } - for (let emojiId of category.emojis) { - pool[emojiId] = data.emojis[emojiId]; + category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]); + }); + + if (custom.length) { + let customIsIncluded = include && include.length ? include.indexOf('custom') > -1 : true; + let customIsExcluded = exclude && exclude.length ? exclude.indexOf('custom') > -1 : false; + if (customIsIncluded && !customIsExcluded) { + addCustomToPool(custom, pool); } } - } else if (previousInclude.length || previousExclude.length) { - index = {}; } - let allResults = values.map((value) => { - let aPool = pool; - let aIndex = index; - let length = 0; + allResults = values.map((value) => { + let aPool = pool, + aIndex = index, + length = 0; - for (let char of value.split('')) { + for (let charIndex = 0; charIndex < value.length; charIndex++) { + const char = value[charIndex]; length++; aIndex[char] = aIndex[char] || {}; @@ -104,9 +109,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo if (subIndex !== -1) { let score = subIndex + 1; - if (sub === id) { - score = 0; - } + if (sub === id) score = 0; aIndex.results.push(emojisList[id]); aIndex.pool[id] = emoji; @@ -130,7 +133,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo }).filter(a => a); if (allResults.length > 1) { - results = intersect(...allResults); + results = intersect.apply(null, allResults); } else if (allResults.length) { results = allResults[0]; } else { diff --git a/app/javascript/mastodon/features/emoji/emoji_picker.js b/app/javascript/mastodon/features/emoji/emoji_picker.js new file mode 100644 index 00000000000..7e145381ea0 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/emoji_picker.js @@ -0,0 +1,7 @@ +import Picker from 'emoji-mart/dist-es/components/picker'; +import Emoji from 'emoji-mart/dist-es/components/emoji'; + +export { + Picker, + Emoji, +}; diff --git a/app/javascript/mastodon/features/emoji/emoji_utils.js b/app/javascript/mastodon/features/emoji/emoji_utils.js index 2742185d9c7..dbf725c1f53 100644 --- a/app/javascript/mastodon/features/emoji/emoji_utils.js +++ b/app/javascript/mastodon/features/emoji/emoji_utils.js @@ -1,11 +1,9 @@ // This code is largely borrowed from: -// https://github.com/missive/emoji-mart/blob/bbd4fbe/src/utils/index.js +// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js import data from './emoji_mart_data_light'; -const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/; - -function buildSearch(thisData) { +const buildSearch = (data) => { const search = []; let addToSearch = (strings, split) => { @@ -24,19 +22,68 @@ function buildSearch(thisData) { }); }; - addToSearch(thisData.short_names, true); - addToSearch(thisData.name, true); - addToSearch(thisData.keywords, false); - addToSearch(thisData.emoticons, false); + addToSearch(data.short_names, true); + addToSearch(data.name, true); + addToSearch(data.keywords, false); + addToSearch(data.emoticons, false); - return search; -} + return search.join(','); +}; + +const _String = String; + +const stringFromCodePoint = _String.fromCodePoint || function () { + let MAX_SIZE = 0x4000; + let codeUnits = []; + let highSurrogate; + let lowSurrogate; + let index = -1; + let length = arguments.length; + if (!length) { + return ''; + } + let result = ''; + while (++index < length) { + let codePoint = Number(arguments[index]); + if ( + !isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity` + codePoint < 0 || // not a valid Unicode code point + codePoint > 0x10FFFF || // not a valid Unicode code point + Math.floor(codePoint) !== codePoint // not an integer + ) { + throw RangeError('Invalid code point: ' + codePoint); + } + if (codePoint <= 0xFFFF) { // BMP code point + codeUnits.push(codePoint); + } else { // Astral code point; split in surrogate halves + // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae + codePoint -= 0x10000; + highSurrogate = (codePoint >> 10) + 0xD800; + lowSurrogate = (codePoint % 0x400) + 0xDC00; + codeUnits.push(highSurrogate, lowSurrogate); + } + if (index + 1 === length || codeUnits.length > MAX_SIZE) { + result += String.fromCharCode.apply(null, codeUnits); + codeUnits.length = 0; + } + } + return result; +}; + + +const _JSON = JSON; + +const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/; +const SKINS = [ + '1F3FA', '1F3FB', '1F3FC', + '1F3FD', '1F3FE', '1F3FF', +]; function unifiedToNative(unified) { let unicodes = unified.split('-'), codePoints = unicodes.map((u) => `0x${u}`); - return String.fromCodePoint(...codePoints); + return stringFromCodePoint.apply(null, codePoints); } function sanitize(emoji) { @@ -70,11 +117,11 @@ function sanitize(emoji) { }; } -function getSanitizedData(emoji) { - return sanitize(getData(emoji)); +function getSanitizedData() { + return sanitize(getData(...arguments)); } -function getData(emoji) { +function getData(emoji, skin, set) { let emojiData = {}; if (typeof emoji === 'string') { @@ -83,6 +130,9 @@ function getData(emoji) { if (matches) { emoji = matches[1]; + if (matches[2]) { + skin = parseInt(matches[2]); + } } if (data.short_names.hasOwnProperty(emoji)) { @@ -92,17 +142,6 @@ function getData(emoji) { if (data.emojis.hasOwnProperty(emoji)) { emojiData = data.emojis[emoji]; } - } else if (emoji.custom) { - emojiData = emoji; - - emojiData.search = buildSearch({ - short_names: emoji.short_names, - name: emoji.name, - keywords: emoji.keywords, - emoticons: emoji.emoticons, - }); - - emojiData.search = emojiData.search.join(','); } else if (emoji.id) { if (data.short_names.hasOwnProperty(emoji.id)) { emoji.id = data.short_names[emoji.id]; @@ -110,31 +149,110 @@ function getData(emoji) { if (data.emojis.hasOwnProperty(emoji.id)) { emojiData = data.emojis[emoji.id]; + skin = skin || emoji.skin; + } + } + + if (!Object.keys(emojiData).length) { + emojiData = emoji; + emojiData.custom = true; + + if (!emojiData.search) { + emojiData.search = buildSearch(emoji); } } emojiData.emoticons = emojiData.emoticons || []; emojiData.variations = emojiData.variations || []; + if (emojiData.skin_variations && skin > 1 && set) { + emojiData = JSON.parse(_JSON.stringify(emojiData)); + + let skinKey = SKINS[skin - 1], + variationData = emojiData.skin_variations[skinKey]; + + if (!variationData.variations && emojiData.variations) { + delete emojiData.variations; + } + + if (variationData[`has_img_${set}`]) { + emojiData.skin_tone = skin; + + for (let k in variationData) { + let v = variationData[k]; + emojiData[k] = v; + } + } + } + if (emojiData.variations && emojiData.variations.length) { - emojiData = JSON.parse(JSON.stringify(emojiData)); + emojiData = JSON.parse(_JSON.stringify(emojiData)); emojiData.unified = emojiData.variations.shift(); } return emojiData; } -function intersect(a, b) { - let set; - let list; - if (a.length < b.length) { - set = new Set(a); - list = b; - } else { - set = new Set(b); - list = a; - } - return Array.from(new Set(list.filter(x => set.has(x)))); +function uniq(arr) { + return arr.reduce((acc, item) => { + if (acc.indexOf(item) === -1) { + acc.push(item); + } + return acc; + }, []); } -export { getData, getSanitizedData, intersect }; +function intersect(a, b) { + const uniqA = uniq(a); + const uniqB = uniq(b); + + return uniqA.filter(item => uniqB.indexOf(item) >= 0); +} + +function deepMerge(a, b) { + let o = {}; + + for (let key in a) { + let originalValue = a[key], + value = originalValue; + + if (b.hasOwnProperty(key)) { + value = b[key]; + } + + if (typeof value === 'object') { + value = deepMerge(originalValue, value); + } + + o[key] = value; + } + + return o; +} + +// https://github.com/sonicdoe/measure-scrollbar +function measureScrollbar() { + const div = document.createElement('div'); + + div.style.width = '100px'; + div.style.height = '100px'; + div.style.overflow = 'scroll'; + div.style.position = 'absolute'; + div.style.top = '-9999px'; + + document.body.appendChild(div); + const scrollbarWidth = div.offsetWidth - div.clientWidth; + document.body.removeChild(div); + + return scrollbarWidth; +} + +export { + getData, + getSanitizedData, + uniq, + intersect, + deepMerge, + unifiedToNative, + measureScrollbar, +}; diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 6978da2f9ef..8f7b91d218b 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -1,5 +1,5 @@ export function EmojiPicker () { - return import(/* webpackChunkName: "emoji_picker" */'emoji-mart'); + return import(/* webpackChunkName: "emoji_picker" */'../../emoji/emoji_picker'); } export function Compose () { diff --git a/package.json b/package.json index d94186cf271..3d085690248 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "css-loader": "^0.28.4", "detect-passive-events": "^1.0.2", "dotenv": "^4.0.0", - "emoji-mart": "^2.0.1", + "emoji-mart": "^2.1.1", "es6-symbol": "^3.1.1", "escape-html": "^1.0.3", "express": "^4.15.2", diff --git a/spec/javascript/components/emoji_index.test.js b/spec/javascript/components/emoji_index.test.js index 07d26a685fc..cdb50cb8c10 100644 --- a/spec/javascript/components/emoji_index.test.js +++ b/spec/javascript/components/emoji_index.test.js @@ -100,7 +100,12 @@ describe('emoji_index', () => { it('can search for thinking_face', () => { let expected = [ { id: 'thinking_face', unified: '1f914', native: '🤔' } ]; expect(search('thinking_fac').map(trimEmojis)).to.deep.equal(expected); - // this is currently broken in emoji-mart - // expect(emojiIndex.search('thinking_fac').map(trimEmojis)).to.deep.equal(expected); + expect(emojiIndex.search('thinking_fac').map(trimEmojis)).to.deep.equal(expected); + }); + + it('can search for woman-facepalming', () => { + let expected = [ { id: 'woman-facepalming', unified: '1f926-200d-2640-fe0f', native: '🤦‍♀️' } ]; + expect(search('woman-facep').map(trimEmojis)).to.deep.equal(expected); + expect(emojiIndex.search('woman-facep').map(trimEmojis)).deep.equal(expected); }); }); diff --git a/yarn.lock b/yarn.lock index 4f085ff2c9d..f0d2f5c232b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2191,9 +2191,9 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" -emoji-mart@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-2.0.1.tgz#b76ea33f2dabc82d8c1d4b6463c8a07fbce23682" +emoji-mart@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-2.1.1.tgz#4bce8ec9d9fd0d8adfd2517e7e296871c40762ac" emoji-regex@^6.1.0: version "6.4.3"