diff --git a/bun.lock b/bun.lock index ddf37bf..57600cc 100644 --- a/bun.lock +++ b/bun.lock @@ -32,6 +32,8 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "embla-carousel-vue": "^8.6.0", + "emojibase": "^16.0.0", + "emojibase-data": "^16.0.3", "fuzzysort": "^3.1.0", "html-to-text": "^9.0.5", "lucide-vue-next": "^0.511.0", @@ -1178,6 +1180,10 @@ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "emojibase": ["emojibase@16.0.0", "", {}, "sha512-Nw2m7JLIO4Ou2X/yZPRNscHQXVbbr6SErjkJ7EooG7MbR3yDZszCv9KTizsXFc7yZl0n3WF+qUKIC/Lw6H9xaQ=="], + + "emojibase-data": ["emojibase-data@16.0.3", "", { "peerDependencies": { "emojibase": "*" } }, "sha512-MopInVCDZeXvqBMPJxnvYUyKw9ImJZqIDr2sABo6acVSPev5IDYX+mf+0tsu96JJyc3INNvgIf06Eso7bdTX2Q=="], + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], diff --git a/components/composer/composer.vue b/components/composer/composer.vue index 0e419aa..70fbcf4 100644 --- a/components/composer/composer.vue +++ b/components/composer/composer.vue @@ -1,5 +1,5 @@ diff --git a/components/notes/reactions/picker/display.vue b/components/notes/reactions/picker/display.vue new file mode 100644 index 0000000..bb9347e --- /dev/null +++ b/components/notes/reactions/picker/display.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/notes/reactions/picker/emoji.ts b/components/notes/reactions/picker/emoji.ts new file mode 100644 index 0000000..7f38036 --- /dev/null +++ b/components/notes/reactions/picker/emoji.ts @@ -0,0 +1,140 @@ +/** + * Adapted from Cinny's code + * @see https://github.com/cinnyapp/cinny/blob/e6f4eeca8edc85ab64179e545b4e2e8c15763633/src/app/plugins/emoji.ts + */ + +import type { CompactEmoji } from "emojibase"; +import emojisData from "emojibase-data/en/compact.json"; +import joypixels from "emojibase-data/en/shortcodes/joypixels.json"; + +export type UnicodeEmoji = CompactEmoji & { + shortcode: string; +}; + +export enum EmojiGroupId { + People = "People", + Nature = "Nature", + Food = "Food", + Activity = "Activity", + Travel = "Travel", + Object = "Object", + Symbol = "Symbol", + Flag = "Flag", +} + +export type UnicodeEmojiGroup = { + id: EmojiGroupId; + order: number; + emojis: UnicodeEmoji[]; +}; + +export const getShortcodesFor = ( + hexcode: string, +): string[] | string | undefined => joypixels[hexcode]; + +export const getShortcodeFor = (hexcode: string): string | undefined => { + const shortcode = joypixels[hexcode]; + return Array.isArray(shortcode) ? shortcode[0] : shortcode; +}; + +export const emojiGroups: UnicodeEmojiGroup[] = [ + { + id: EmojiGroupId.People, + order: 0, + emojis: [], + }, + { + id: EmojiGroupId.Nature, + order: 1, + emojis: [], + }, + { + id: EmojiGroupId.Food, + order: 2, + emojis: [], + }, + { + id: EmojiGroupId.Activity, + order: 3, + emojis: [], + }, + { + id: EmojiGroupId.Travel, + order: 4, + emojis: [], + }, + { + id: EmojiGroupId.Object, + order: 5, + emojis: [], + }, + { + id: EmojiGroupId.Symbol, + order: 6, + emojis: [], + }, + { + id: EmojiGroupId.Flag, + order: 7, + emojis: [], + }, +]; + +export const emojis: UnicodeEmoji[] = []; + +function addEmojiToGroup(groupIndex: number, emoji: UnicodeEmoji) { + emojiGroups[groupIndex]?.emojis.push(emoji); +} + +function getGroupIndex(emoji: UnicodeEmoji): number | undefined { + switch (emoji.group) { + case 0: + case 1: + return 0; // People + case 3: + return 1; // Nature + case 4: + return 2; // Food + case 6: + return 3; // Activity + case 5: + return 4; // Travel + case 7: + return 5; // Object + case 8: + case undefined: + return 6; // Symbol + case 9: + return 7; // Flag + default: + return undefined; // Unknown group + } +} + +for (const emoji of emojisData) { + const myShortCodes = getShortcodesFor(emoji.hexcode); + + if (!myShortCodes) { + continue; + } + if (Array.isArray(myShortCodes) && myShortCodes.length === 0) { + continue; + } + + const em: UnicodeEmoji = { + ...emoji, + shortcode: Array.isArray(myShortCodes) + ? (myShortCodes[0] as string) + : myShortCodes, + shortcodes: Array.isArray(myShortCodes) + ? myShortCodes + : emoji.shortcodes, + }; + + const groupIndex = getGroupIndex(em); + + if (groupIndex !== undefined) { + addEmojiToGroup(groupIndex, em); + emojis.push(em); + } +} diff --git a/components/notes/reactions/picker/emoji.vue b/components/notes/reactions/picker/emoji.vue new file mode 100644 index 0000000..f06a1f5 --- /dev/null +++ b/components/notes/reactions/picker/emoji.vue @@ -0,0 +1,27 @@ + + + diff --git a/components/notes/reactions/picker/index.vue b/components/notes/reactions/picker/index.vue new file mode 100644 index 0000000..212e305 --- /dev/null +++ b/components/notes/reactions/picker/index.vue @@ -0,0 +1,138 @@ + + + diff --git a/components/notes/reactions/picker/sidebar.vue b/components/notes/reactions/picker/sidebar.vue new file mode 100644 index 0000000..2fffa4f --- /dev/null +++ b/components/notes/reactions/picker/sidebar.vue @@ -0,0 +1,47 @@ + + + diff --git a/components/notes/reactions/picker/virtual.ts b/components/notes/reactions/picker/virtual.ts new file mode 100644 index 0000000..40f52ff --- /dev/null +++ b/components/notes/reactions/picker/virtual.ts @@ -0,0 +1,117 @@ +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 | UnicodeEmoji)[]; + }; + +export const getVirtualizedItems = ( + customCategories: Record[]>, + 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; +}; diff --git a/components/notes/reactions/reaction.vue b/components/notes/reactions/reaction.vue index 3f58174..6bf9784 100644 --- a/components/notes/reactions/reaction.vue +++ b/components/notes/reactions/reaction.vue @@ -1,7 +1,7 @@