diff --git a/.storybook/main.ts b/.storybook/main.ts index c249d1c06d..2f70c80dbf 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -27,6 +27,7 @@ const config: StorybookConfig = { 'oops.gif', 'oops.png', ].map((path) => ({ from: `../public/${path}`, to: `/${path}` })), + { from: '../app/javascript/images/logo.svg', to: '/custom-emoji/logo.svg' }, ], viteFinal(config) { // For an unknown reason, Storybook does not use the root diff --git a/app/javascript/mastodon/components/emoji/emoji.stories.tsx b/app/javascript/mastodon/components/emoji/emoji.stories.tsx index fcc81db6c3..e7d1887aa9 100644 --- a/app/javascript/mastodon/components/emoji/emoji.stories.tsx +++ b/app/javascript/mastodon/components/emoji/emoji.stories.tsx @@ -2,6 +2,9 @@ import type { ComponentProps } from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; +import { customEmojiFactory } from '@/testing/factories'; + +import { CustomEmojiProvider } from './context'; import { Emoji } from './index'; type EmojiProps = ComponentProps & { @@ -34,7 +37,11 @@ const meta = { }, }, render(args) { - return ; + return ( + + + + ); }, } satisfies Meta; @@ -49,9 +56,3 @@ export const CustomEmoji: Story = { code: ':custom:', }, }; - -export const LegacyEmoji: Story = { - args: { - code: ':copyright:', - }, -}; diff --git a/app/javascript/mastodon/features/emoji/render.test.ts b/app/javascript/mastodon/features/emoji/render.test.ts index 782148b36e..dffebd1f8c 100644 --- a/app/javascript/mastodon/features/emoji/render.test.ts +++ b/app/javascript/mastodon/features/emoji/render.test.ts @@ -83,12 +83,8 @@ describe('stringToEmojiState', () => { }); }); - test('returns custom emoji state for valid custom emoji', () => { - expect(stringToEmojiState(':smile:')).toEqual({ - type: 'custom', - code: 'smile', - data: undefined, - }); + test('returns null for custom emoji without data', () => { + expect(stringToEmojiState(':smile:')).toBeNull(); }); test('returns custom emoji state with data when provided', () => { @@ -108,7 +104,6 @@ describe('stringToEmojiState', () => { test('returns null for invalid emoji strings', () => { expect(stringToEmojiState('notanemoji')).toBeNull(); - expect(stringToEmojiState(':invalid-emoji:')).toBeNull(); }); }); @@ -142,21 +137,13 @@ describe('loadEmojiDataToState', () => { }); }); - test('loads custom emoji data into state', async () => { - const dbCall = vi - .spyOn(db, 'loadCustomEmojiByShortcode') - .mockResolvedValueOnce(customEmojiFactory()); + test('returns null for custom emoji without data', async () => { const customState = { type: 'custom', code: 'smile', } as const satisfies EmojiStateCustom; const result = await loadEmojiDataToState(customState, 'en'); - expect(dbCall).toHaveBeenCalledWith('smile'); - expect(result).toEqual({ - type: 'custom', - code: 'smile', - data: customEmojiFactory(), - }); + expect(result).toBeNull(); }); test('loads unicode data using legacy shortcode', async () => { @@ -194,16 +181,6 @@ describe('loadEmojiDataToState', () => { expect(result).toBeNull(); }); - test('returns null if custom emoji not found in database', async () => { - vi.spyOn(db, 'loadCustomEmojiByShortcode').mockResolvedValueOnce(undefined); - const customState = { - type: 'custom', - code: 'smile', - } as const satisfies EmojiStateCustom; - const result = await loadEmojiDataToState(customState, 'en'); - expect(result).toBeNull(); - }); - test('retries loading emoji data once if initial load fails', async () => { const dbCall = vi .spyOn(db, 'loadEmojiByHexcode') diff --git a/app/javascript/mastodon/features/emoji/render.ts b/app/javascript/mastodon/features/emoji/render.ts index e00525fe0a..38bc7fd7e5 100644 --- a/app/javascript/mastodon/features/emoji/render.ts +++ b/app/javascript/mastodon/features/emoji/render.ts @@ -5,7 +5,6 @@ import { EMOJI_TYPE_CUSTOM, } from './constants'; import { - loadCustomEmojiByShortcode, loadEmojiByHexcode, loadLegacyShortcodesByShortcode, LocaleNotLoadedError, @@ -80,7 +79,7 @@ export function tokenizeText(text: string): TokenizedText { export function stringToEmojiState( code: string, customEmoji: ExtraCustomEmojiMap = {}, -): EmojiState | null { +): EmojiStateUnicode | Required | null { if (isUnicodeEmoji(code)) { return { type: EMOJI_TYPE_UNICODE, @@ -90,11 +89,13 @@ export function stringToEmojiState( if (isCustomEmoji(code)) { const shortCode = code.slice(1, -1); - return { - type: EMOJI_TYPE_CUSTOM, - code: shortCode, - data: customEmoji[shortCode], - }; + if (customEmoji[shortCode]) { + return { + type: EMOJI_TYPE_CUSTOM, + code: shortCode, + data: customEmoji[shortCode], + }; + } } return null; @@ -115,33 +116,29 @@ export async function loadEmojiDataToState( return state; } + // Don't try to load data for custom emoji. + if (state.type === EMOJI_TYPE_CUSTOM) { + return null; + } + // First, try to load the data from IndexedDB. try { const legacyCode = await loadLegacyShortcodesByShortcode(state.code); // This is duplicative, but that's because TS can't distinguish the state type easily. - if (state.type === EMOJI_TYPE_UNICODE || legacyCode) { - const data = await loadEmojiByHexcode( - legacyCode?.hexcode ?? state.code, - locale, - ); - if (data) { - return { - ...state, - type: EMOJI_TYPE_UNICODE, - data, - // TODO: Use CLDR shortcodes when the picker supports them. - shortcode: legacyCode?.shortcodes.at(0), - }; - } - } else { - const data = await loadCustomEmojiByShortcode(state.code); - if (data) { - return { - ...state, - data, - }; - } + const data = await loadEmojiByHexcode( + legacyCode?.hexcode ?? state.code, + locale, + ); + if (data) { + return { + ...state, + type: EMOJI_TYPE_UNICODE, + data, + // TODO: Use CLDR shortcodes when the picker supports them. + shortcode: legacyCode?.shortcodes.at(0), + }; } + // If not found, assume it's not an emoji and return null. log( 'Could not find emoji %s of type %s for locale %s', diff --git a/app/javascript/testing/factories.ts b/app/javascript/testing/factories.ts index 72d7abe000..28c0eca809 100644 --- a/app/javascript/testing/factories.ts +++ b/app/javascript/testing/factories.ts @@ -128,8 +128,8 @@ export function customEmojiFactory( ): CustomEmojiData { return { shortcode: 'custom', - static_url: 'emoji/custom/static', - url: 'emoji/custom', + static_url: '/custom-emoji/logo.svg', + url: '/custom-emoji/logo.svg', visible_in_picker: true, ...data, };