diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 10:08:36 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 10:08:36 +0300 |
commit | 48aff82709769b098321c738f3444b9bdaa694c6 (patch) | |
tree | e00c7c43e2d9b603a5a6af576b1685e400410dee /app/assets/javascripts/emoji | |
parent | 879f5329ee916a948223f8f43d77fba4da6cd028 (diff) |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/emoji')
-rw-r--r-- | app/assets/javascripts/emoji/index.js | 239 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/support/index.js | 8 |
2 files changed, 194 insertions, 53 deletions
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js index 4567c807c40..4a56843c0b5 100644 --- a/app/assets/javascripts/emoji/index.js +++ b/app/assets/javascripts/emoji/index.js @@ -1,53 +1,57 @@ -import { uniq } from 'lodash'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; import emojiAliases from 'emojis/aliases.json'; import axios from '../lib/utils/axios_utils'; - import AccessorUtilities from '../lib/utils/accessor'; let emojiMap = null; -let emojiPromise = null; let validEmojiNames = null; export const EMOJI_VERSION = '1'; const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); -export function initEmojiMap() { - emojiPromise = - emojiPromise || - new Promise((resolve, reject) => { - if (emojiMap) { - resolve(emojiMap); - } else if ( - isLocalStorageAvailable && - window.localStorage.getItem('gl-emoji-map-version') === EMOJI_VERSION && - window.localStorage.getItem('gl-emoji-map') - ) { - emojiMap = JSON.parse(window.localStorage.getItem('gl-emoji-map')); - validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)]; - resolve(emojiMap); - } else { - // We load the JSON file direct from the server - // because it can't be loaded from a CDN due to - // cross domain problems with JSON - axios - .get(`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`) - .then(({ data }) => { - emojiMap = data; - validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)]; - resolve(emojiMap); - if (isLocalStorageAvailable) { - window.localStorage.setItem('gl-emoji-map-version', EMOJI_VERSION); - window.localStorage.setItem('gl-emoji-map', JSON.stringify(emojiMap)); - } - }) - .catch(err => { - reject(err); - }); - } - }); +async function loadEmoji() { + if ( + isLocalStorageAvailable && + window.localStorage.getItem('gl-emoji-map-version') === EMOJI_VERSION && + window.localStorage.getItem('gl-emoji-map') + ) { + return JSON.parse(window.localStorage.getItem('gl-emoji-map')); + } + + // We load the JSON file direct from the server + // because it can't be loaded from a CDN due to + // cross domain problems with JSON + const { data } = await axios.get( + `${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`, + ); + window.localStorage.setItem('gl-emoji-map-version', EMOJI_VERSION); + window.localStorage.setItem('gl-emoji-map', JSON.stringify(data)); + return data; +} + +async function prepareEmojiMap() { + emojiMap = await loadEmoji(); + + validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)]; + + Object.keys(emojiMap).forEach(name => { + emojiMap[name].aliases = []; + emojiMap[name].name = name; + }); + Object.entries(emojiAliases).forEach(([alias, name]) => { + // This check, `if (name in emojiMap)` is necessary during testing. In + // production, it shouldn't be necessary, because at no point should there + // be an entry in aliases.json with no corresponding entry in emojis.json. + // However, during testing, the endpoint for emojis.json is mocked with a + // small dataset, whereas aliases.json is always `import`ed directly. + if (name in emojiMap) emojiMap[name].aliases.push(alias); + }); +} - return emojiPromise; +export function initEmojiMap() { + initEmojiMap.promise = initEmojiMap.promise || prepareEmojiMap(); + return initEmojiMap.promise; } export function normalizeEmojiName(name) { @@ -62,13 +66,148 @@ export function isEmojiNameValid(name) { return validEmojiNames.indexOf(name) >= 0; } -export function filterEmojiNames(filter) { - const match = filter.toLowerCase(); - return validEmojiNames.filter(name => name.indexOf(match) >= 0); +export function getAllEmoji() { + return emojiMap; +} + +/** + * Retrieves an emoji by name or alias. + * + * Note: `initEmojiMap` must have been called and completed before this method + * can safely be called. + * + * @param {String} query The emoji name + * @param {Boolean} fallback If true, a fallback emoji will be returned if the + * named emoji does not exist. Defaults to false. + * @returns {Object} The matching emoji. + */ +export function getEmoji(query, fallback = false) { + // TODO https://gitlab.com/gitlab-org/gitlab/-/issues/268208 + const fallbackEmoji = emojiMap.grey_question; + if (!query) { + return fallback ? fallbackEmoji : null; + } + + if (!emojiMap) { + // eslint-disable-next-line @gitlab/require-i18n-strings + throw new Error('The emoji map is uninitialized or initialization has not completed'); + } + + const lowercaseQuery = query.toLowerCase(); + const name = normalizeEmojiName(lowercaseQuery); + + if (name in emojiMap) { + return emojiMap[name]; + } + + return fallback ? fallbackEmoji : null; } -export function filterEmojiNamesByAlias(filter) { - return uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name))); +const searchMatchers = { + // Fuzzy matching compares using a fuzzy matching library + fuzzy: (value, query) => { + const score = fuzzaldrinPlus.score(value, query) > 0; + return { score, success: score > 0 }; + }, + // Contains matching compares by indexOf + contains: (value, query) => { + const index = value.indexOf(query.toLowerCase()); + return { index, success: index >= 0 }; + }, + // Exact matching compares by equality + exact: (value, query) => { + return { success: value === query.toLowerCase() }; + }, +}; + +const searchPredicates = { + // Search by name + name: (matcher, query) => emoji => { + const m = matcher(emoji.name, query); + return [{ ...m, emoji, field: emoji.name }]; + }, + // Search by alias + alias: (matcher, query) => emoji => + emoji.aliases.map(alias => { + const m = matcher(alias, query); + return { ...m, emoji, field: alias }; + }), + // Search by description + description: (matcher, query) => emoji => { + const m = matcher(emoji.d, query); + return [{ ...m, emoji, field: emoji.d }]; + }, + // Search by unicode value (always exact) + unicode: (matcher, query) => emoji => { + return [{ emoji, field: emoji.e, success: emoji.e === query }]; + }, +}; + +/** + * Searches emoji by name, aliases, description, and unicode value and returns + * an array of matches. + * + * Behavior is undefined if `opts.fields` is empty or if `opts.match` is fuzzy + * and the query is empty. + * + * Note: `initEmojiMap` must have been called and completed before this method + * can safely be called. + * + * @param {String} query Search query. + * @param {Object} opts Search options (optional). + * @param {String[]} opts.fields Fields to search. Choices are 'name', 'alias', + * 'description', and 'unicode' (value). Default is all (four) fields. + * @param {String} opts.match Search method to use. Choices are 'exact', + * 'contains', or 'fuzzy'. All methods are case-insensitive. Exact matching (the + * default) compares by equality. Contains matching compares by indexOf. Fuzzy + * matching compares using a fuzzy matching library. + * @param {Boolean} opts.fallback If true, a fallback emoji will be returned if + * the result set is empty. Defaults to false. + * @param {Boolean} opts.raw Returns the raw match data instead of just the + * matching emoji. + * @returns {Object[]} A list of emoji that match the query. + */ +export function searchEmoji(query, opts) { + if (!emojiMap) { + // eslint-disable-next-line @gitlab/require-i18n-strings + throw new Error('The emoji map is uninitialized or initialization has not completed'); + } + + const { + fields = ['name', 'alias', 'description', 'unicode'], + match = 'exact', + fallback = false, + raw = false, + } = opts || {}; + + const fallbackEmoji = emojiMap.grey_question; + if (!query) { + if (fallback) { + return raw ? [{ emoji: fallbackEmoji }] : [fallbackEmoji]; + } + + return []; + } + + // optimization for an exact match in name and alias + if (match === 'exact' && new Set([...fields, 'name', 'alias']).size === 2) { + const emoji = getEmoji(query, fallback); + return emoji ? [emoji] : []; + } + + const matcher = searchMatchers[match] || searchMatchers.exact; + const predicates = fields.map(f => searchPredicates[f](matcher, query)); + + const results = Object.values(emojiMap) + .flatMap(emoji => predicates.flatMap(predicate => predicate(emoji))) + .filter(r => r.success); + + // Fallback to question mark for unknown emojis + if (fallback && results.length === 0) { + return raw ? [{ emoji: fallbackEmoji }] : [fallbackEmoji]; + } + + return raw ? results : results.map(r => r.emoji); } let emojiCategoryMap; @@ -95,16 +234,10 @@ export function getEmojiCategoryMap() { } export function getEmojiInfo(query) { - let name = normalizeEmojiName(query); - let emojiInfo = emojiMap[name]; - - // Fallback to question mark for unknown emojis - if (!emojiInfo) { - name = 'grey_question'; - emojiInfo = emojiMap[name]; - } - - return { ...emojiInfo, name }; + return searchEmoji(query, { + fields: ['name', 'alias'], + fallback: true, + })[0]; } export function emojiFallbackImageSrc(inputName) { diff --git a/app/assets/javascripts/emoji/support/index.js b/app/assets/javascripts/emoji/support/index.js index 1f7852dd487..14b80be9b43 100644 --- a/app/assets/javascripts/emoji/support/index.js +++ b/app/assets/javascripts/emoji/support/index.js @@ -5,6 +5,14 @@ import getUnicodeSupportMap from './unicode_support_map'; let browserUnicodeSupportMap; export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) { + // Skipping the map creation for Bots + RSPec + if ( + navigator.userAgent.indexOf('HeadlessChrome') > -1 || + navigator.userAgent.indexOf('Lighthouse') > -1 || + navigator.userAgent.indexOf('Speedindex') > -1 + ) { + return true; + } browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap(); return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion); } |