use store methods for custom emoji search

This commit is contained in:
Salihu
2025-11-18 00:56:45 +01:00
parent ef2ea41d63
commit 3aad383a12
2 changed files with 73 additions and 93 deletions
+48 -27
View File
@@ -8,6 +8,7 @@ import SuggestionsMenu, {
Props as SuggestionsMenuProps,
} from "./SuggestionsMenu";
import { isInternalUrl } from "@shared/utils/urls";
import useStores from "~/hooks/useStores";
type Emoji = {
name: string;
@@ -25,37 +26,36 @@ type Props = Omit<
const EmojiMenu = (props: Props) => {
const { search = "" } = props;
const [items, setItems] = useState<Emoji[]>([]);
const { emojis } = useStores();
useEffect(() => {
const setEmojiItems = (results: ShortEmojiType[]) => {
const mappedItems = results
.map((item) => {
// We snake_case the shortcode for backwards compatability with gemoji to
// avoid multiple formats being written into documents.
// @ts-expect-error emojiMartToGemoji key
const shortcode = snakeCase(emojiMartToGemoji[item.id] || item.id);
const emoji = item.value;
return {
name: "emoji",
title: emoji,
description: capitalize(item.name.toLowerCase()),
emoji,
attrs: {
markup: shortcode,
"data-name": !isInternalUrl(emoji) ? shortcode : item.name,
"data-url": isInternalUrl(emoji) ? emoji : undefined,
},
};
})
.slice(0, 15);
setItems(mappedItems);
const updateItems = (results: ShortEmojiType[]) => {
setItems(results.map(toMenuItem).slice(0, 15));
};
const results = emojiSearch({ query: search, onUpdate: setEmojiItems });
setEmojiItems(results);
}, [search]);
// search through regular emojis
const localResults = emojiSearch({ query: search });
updateItems(localResults);
// Fetch and merge custom emojis
emojis.fetchPage({ query: search }).then((serverData) => {
if (!serverData.length) {return;}
const customEmojis = serverData.map((e) => ({
id: e.id,
name: e.name,
search: e.name,
value: e.url,
}));
const mergedResults = emojiSearch({
query: search,
emojis: customEmojis,
});
updateItems(mergedResults);
});
}, [search, emojis]);
const renderMenuItem = useCallback(
(item, _index, options) => (
@@ -79,4 +79,25 @@ const EmojiMenu = (props: Props) => {
);
};
const toMenuItem = (item: ShortEmojiType): Emoji => {
// We snake_case the shortcode for backwards compatability with gemoji to
// avoid multiple formats being written into documents.
// @ts-expect-error emojiMartToGemoji key
const shortcode = snakeCase(emojiMartToGemoji[item.id] || item.id);
const emoji = item.value;
const isCustom = isInternalUrl(emoji);
return {
name: "emoji",
title: emoji,
description: capitalize(item.name.toLowerCase()),
emoji,
attrs: {
markup: shortcode,
"data-name": isCustom ? item.name : shortcode,
"data-url": isCustom ? emoji : undefined,
},
};
};
export default EmojiMenu;
+25 -66
View File
@@ -1,16 +1,10 @@
import RawData from "@emoji-mart/data";
import type {
EmojiMartData,
Skin,
Emoji as EmojiMartType,
} 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 { client } from "~/utils/ApiClient";
import Logger from "~/utils/Logger";
init({ data: RawData });
@@ -105,7 +99,7 @@ const Emojis = allowFlagEmoji
)
);
const searcher = (emojis: EmojiMartType[]) =>
const searcher = (emojis: searchEmojis[]) =>
new FuzzySearch(emojis, ["search"], {
caseSensitive: false,
sort: true,
@@ -163,23 +157,6 @@ const CATEGORY_TO_EMOJI_IDS: Record<EmojiCategory, string[]> =
{} as Record<EmojiCategory, string[]>
);
export const getCustomEmojis = async (
search: string
): Promise<EmojiMartType[] | null> => {
try {
const response = await client.post("/emojis.list", { query: search });
return response.data.map((d: any) => ({
id: d.id,
name: d.name,
search: d.name,
value: d.url,
}));
} catch (error) {
Logger.error("Failed to fetch custom emojis:", error);
return null;
}
};
export const getEmojis = ({
ids,
skinTone,
@@ -215,57 +192,39 @@ export const getEmojisWithCategory = ({
export const getEmojiVariants = ({ id }: { id: string }) =>
EMOJI_ID_TO_VARIANTS[id];
type searchEmojis = Emoji & {
search?: string;
skins?: Skin[];
};
export const search = ({
emojis = [],
query,
skinTone,
onUpdate,
}: {
emojis?: searchEmojis[];
query: string;
skinTone?: EmojiSkinTone;
onUpdate?: (results: any[]) => void;
}): Emoji[] => {
const queryLowercase = query.toLowerCase();
const emojiSkinTone = skinTone ?? EmojiSkinTone.Default;
const processEmojis = (emojis: EmojiMartType[]) => {
const matchedEmojis = searcher(emojis)
.search(queryLowercase)
.map((emoji) => {
if (!emoji.skins) {
return emoji;
}
return (
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;
});
};
// Return standard emojis immediately
const standardResults = processEmojis(Object.values(Emojis));
// Load custom emojis asynchronously and update results
getCustomEmojis(query)
.then((customEmojis) => {
if (customEmojis) {
const combinedResults = processEmojis([
...Object.values(Emojis),
...customEmojis,
]);
onUpdate?.(combinedResults);
const matchedEmojis = searcher([...Object.values(Emojis), ...emojis])
.search(query.toLowerCase())
.map((emoji) => {
if (!emoji.skins) {
return emoji;
}
})
.catch((error) => {
Logger.error("Failed to load custom emojis:", error);
const emojiSkinTone = skinTone ?? EmojiSkinTone.Default;
return (
EMOJI_ID_TO_VARIANTS[emoji.id][emojiSkinTone] ??
EMOJI_ID_TO_VARIANTS[emoji.id][EmojiSkinTone.Default]
);
});
return standardResults as Emoji[];
return sortBy(matchedEmojis, (emoji) => {
const nlc = emoji.name.toLowerCase();
return query === nlc ? -1 : nlc.startsWith(query) ? 0 : 1;
}) as Emoji[];
};
/**