mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
use store methods for custom emoji search
This commit is contained in:
@@ -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
@@ -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[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user