From ae308fd3a9a1bcd7b50506e665a2e48e7f7f8f41 Mon Sep 17 00:00:00 2001 From: ChaosExAnima Date: Thu, 13 Nov 2025 14:04:18 +0100 Subject: [PATCH] add emoji list --- .../emoji/picker/emoji-picker.stories.tsx | 13 ++- .../components/emoji/picker/index.tsx | 77 ++++++++++--- .../mastodon/components/emoji/picker/list.tsx | 101 ++++++++++++++++++ .../components/emoji/picker/styles.module.css | 90 ++++++++++++++-- .../mastodon/features/emoji/database.ts | 10 ++ 5 files changed, 265 insertions(+), 26 deletions(-) create mode 100644 app/javascript/mastodon/components/emoji/picker/list.tsx diff --git a/app/javascript/mastodon/components/emoji/picker/emoji-picker.stories.tsx b/app/javascript/mastodon/components/emoji/picker/emoji-picker.stories.tsx index bbd320434d2..980ad61411e 100644 --- a/app/javascript/mastodon/components/emoji/picker/emoji-picker.stories.tsx +++ b/app/javascript/mastodon/components/emoji/picker/emoji-picker.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; +import { action } from 'storybook/actions'; import { importCustomEmojiData, @@ -7,14 +8,18 @@ import { import { MockEmojiPicker } from './index'; +const onSelect = action('emoji selected'); + const meta = { title: 'Components/Emoji/EmojiPicker', - render() { + render(_args, { globals }) { + const locale = typeof globals.locale === 'string' ? globals.locale : 'en'; + void importCustomEmojiData(); - void importEmojiData('en'); - return ; + void importEmojiData(locale); + return ; }, -} satisfies Meta; +} satisfies Meta; export default meta; diff --git a/app/javascript/mastodon/components/emoji/picker/index.tsx b/app/javascript/mastodon/components/emoji/picker/index.tsx index 929a9f7e783..f9fbd4ad202 100644 --- a/app/javascript/mastodon/components/emoji/picker/index.tsx +++ b/app/javascript/mastodon/components/emoji/picker/index.tsx @@ -1,14 +1,13 @@ import { useCallback, useState } from 'react'; import type { FC, MouseEventHandler } from 'react'; +import type { GroupMessage, MessagesDataset } from 'emojibase'; import messages from 'emojibase-data/en/messages.json'; import { loadUnicodeEmojiGroupIcon } from '@/mastodon/features/emoji/database'; import { useEmojiAppState } from '@/mastodon/features/emoji/mode'; -import type { - CustomEmojiData, - UnicodeEmojiData, -} from '@/mastodon/features/emoji/types'; +import type { AnyEmojiData } from '@/mastodon/features/emoji/types'; +import { usePrevious } from '@/mastodon/hooks/usePrevious'; import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; import { Emoji } from '..'; @@ -20,9 +19,24 @@ import { mockCustomEmojis, mockCustomGroups, } from './constants'; +import { PickerGroupList } from './list'; import classes from './styles.module.css'; -export const MockEmojiPicker: FC = () => { +interface MockEmojiPickerProps { + onSelect?: (emojiCode: string) => void; +} + +export const MockEmojiPicker: FC = ({ onSelect }) => { + const handleEmojiSelect = useCallback( + (emoji: AnyEmojiData) => { + if (onSelect) { + const code = + 'unicode' in emoji ? emoji.unicode : `:${emoji.shortcode}:`; + onSelect(code); + } + }, + [onSelect], + ); const handleGroupSelect = useCallback((key: string) => { // eslint-disable-next-line no-console console.log('Selected group:', key); @@ -34,6 +48,24 @@ export const MockEmojiPicker: FC = () => { setShowSettings((prev) => !prev); }, []); + const { currentLocale } = useEmojiAppState(); + // This isn't needed in real life, as the current locale is only set on page load. + const prevLocale = usePrevious(currentLocale); + const [groups, setGroups] = useState([]); + if (prevLocale !== currentLocale) { + // This is messy, but it's just for the mock picker. + import( + `../../../../../../node_modules/emojibase-data/${currentLocale}/messages.json` + ) + .then((module: { default: MessagesDataset }) => { + setGroups(module.default.groups); + }) + .catch((err: unknown) => { + console.warn('fell back to en messages', err); + setGroups(messages.groups); + }); + } + return (
@@ -51,7 +83,26 @@ export const MockEmojiPicker: FC = () => { />
{showSettings &&
Settings here
} - {!showSettings &&
} + {!showSettings && ( +
+ {mockCustomGroups.map((group) => ( + + ))} + {groups.map((group) => ( + + ))} +
+ )}
    {mockCustomGroups.map((group) => ( { /> ))}
  • - {messages.groups.map((group) => ( + {groups.map((group) => ( = ({ onSelect, message, group }) => { [onSelect, group], ); const { currentLocale } = useEmojiAppState(); - const [icon, setIcon] = useState( - () => { - const emoji = mockCustomEmojis.find((emoji) => emoji.category === group); - return emoji ?? null; - }, - ); + const [icon, setIcon] = useState(() => { + const emoji = mockCustomEmojis.find((emoji) => emoji.category === group); + return emoji ?? null; + }); - if (group in groupKeysToNumber) { + if (group in groupKeysToNumber && icon === null) { const groupNum = groupKeysToNumber[group]; if (typeof groupNum !== 'undefined') { void loadUnicodeEmojiGroupIcon(groupNum, currentLocale).then(setIcon); diff --git a/app/javascript/mastodon/components/emoji/picker/list.tsx b/app/javascript/mastodon/components/emoji/picker/list.tsx new file mode 100644 index 00000000000..3b3a680b9d7 --- /dev/null +++ b/app/javascript/mastodon/components/emoji/picker/list.tsx @@ -0,0 +1,101 @@ +import { useCallback, useState } from 'react'; +import type { FC, MouseEventHandler } from 'react'; + +import classNames from 'classnames'; + +import { loadUnicodeEmojiGroup } from '@/mastodon/features/emoji/database'; +import { useEmojiAppState } from '@/mastodon/features/emoji/mode'; +import type { AnyEmojiData } from '@/mastodon/features/emoji/types'; +import ArrowIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react'; + +import { Emoji } from '..'; + +import { groupKeysToNumber, mockCustomEmojis } from './constants'; +import classes from './styles.module.css'; + +interface PickerGroupListProps { + group: string; + name: string; + onSelect: (emoji: AnyEmojiData) => void; +} + +export const PickerGroupList: FC = ({ + group, + name, + onSelect, +}) => { + const [emojis, setEmojis] = useState(() => { + const emojis = mockCustomEmojis.filter((emoji) => emoji.category === group); + return emojis.length > 0 ? emojis : null; + }); + + const { currentLocale } = useEmojiAppState(); + if (group in groupKeysToNumber && emojis === null) { + const groupNum = groupKeysToNumber[group]; + if (typeof groupNum !== 'undefined') { + void loadUnicodeEmojiGroup(groupNum, currentLocale).then(setEmojis); + } + } + + const [isMinimized, setMinimized] = useState(false); + const handleToggleMinimize = useCallback(() => { + setMinimized((prev) => !prev); + }, []); + + // Still loading emojis. + if (emojis === null) { + return null; + } + + return ( +
    +

    + +

    + {!isMinimized && ( +
      + {emojis.map((emoji) => ( + + ))} +
    + )} +
    + ); +}; + +interface PickerListEmojiProps { + emoji: AnyEmojiData; + onClick: (emoji: AnyEmojiData) => void; +} + +const PickerListEmoji: FC = ({ emoji, onClick }) => { + const handleClick: MouseEventHandler = useCallback(() => { + onClick(emoji); + }, [emoji, onClick]); + return ( +
  • + +
  • + ); +}; diff --git a/app/javascript/mastodon/components/emoji/picker/styles.module.css b/app/javascript/mastodon/components/emoji/picker/styles.module.css index 7fca1443aa6..d595c1edfb6 100644 --- a/app/javascript/mastodon/components/emoji/picker/styles.module.css +++ b/app/javascript/mastodon/components/emoji/picker/styles.module.css @@ -49,6 +49,86 @@ overflow: hidden auto; scrollbar-width: thin; padding: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.groupHeader { + font-size: 1rem; + font-weight: bold; + margin-bottom: 0.5rem; + transition: opacity 0.1s ease; + + &:not(.isMinimized):hover { + opacity: 0.7; + } + + button { + appearance: none; + border: none; + background-color: transparent; + padding: 0; + display: flex; + flex-direction: row; + align-items: center; + width: 100%; + } +} + +.groupHeader.isMinimized { + opacity: 0.5; +} + +.groupHeaderArrow { + transition: transform 0.1s ease; + width: 1rem; + height: 1rem; +} + +.isMinimized .groupHeaderArrow { + transform: rotate(-90deg); +} + +.emojiGrid { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 0.5rem; +} + +.listButton, +.groupButton { + aspect-ratio: 1; + padding: 0; + appearance: none; + display: block; + + > * { + pointer-events: none; + } +} + +.listButton { + border: none; + background-color: transparent; + font-size: 1.5rem; + text-align: center; + width: 100%; + border-radius: 5px; + transition: background-color 0.1s ease; + + &:hover { + background-color: gray; + } + + > img { + padding: 5px; + } + + > * { + width: 100%; + height: 100%; + } } .nav { @@ -61,16 +141,10 @@ } .groupButton { - border-radius: 50%; - aspect-ratio: 1; - border: 1px solid gray; width: 2rem; height: 2rem; - padding: 0; - - > * { - pointer-events: none; - } + border-radius: 50%; + border: 1px solid gray; } .separator { diff --git a/app/javascript/mastodon/features/emoji/database.ts b/app/javascript/mastodon/features/emoji/database.ts index 92824cb432b..c86741ce486 100644 --- a/app/javascript/mastodon/features/emoji/database.ts +++ b/app/javascript/mastodon/features/emoji/database.ts @@ -172,6 +172,16 @@ export async function loadLatestEtag(localeString: string) { return etag ?? null; } +export async function loadUnicodeEmojiGroup( + group: number, + localeString: string, +) { + const locale = toLoadedLocale(localeString); + const db = await loadDB(); + const emojis = await db.getAllFromIndex(locale, 'group', group); + return emojis.toSorted(({ order: a = 0 }, { order: b = 0 }) => a - b); +} + export async function loadUnicodeEmojiGroupIcon( group: number, localeString: string,