diff --git a/app/editor/components/EmojiMenu.tsx b/app/editor/components/EmojiMenu.tsx index d6cd4f8967..524f5c96af 100644 --- a/app/editor/components/EmojiMenu.tsx +++ b/app/editor/components/EmojiMenu.tsx @@ -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([]); + 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; diff --git a/shared/utils/emoji.ts b/shared/utils/emoji.ts index 2a3bdc82f7..16e9a5ed25 100644 --- a/shared/utils/emoji.ts +++ b/shared/utils/emoji.ts @@ -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 = {} as Record ); -export const getCustomEmojis = async ( - search: string -): Promise => { - 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[]; }; /**