[Glitch] Refactor emoji search
Port 34c91555ae956181e3f45602220a805a3ac7cf9e to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
0c80bf0e82
commit
3f884ada27
@ -7,7 +7,7 @@ import api from 'flavours/glitch/api';
|
||||
import { browserHistory } from 'flavours/glitch/components/router';
|
||||
import { countableText } from 'flavours/glitch/features/compose/util/counter';
|
||||
import { tagHistory } from 'flavours/glitch/settings';
|
||||
import { fetchCustomEmojiData } from '@/flavours/glitch/features/emoji/picker';
|
||||
import { emojiMartSearch } from '@/flavours/glitch/features/emoji/picker';
|
||||
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
|
||||
|
||||
import { showAlert, showAlertForError } from './alerts';
|
||||
@ -625,9 +625,9 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, token) => {
|
||||
}, 200, { leading: true, trailing: true });
|
||||
|
||||
const fetchComposeSuggestionsEmojis = async (dispatch, token) => {
|
||||
const custom = await fetchCustomEmojiData();
|
||||
const { search } = await import('@/flavours/glitch/features/emoji/emoji_mart_search_light');
|
||||
const results = search(token.replace(':', ''), { maxResults: 5, custom });
|
||||
// Right now we are hard-coding the locale to English since the picker search only supports English.
|
||||
// Once we replace the legacy picker we can remove this and use the actual locale of the user.
|
||||
const results = await emojiMartSearch(token, 'en', 5);
|
||||
dispatch(readyComposeSuggestionsEmojis(token, results));
|
||||
};
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { useCustomEmojis } from '@/flavours/glitch/hooks/useCustomEmojis';
|
||||
import { Emoji } from './emoji';
|
||||
|
||||
interface LegacyEmoji {
|
||||
colons: string;
|
||||
id: string;
|
||||
custom?: boolean;
|
||||
native?: string;
|
||||
imageUrl?: string;
|
||||
@ -13,10 +13,11 @@ interface LegacyEmoji {
|
||||
|
||||
export const AutosuggestEmoji: FC<{ emoji: LegacyEmoji }> = ({ emoji }) => {
|
||||
const emojis = useCustomEmojis();
|
||||
const colons = `:${emoji.id}:`;
|
||||
return (
|
||||
<div className='autosuggest-emoji'>
|
||||
<Emoji code={emoji.native ?? emoji.colons} customEmoji={emojis} />
|
||||
<div className='autosuggest-emoji__name'>{emoji.colons}</div>
|
||||
<Emoji code={emoji.native ?? colons} customEmoji={emojis} />
|
||||
<div className='autosuggest-emoji__name'>{colons}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -46,6 +46,8 @@ const loadDB = (() => {
|
||||
return loadPromise;
|
||||
})();
|
||||
|
||||
type ScoreMap = Map<string, AnyEmojiData & { score: number }>;
|
||||
|
||||
export async function search({
|
||||
query,
|
||||
locale: localeString,
|
||||
@ -75,7 +77,7 @@ export async function search({
|
||||
|
||||
// Create an array of emoji results
|
||||
const db = await loadDB();
|
||||
const resultArrays: Map<string, AnyEmojiData>[] = [];
|
||||
const resultArrays: ScoreMap[] = [];
|
||||
for (let i = 0; i < queryTokens.length; i++) {
|
||||
const token = queryTokens[i];
|
||||
if (!token) continue;
|
||||
@ -90,14 +92,21 @@ export async function search({
|
||||
db.getAllFromIndex(locale, 'tokens', range),
|
||||
db.getAllFromIndex('custom', 'tokens', range),
|
||||
]);
|
||||
const resultMap = new Map<string, AnyEmojiData>([
|
||||
...unicodeResults.map(
|
||||
(emoji) => [emoji.hexcode, emoji] as [string, AnyEmojiData],
|
||||
),
|
||||
...customResults.map(
|
||||
(emoji) => [emoji.shortcode, emoji] as [string, AnyEmojiData],
|
||||
),
|
||||
]);
|
||||
const resultMap: ScoreMap = new Map();
|
||||
for (const emoji of unicodeResults) {
|
||||
const score = getScoreForEmoji(emoji, token);
|
||||
if (score === null) {
|
||||
continue;
|
||||
}
|
||||
resultMap.set(emoji.hexcode, { ...emoji, score });
|
||||
}
|
||||
for (const emoji of customResults) {
|
||||
const score = getScoreForEmoji(emoji, token);
|
||||
if (score === null) {
|
||||
continue;
|
||||
}
|
||||
resultMap.set(emoji.shortcode, { ...emoji, score });
|
||||
}
|
||||
log('found %d results for token "%s"', resultMap.size, token);
|
||||
resultArrays.push(resultMap);
|
||||
}
|
||||
@ -106,7 +115,7 @@ export async function search({
|
||||
const results = Array.from(
|
||||
resultArrays
|
||||
.reduce((prev, curr) => {
|
||||
const intersection = new Map<string, AnyEmojiData>();
|
||||
const intersection: ScoreMap = new Map();
|
||||
for (const [code, emoji] of prev) {
|
||||
if (curr.has(code)) {
|
||||
intersection.set(code, emoji);
|
||||
@ -115,33 +124,7 @@ export async function search({
|
||||
return intersection;
|
||||
})
|
||||
.values(),
|
||||
);
|
||||
|
||||
results.sort((a, b) => {
|
||||
// Checks if a or b has the last token exactly, or only a prefix.
|
||||
const aHasToken = a.tokens.includes(lastToken);
|
||||
const bHasToken = b.tokens.includes(lastToken);
|
||||
if (aHasToken && !bHasToken) {
|
||||
return -1;
|
||||
} else if (!aHasToken && bHasToken) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If one is a custom emoji, prioritize it over Unicode emojis.
|
||||
if ('category' in a) {
|
||||
return -1;
|
||||
} else if ('category' in b) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If both are Unicode emojis, prioritize by order.
|
||||
if ('order' in a && 'order' in b) {
|
||||
return (a.order ?? 0) - (b.order ?? 0); // If these are both Unicode emojis, sort by order.
|
||||
}
|
||||
|
||||
// ¯\_(ツ)_/¯
|
||||
return 0;
|
||||
});
|
||||
).toSorted((a, b) => a.score - b.score);
|
||||
|
||||
const time = performance.measure('emoji-search-end', 'emoji-search-start');
|
||||
log(
|
||||
@ -157,6 +140,24 @@ export async function search({
|
||||
return results;
|
||||
}
|
||||
|
||||
function getScoreForEmoji(emoji: AnyEmojiData, query: string) {
|
||||
const id = 'shortcode' in emoji ? emoji.shortcode : emoji.label;
|
||||
if (id === query) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let index = 1;
|
||||
for (const token of [id, emoji.tokens]) {
|
||||
const tokenIndex = token.indexOf(query);
|
||||
if (tokenIndex !== -1) {
|
||||
return index + tokenIndex / token.length;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function putEmojiData(emojis: CompactEmoji[], locale: Locale) {
|
||||
loadedLocales.add(locale);
|
||||
const db = await loadDB();
|
||||
@ -252,6 +253,12 @@ export async function loadEmojiByHexcode(
|
||||
return skinHexcodeToEmoji(hexcode, skinResult);
|
||||
}
|
||||
|
||||
export async function loadAllUnicodeEmojis(localeString: string) {
|
||||
const locale = await toLoadedLocale(localeString);
|
||||
const db = await loadDB();
|
||||
return db.getAll(locale);
|
||||
}
|
||||
|
||||
export async function loadCustomEmojiByShortcode(shortcode: string) {
|
||||
const db = await loadDB();
|
||||
return db.get('custom', shortcode);
|
||||
@ -285,6 +292,11 @@ export async function loadLegacyShortcodesByShortcode(shortcode: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadAllShortcodes() {
|
||||
const db = await loadDB();
|
||||
return db.getAll('shortcodes');
|
||||
}
|
||||
|
||||
// Private functions
|
||||
|
||||
async function syncLocales(db: Database) {
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import type { BaseEmoji, EmojiData, NimbleEmojiIndex } from 'emoji-mart';
|
||||
import type { Category, Data, Emoji } from 'emoji-mart/dist-es/utils/data';
|
||||
|
||||
/*
|
||||
* The 'search' property, although not defined in the [`Emoji`]{@link node_modules/@types/emoji-mart/dist-es/utils/data.d.ts#Emoji} type,
|
||||
* is used in the application.
|
||||
* This could be due to an oversight by the library maintainer.
|
||||
* The `search` property is defined and used [here]{@link node_modules/emoji-mart/dist/utils/data.js#uncompress}.
|
||||
*/
|
||||
export type Search = string;
|
||||
/*
|
||||
* The 'skins' property does not exist in the application data.
|
||||
* This could be a potential area of refactoring or error handling.
|
||||
* The non-existence of 'skins' property is evident at [this location]{@link app/javascript/flavours/glitch/features/emoji/emoji_compressed.js:121}.
|
||||
*/
|
||||
type Skins = null;
|
||||
|
||||
type Filename = string;
|
||||
type UnicodeFilename = string;
|
||||
export type FilenameData = [
|
||||
filename: Filename,
|
||||
unicodeFilename?: UnicodeFilename,
|
||||
][];
|
||||
export type ShortCodesToEmojiDataKey =
|
||||
| EmojiData['id']
|
||||
| BaseEmoji['native']
|
||||
| keyof NimbleEmojiIndex['emojis'];
|
||||
|
||||
type SearchData = [
|
||||
BaseEmoji['native'],
|
||||
Emoji['short_names'],
|
||||
Search,
|
||||
Emoji['unified'],
|
||||
];
|
||||
|
||||
export type ShortCodesToEmojiData = Record<
|
||||
ShortCodesToEmojiDataKey,
|
||||
[FilenameData, SearchData]
|
||||
>;
|
||||
type EmojisWithoutShortCodes = FilenameData;
|
||||
|
||||
type EmojiCompressed = [
|
||||
ShortCodesToEmojiData,
|
||||
Skins,
|
||||
Category[],
|
||||
Data['aliases'],
|
||||
EmojisWithoutShortCodes,
|
||||
];
|
||||
|
||||
/*
|
||||
* `emoji_compressed.js` uses `babel-plugin-preval`, which makes it difficult to convert to TypeScript.
|
||||
* As a temporary solution, we are allowing a default export here to apply the TypeScript type `EmojiCompressed` to the JS file export.
|
||||
* - {@link app/javascript/flavours/glitch/features/emoji/emoji_compressed.js}
|
||||
*/
|
||||
declare const emojiCompressed: EmojiCompressed;
|
||||
|
||||
export default emojiCompressed; // eslint-disable-line import/no-default-export
|
||||
@ -1,138 +0,0 @@
|
||||
// @preval
|
||||
// http://www.unicode.org/Public/emoji/5.0/emoji-test.txt
|
||||
// This file contains the compressed version of the emoji data from
|
||||
// both emoji_map.json and from emoji-mart's emojiIndex and data objects.
|
||||
// It's designed to be emitted in an array format to take up less space
|
||||
// over the wire.
|
||||
|
||||
// This version comment should be bumped each time the emoji data is changed
|
||||
// to ensure that the prevaled file is regenerated by Babel
|
||||
// version: 4
|
||||
|
||||
import { NimbleEmojiIndex } from 'emoji-mart';
|
||||
import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
|
||||
|
||||
import data from './emoji_data.json';
|
||||
import emojiMap from './emoji_map.json';
|
||||
import { unicodeToFilename, unicodeToUnifiedName } from './unicode_utils';
|
||||
|
||||
emojiMartUncompress(data);
|
||||
|
||||
const emojiMartData = data;
|
||||
const emojiIndex = new NimbleEmojiIndex(emojiMartData);
|
||||
|
||||
const excluded = ['®', '©', '™'];
|
||||
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
|
||||
const shortcodeMap = {};
|
||||
|
||||
const shortCodesToEmojiData = {};
|
||||
const emojisWithoutShortCodes = [];
|
||||
|
||||
Object.keys(emojiIndex.emojis).forEach((key) => {
|
||||
let emoji = emojiIndex.emojis[key];
|
||||
|
||||
// Emojis with skin tone modifiers are stored like this
|
||||
if (Object.hasOwn(emoji, '1')) {
|
||||
emoji = emoji['1'];
|
||||
}
|
||||
|
||||
shortcodeMap[emoji.native] = emoji.id;
|
||||
});
|
||||
|
||||
const stripModifiers = (unicode) => {
|
||||
skinTones.forEach((tone) => {
|
||||
unicode = unicode.replace(tone, '');
|
||||
});
|
||||
|
||||
return unicode;
|
||||
};
|
||||
|
||||
Object.keys(emojiMap).forEach((key) => {
|
||||
if (excluded.includes(key)) {
|
||||
delete emojiMap[key];
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedKey = stripModifiers(key);
|
||||
let shortcode = shortcodeMap[normalizedKey];
|
||||
|
||||
if (!shortcode) {
|
||||
shortcode = shortcodeMap[normalizedKey + '\uFE0F'];
|
||||
}
|
||||
|
||||
const filename = emojiMap[key];
|
||||
|
||||
const filenameData = [key];
|
||||
|
||||
if (unicodeToFilename(key) !== filename) {
|
||||
// filename can't be derived using unicodeToFilename
|
||||
filenameData.push(filename);
|
||||
}
|
||||
|
||||
if (typeof shortcode === 'undefined') {
|
||||
emojisWithoutShortCodes.push(filenameData);
|
||||
} else {
|
||||
if (!Array.isArray(shortCodesToEmojiData[shortcode])) {
|
||||
shortCodesToEmojiData[shortcode] = [[]];
|
||||
}
|
||||
|
||||
shortCodesToEmojiData[shortcode][0].push(filenameData);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(emojiIndex.emojis).forEach((key) => {
|
||||
let emoji = emojiIndex.emojis[key];
|
||||
|
||||
// Emojis with skin tone modifiers are stored like this
|
||||
if (Object.hasOwn(emoji, '1')) {
|
||||
emoji = emoji['1'];
|
||||
}
|
||||
|
||||
const { native } = emoji;
|
||||
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||
|
||||
if (short_names[0] !== key) {
|
||||
throw new Error(
|
||||
'The compressor expects the first short_code to be the ' +
|
||||
'key. It may need to be rewritten if the emoji change such that this ' +
|
||||
'is no longer the case.',
|
||||
);
|
||||
}
|
||||
|
||||
short_names = short_names.slice(1); // first short name can be inferred from the key
|
||||
|
||||
const searchData = [native, short_names, search];
|
||||
|
||||
if (unicodeToUnifiedName(native) !== unified) {
|
||||
// unified name can't be derived from unicodeToUnifiedName
|
||||
searchData.push(unified);
|
||||
}
|
||||
|
||||
if (!Array.isArray(shortCodesToEmojiData[key])) {
|
||||
shortCodesToEmojiData[key] = [[]];
|
||||
}
|
||||
|
||||
shortCodesToEmojiData[key].push(searchData);
|
||||
});
|
||||
|
||||
// JSON.parse/stringify is to emulate what @preval is doing and avoid any
|
||||
// inconsistent behavior in dev mode
|
||||
export default JSON.parse(
|
||||
JSON.stringify([
|
||||
shortCodesToEmojiData,
|
||||
/*
|
||||
* The property `skins` is not found in the current context.
|
||||
* This could potentially lead to issues when interacting with modules or data structures
|
||||
* that expect the presence of `skins` property.
|
||||
* Currently, no definitions or references to `skins` property can be found in:
|
||||
* - {@link node_modules/emoji-mart/dist/utils/data.js}
|
||||
* - {@link node_modules/emoji-mart/data/all.json}
|
||||
* - {@link app/javascript/flavours/glitch/features/emoji/emoji_compressed.d.ts#Skins}
|
||||
* Future refactorings or updates should consider adding definitions or handling for `skins` property.
|
||||
*/
|
||||
emojiMartData.skins,
|
||||
emojiMartData.categories,
|
||||
emojiMartData.aliases,
|
||||
emojisWithoutShortCodes,
|
||||
]),
|
||||
);
|
||||
File diff suppressed because one or more lines are too long
@ -1,50 +0,0 @@
|
||||
// The output of this module is designed to mimic emoji-mart's
|
||||
// "data" object, such that we can use it for a light version of emoji-mart's
|
||||
// emojiIndex.search functionality.
|
||||
import type { BaseEmoji } from 'emoji-mart';
|
||||
import type { Emoji } from 'emoji-mart/dist-es/utils/data';
|
||||
import emojiCompressed from 'virtual:mastodon-emoji-compressed';
|
||||
import type {
|
||||
Search,
|
||||
ShortCodesToEmojiData,
|
||||
} from 'virtual:mastodon-emoji-compressed';
|
||||
|
||||
import { unicodeToUnifiedName } from './unicode_utils';
|
||||
|
||||
type Emojis = Record<
|
||||
NonNullable<keyof ShortCodesToEmojiData>,
|
||||
{
|
||||
native: BaseEmoji['native'];
|
||||
search: Search;
|
||||
short_names: Emoji['short_names'];
|
||||
unified: Emoji['unified'];
|
||||
}
|
||||
>;
|
||||
|
||||
const [
|
||||
shortCodesToEmojiData,
|
||||
_skins,
|
||||
categories,
|
||||
short_names,
|
||||
_emojisWithoutShortCodes,
|
||||
] = emojiCompressed;
|
||||
|
||||
const emojis: Emojis = {};
|
||||
|
||||
// decompress
|
||||
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
||||
const emojiData = shortCodesToEmojiData[shortCode];
|
||||
if (!emojiData) return;
|
||||
|
||||
const [_filenameData, searchData] = emojiData;
|
||||
const [native, short_names, search, unified] = searchData;
|
||||
|
||||
emojis[shortCode] = {
|
||||
native,
|
||||
search,
|
||||
short_names: short_names ? [shortCode].concat(short_names) : undefined,
|
||||
unified: unified ?? unicodeToUnifiedName(native),
|
||||
};
|
||||
});
|
||||
|
||||
export { emojis, categories, short_names };
|
||||
@ -1,185 +0,0 @@
|
||||
// This code is largely borrowed from:
|
||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
|
||||
|
||||
import { emojis, categories } from './emoji_mart_data_light';
|
||||
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
|
||||
|
||||
let originalPool = {};
|
||||
let index = {};
|
||||
let emojisList = {};
|
||||
let emoticonsList = {};
|
||||
let customEmojisList = [];
|
||||
|
||||
for (let emoji in emojis) {
|
||||
let emojiData = emojis[emoji];
|
||||
let { short_names, emoticons } = emojiData;
|
||||
let id = short_names[0];
|
||||
|
||||
if (emoticons) {
|
||||
emoticons.forEach(emoticon => {
|
||||
if (emoticonsList[emoticon]) {
|
||||
return;
|
||||
}
|
||||
|
||||
emoticonsList[emoticon] = id;
|
||||
});
|
||||
}
|
||||
|
||||
emojisList[id] = getSanitizedData(id);
|
||||
originalPool[id] = emojiData;
|
||||
}
|
||||
|
||||
function clearCustomEmojis(pool) {
|
||||
customEmojisList.forEach((emoji) => {
|
||||
let emojiId = emoji.id || emoji.short_names[0];
|
||||
|
||||
delete pool[emojiId];
|
||||
delete emojisList[emojiId];
|
||||
});
|
||||
}
|
||||
|
||||
function addCustomToPool(custom, pool) {
|
||||
if (customEmojisList.length) clearCustomEmojis(pool);
|
||||
|
||||
custom.forEach((emoji) => {
|
||||
let emojiId = emoji.id || emoji.short_names[0];
|
||||
|
||||
if (emojiId && !pool[emojiId]) {
|
||||
pool[emojiId] = getData(emoji);
|
||||
emojisList[emojiId] = getSanitizedData(emoji);
|
||||
}
|
||||
});
|
||||
|
||||
customEmojisList = custom;
|
||||
index = {};
|
||||
}
|
||||
|
||||
function search(value, { emojisToShowFilter, maxResults, include, exclude, custom } = {}) {
|
||||
if (custom !== undefined) {
|
||||
if (customEmojisList !== custom)
|
||||
addCustomToPool(custom, originalPool);
|
||||
} else {
|
||||
custom = [];
|
||||
}
|
||||
|
||||
maxResults = maxResults || 75;
|
||||
include = include || [];
|
||||
exclude = exclude || [];
|
||||
|
||||
let results = null,
|
||||
pool = originalPool;
|
||||
|
||||
if (value.length) {
|
||||
if (value === '-' || value === '-1') {
|
||||
return [emojisList['-1']];
|
||||
}
|
||||
|
||||
let values = value.toLowerCase().split(/[\s|,\-_]+/),
|
||||
allResults = [];
|
||||
|
||||
if (values.length > 2) {
|
||||
values = [values[0], values[1]];
|
||||
}
|
||||
|
||||
if (include.length || exclude.length) {
|
||||
pool = {};
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
category.emojis.forEach(emojiId => pool[emojiId] = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const searchValue = (value) => {
|
||||
let aPool = pool,
|
||||
aIndex = index,
|
||||
length = 0;
|
||||
|
||||
for (let charIndex = 0; charIndex < value.length; charIndex++) {
|
||||
const char = value[charIndex];
|
||||
length++;
|
||||
|
||||
aIndex[char] = aIndex[char] || {};
|
||||
aIndex = aIndex[char];
|
||||
|
||||
if (!aIndex.results) {
|
||||
let scores = {};
|
||||
|
||||
aIndex.results = [];
|
||||
aIndex.pool = {};
|
||||
|
||||
for (let id in aPool) {
|
||||
let emoji = aPool[id],
|
||||
{ search } = emoji,
|
||||
sub = value.slice(0, length),
|
||||
subIndex = search.indexOf(sub);
|
||||
|
||||
if (subIndex !== -1) {
|
||||
let score = subIndex + 1;
|
||||
if (sub === id) score = 0;
|
||||
|
||||
aIndex.results.push(emojisList[id]);
|
||||
aIndex.pool[id] = emoji;
|
||||
|
||||
scores[id] = score;
|
||||
}
|
||||
}
|
||||
|
||||
aIndex.results.sort((a, b) => {
|
||||
let aScore = scores[a.id],
|
||||
bScore = scores[b.id];
|
||||
|
||||
return aScore - bScore;
|
||||
});
|
||||
}
|
||||
|
||||
aPool = aIndex.pool;
|
||||
}
|
||||
|
||||
return aIndex.results;
|
||||
};
|
||||
|
||||
if (values.length > 1) {
|
||||
results = searchValue(value);
|
||||
} else {
|
||||
results = [];
|
||||
}
|
||||
|
||||
allResults = values.map(searchValue).filter(a => a);
|
||||
|
||||
if (allResults.length > 1) {
|
||||
allResults = intersect.apply(null, allResults);
|
||||
} else if (allResults.length) {
|
||||
allResults = allResults[0];
|
||||
}
|
||||
|
||||
results = uniq(results.concat(allResults));
|
||||
}
|
||||
|
||||
if (results) {
|
||||
if (emojisToShowFilter) {
|
||||
results = results.filter((result) => emojisToShowFilter(emojis[result.id]));
|
||||
}
|
||||
|
||||
if (results && results.length > maxResults) {
|
||||
results = results.slice(0, maxResults);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export { search };
|
||||
@ -1,217 +0,0 @@
|
||||
// This code is largely borrowed from:
|
||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js
|
||||
|
||||
import * as data from './emoji_mart_data_light';
|
||||
|
||||
const buildSearch = (data) => {
|
||||
const search = [];
|
||||
|
||||
let addToSearch = (strings, split) => {
|
||||
if (!strings) {
|
||||
return;
|
||||
}
|
||||
|
||||
(Array.isArray(strings) ? strings : [strings]).forEach((string) => {
|
||||
(split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => {
|
||||
s = s.toLowerCase();
|
||||
|
||||
if (search.indexOf(s) === -1) {
|
||||
search.push(s);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addToSearch(data.short_names, true);
|
||||
addToSearch(data.name, true);
|
||||
addToSearch(data.keywords, false);
|
||||
addToSearch(data.emoticons, false);
|
||||
|
||||
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 stringFromCodePoint.apply(null, codePoints);
|
||||
}
|
||||
|
||||
function sanitize(emoji) {
|
||||
let { name, short_names, skin_tone, skin_variations, emoticons, unified, custom, imageUrl } = emoji,
|
||||
id = emoji.id || short_names[0],
|
||||
colons = `:${id}:`;
|
||||
|
||||
if (custom) {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
colons,
|
||||
emoticons,
|
||||
custom,
|
||||
imageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (skin_tone) {
|
||||
colons += `:skin-tone-${skin_tone}:`;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
colons,
|
||||
emoticons,
|
||||
unified: unified.toLowerCase(),
|
||||
skin: skin_tone || (skin_variations ? 1 : null),
|
||||
native: unifiedToNative(unified),
|
||||
};
|
||||
}
|
||||
|
||||
function getSanitizedData() {
|
||||
return sanitize(getData(...arguments));
|
||||
}
|
||||
|
||||
function getData(emoji, skin, set) {
|
||||
let emojiData = {};
|
||||
|
||||
if (typeof emoji === 'string') {
|
||||
let matches = emoji.match(COLONS_REGEX);
|
||||
|
||||
if (matches) {
|
||||
emoji = matches[1];
|
||||
|
||||
if (matches[2]) {
|
||||
skin = parseInt(matches[2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data.short_names, emoji)) {
|
||||
emoji = data.short_names[emoji];
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data.emojis, emoji)) {
|
||||
emojiData = data.emojis[emoji];
|
||||
}
|
||||
} else if (emoji.id) {
|
||||
if (Object.hasOwn(data.short_names, emoji.id)) {
|
||||
emoji.id = data.short_names[emoji.id];
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data.emojis, 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.unified = emojiData.variations.shift();
|
||||
}
|
||||
|
||||
return emojiData;
|
||||
}
|
||||
|
||||
function uniq(arr) {
|
||||
return arr.reduce((acc, item) => {
|
||||
if (acc.indexOf(item) === -1) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function intersect(a, b) {
|
||||
const uniqA = uniq(a);
|
||||
const uniqB = uniq(b);
|
||||
|
||||
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
|
||||
}
|
||||
|
||||
export {
|
||||
getData,
|
||||
getSanitizedData,
|
||||
uniq,
|
||||
intersect,
|
||||
};
|
||||
@ -75,6 +75,36 @@ export async function fetchCustomEmojiData() {
|
||||
return customEmojis;
|
||||
}
|
||||
|
||||
type LegacyEmoji =
|
||||
| { id: string; custom?: false; native: string }
|
||||
| {
|
||||
id: string;
|
||||
custom: true;
|
||||
};
|
||||
|
||||
// Replicates the old legacy search function.
|
||||
export async function emojiMartSearch(
|
||||
token: string,
|
||||
locale: string,
|
||||
limit = 5,
|
||||
): Promise<LegacyEmoji[]> {
|
||||
const query = token.replace(':', '').toLowerCase().trim();
|
||||
if (!query.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { search } = await import('./database');
|
||||
const results = await search({ query, locale, limit });
|
||||
return results.map((emoji) =>
|
||||
'shortcode' in emoji
|
||||
? { id: emoji.shortcode, custom: true }
|
||||
: {
|
||||
id: emoji.label.replaceAll(' ', '_').toLowerCase(),
|
||||
native: emoji.unicode,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function usePickerEmojis() {
|
||||
const [, setLoaded] = useState(customEmojis !== null);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user