Files
outline/shared/utils/emoji.ts
2024-06-23 06:31:18 -07:00

137 lines
3.6 KiB
TypeScript

import RawData from "@emoji-mart/data";
import type { EmojiMartData, Skin } from "@emoji-mart/data";
import { init, Data } from "emoji-mart";
import FuzzySearch from "fuzzy-search";
import capitalize from "lodash/capitalize";
import sortBy from "lodash/sortBy";
import { Emoji, EmojiCategory, EmojiSkinTone, EmojiVariants } from "../types";
import { isMac } from "./browser";
const isMacEnv = isMac();
init({ data: RawData });
// Data has the pre-processed "search" terms.
const TypedData = Data as EmojiMartData;
const flagEmojiIds =
TypedData.categories
.filter(({ id }) => id === EmojiCategory.Flags.toLowerCase())
.map(({ emojis }) => emojis)[0] ?? [];
const Categories = TypedData.categories.filter(
({ id }) => isMacEnv || capitalize(id) !== EmojiCategory.Flags
);
const Emojis = Object.fromEntries(
Object.entries(TypedData.emojis).filter(
([id]) => isMacEnv || !flagEmojiIds.includes(id)
)
);
const searcher = new FuzzySearch(Object.values(Emojis), ["search"], {
caseSensitive: false,
sort: true,
});
// Codes defined by unicode.org
const SKINTONE_CODE_TO_ENUM = {
"1f3fb": EmojiSkinTone.Light,
"1f3fc": EmojiSkinTone.MediumLight,
"1f3fd": EmojiSkinTone.Medium,
"1f3fe": EmojiSkinTone.MediumDark,
"1f3ff": EmojiSkinTone.Dark,
};
type GetVariantsProps = {
id: string;
name: string;
skins: Skin[];
};
const getVariants = ({ id, name, skins }: GetVariantsProps): EmojiVariants =>
skins.reduce((obj, skin) => {
const skinToneCode = skin.unified.split("-")[1];
const skinToneType =
SKINTONE_CODE_TO_ENUM[skinToneCode] ?? EmojiSkinTone.Default;
obj[skinToneType] = { id, name, value: skin.native } satisfies Emoji;
return obj;
}, {} as EmojiVariants);
const EMOJI_ID_TO_VARIANTS = Object.entries(Emojis).reduce(
(obj, [id, emoji]) => {
obj[id] = getVariants({
id,
name: emoji.name,
skins: emoji.skins,
});
return obj;
},
{} as Record<string, EmojiVariants>
);
const CATEGORY_TO_EMOJI_IDS: Record<EmojiCategory, string[]> =
Categories.reduce((obj, { id, emojis }) => {
const category = EmojiCategory[capitalize(id)];
if (!category) {
return obj;
}
obj[category] = emojis;
return obj;
}, {} as Record<EmojiCategory, string[]>);
export const getEmojis = ({
ids,
skinTone,
}: {
ids: string[];
skinTone: EmojiSkinTone;
}): Emoji[] =>
ids.map(
(id) =>
EMOJI_ID_TO_VARIANTS[id][skinTone] ??
EMOJI_ID_TO_VARIANTS[id][EmojiSkinTone.Default]
);
export const getEmojisWithCategory = ({
skinTone,
}: {
skinTone: EmojiSkinTone;
}): Record<EmojiCategory, Emoji[]> =>
Object.keys(CATEGORY_TO_EMOJI_IDS).reduce((obj, category: EmojiCategory) => {
const emojiIds = CATEGORY_TO_EMOJI_IDS[category];
const emojis = emojiIds.map(
(emojiId) =>
EMOJI_ID_TO_VARIANTS[emojiId][skinTone] ??
EMOJI_ID_TO_VARIANTS[emojiId][EmojiSkinTone.Default]
);
obj[category] = emojis;
return obj;
}, {} as Record<EmojiCategory, Emoji[]>);
export const getEmojiVariants = ({ id }: { id: string }) =>
EMOJI_ID_TO_VARIANTS[id];
export const search = ({
query,
skinTone,
}: {
query: string;
skinTone?: EmojiSkinTone;
}) => {
const queryLowercase = query.toLowerCase();
const emojiSkinTone = skinTone ?? EmojiSkinTone.Default;
const matchedEmojis = searcher
.search(queryLowercase)
.map(
(emoji) =>
EMOJI_ID_TO_VARIANTS[emoji.id][emojiSkinTone] ??
EMOJI_ID_TO_VARIANTS[emoji.id][EmojiSkinTone.Default]
);
return sortBy(matchedEmojis, (emoji) => {
const nlc = emoji.name.toLowerCase();
return query === nlc ? -1 : nlc.startsWith(queryLowercase) ? 0 : 1;
});
};