frontend/components/notes/reactions/picker/virtual.ts
Jesse Wierzbinski 2b72a2a5ea
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 0s
Deploy to GitHub Pages / build (push) Failing after 0s
Deploy to GitHub Pages / deploy (push) Has been skipped
Docker / build (push) Failing after 0s
Mirror to Codeberg / Mirror (push) Failing after 0s
feat: Add custom emoji picker
2025-05-27 21:45:54 +02:00

118 lines
3.4 KiB
TypeScript

import type { CustomEmoji } from "@versia/client/schemas";
import { go } from "fuzzysort";
import { nanoid } from "nanoid";
import type { z } from "zod";
import { type UnicodeEmoji, emojiGroups } from "./emoji";
export const EMOJI_PER_ROW = 7;
export type VirtualizedItem =
| { headerId: string; type: "header"; name: string; categoryId: string }
| {
rowId: string;
type: "emoji-row";
emojis: (z.infer<typeof CustomEmoji> | UnicodeEmoji)[];
};
export const getVirtualizedItems = (
customCategories: Record<string, z.infer<typeof CustomEmoji>[]>,
searchQuery?: string,
): VirtualizedItem[] => {
const items: VirtualizedItem[] = [];
// Add custom emoji categories first
for (const [categoryName, categoryEmojis] of Object.entries(
customCategories,
)) {
// Add category header
items.push({
headerId: nanoid(),
type: "header",
name: categoryName,
categoryId: `custom-${categoryName}`,
});
// Add emoji rows for this category
for (let i = 0; i < categoryEmojis.length; i += EMOJI_PER_ROW) {
items.push({
rowId: nanoid(),
type: "emoji-row",
emojis: categoryEmojis.slice(i, i + EMOJI_PER_ROW),
});
}
}
// Add unicode emoji groups
for (const group of emojiGroups) {
if (group.emojis.length === 0) {
continue;
}
// Add group header
items.push({
headerId: nanoid(),
type: "header",
name: group.id,
categoryId: group.id,
});
// Add emoji rows for this group
for (let i = 0; i < group.emojis.length; i += EMOJI_PER_ROW) {
items.push({
rowId: nanoid(),
type: "emoji-row",
emojis: group.emojis.slice(i, i + EMOJI_PER_ROW),
});
}
}
// If search query is provided, add extra category for search results
// with emojis that contain the search query in their shortcode
// ordered with fuzzysort
if (searchQuery) {
const customEmojiMatches = Object.values(customCategories)
.flat()
.filter((emoji) =>
emoji.shortcode
.toLowerCase()
.includes(searchQuery.toLowerCase()),
);
const unicodeEmojiMatches = emojiGroups
.flatMap((group) => group.emojis)
.filter((emoji) =>
emoji.shortcode
.toLowerCase()
.includes(searchQuery.toLowerCase()),
);
const results = go(
searchQuery,
[...customEmojiMatches, ...unicodeEmojiMatches],
{
key: "shortcode",
limit: 20,
},
);
items.splice(0, 0, {
headerId: nanoid(),
type: "header",
name: "Search Results",
categoryId: "search-results",
});
for (let i = 0; i < results.length; i += EMOJI_PER_ROW) {
const emojis = results
.slice(i, i + EMOJI_PER_ROW)
.map((result) => result.obj);
items.splice(1 + i / EMOJI_PER_ROW, 0, {
rowId: nanoid(),
type: "emoji-row",
emojis,
});
}
}
return items;
};