diff --git a/app/javascript/mastodon/features/account_edit/index.tsx b/app/javascript/mastodon/features/account_edit/index.tsx index a119e8e61da..d3a3c967070 100644 --- a/app/javascript/mastodon/features/account_edit/index.tsx +++ b/app/javascript/mastodon/features/account_edit/index.tsx @@ -17,6 +17,7 @@ import { ToggleField } from '@/mastodon/components/form_fields'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId'; +import { useCustomEmojis } from '@/mastodon/hooks/useCustomEmojis'; import { autoPlayGif } from '@/mastodon/initial_state'; import { fetchProfile, @@ -175,7 +176,7 @@ export const AccountEdit: FC = () => { }, [dispatch, profile?.bot]); // Normally we would use the account emoji, but we want all custom emojis to be available to render after editing. - const emojis = useAppSelector((state) => state.custom_emojis); + const emojis = useCustomEmojis(); const htmlHandlers = useElementHandledLink({ hashtagAccountId: profile?.id, }); diff --git a/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx b/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx index d0f67ddc62c..98324cf4dd2 100644 --- a/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx @@ -1,4 +1,10 @@ -import { forwardRef, useCallback, useImperativeHandle, useState } from 'react'; +import { + forwardRef, + useCallback, + useImperativeHandle, + useMemo, + useState, +} from 'react'; import type { FC, FocusEventHandler } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; @@ -9,6 +15,7 @@ import { closeModal } from '@/mastodon/actions/modal'; import { Button } from '@/mastodon/components/button'; import type { FieldStatus } from '@/mastodon/components/form_fields'; import { EmojiTextInputField } from '@/mastodon/components/form_fields'; +import { useCustomEmojis } from '@/mastodon/hooks/useCustomEmojis'; import { removeField, selectFieldById, @@ -104,11 +111,6 @@ const selectFieldLimits = createAppSelector( const RECOMMENDED_LIMIT = 40; -const selectEmojiCodes = createAppSelector( - [(state) => state.custom_emojis], - (emojis) => emojis.map((emoji) => emoji.get('shortcode')).toArray(), -); - interface ConfirmationMessage { message: string; confirm: string; @@ -143,7 +145,11 @@ export const EditFieldModal = forwardRef< value?: FieldStatus; }>({}); - const customEmojiCodes = useAppSelector(selectEmojiCodes); + const customEmojis = useCustomEmojis(); + const customEmojiCodes = useMemo( + () => Object.keys(customEmojis ?? {}), + [customEmojis], + ); const checkField = useCallback( (value: string): FieldStatus | null => { if (!value.trim()) { diff --git a/app/javascript/mastodon/features/account_edit/modals/fields_reorder_modal.tsx b/app/javascript/mastodon/features/account_edit/modals/fields_reorder_modal.tsx index 8a94c99ac28..60ec040579a 100644 --- a/app/javascript/mastodon/features/account_edit/modals/fields_reorder_modal.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/fields_reorder_modal.tsx @@ -35,6 +35,7 @@ import { CSS } from '@dnd-kit/utilities'; import { CustomEmojiProvider } from '@/mastodon/components/emoji/context'; import { normalizeKey } from '@/mastodon/components/hotkeys/utils'; import { Icon } from '@/mastodon/components/icon'; +import { useCustomEmojis } from '@/mastodon/hooks/useCustomEmojis'; import type { FieldData } from '@/mastodon/reducers/slices/profile_edit'; import { patchProfile, @@ -217,7 +218,7 @@ export const ReorderFieldsModal: FC = ({ onClose }) => { void dispatch(patchProfile({ fields_attributes: newFields })).then(onClose); }, [dispatch, fieldKeys, fields, onClose]); - const emojis = useAppSelector((state) => state.custom_emojis); + const emojis = useCustomEmojis(); return ( // Add a wrapper here in the capture phase, so that it can be intercepted before the window listener in ModalRoot. diff --git a/app/javascript/mastodon/features/emoji/database.ts b/app/javascript/mastodon/features/emoji/database.ts index 79845b02e7e..80bdfd12ff4 100644 --- a/app/javascript/mastodon/features/emoji/database.ts +++ b/app/javascript/mastodon/features/emoji/database.ts @@ -267,6 +267,11 @@ export async function searchCustomEmojisByShortcodes(shortcodes: string[]) { return results.filter((emoji) => shortcodes.includes(emoji.shortcode)); } +export async function loadAllCustomEmoji() { + const db = await loadDB(); + return db.getAll('custom'); +} + export async function loadLegacyShortcodesByShortcode(shortcode: string) { const db = await loadDB(); return db.getFromIndex( diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index f19b300f3f0..6d2cfbb46ab 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -185,21 +185,16 @@ export function cleanExtraEmojis(extraEmojis?: CustomEmojiMapArg | null) { if (!extraEmojis) { return null; } - if (Array.isArray(extraEmojis)) { - return extraEmojis.reduce( - (acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }), - {}, - ); + if (!Array.isArray(extraEmojis) && !isList(extraEmojis)) { + return extraEmojis; } - if (isList(extraEmojis)) { - return extraEmojis - .toJS() - .reduce( - (acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }), - {}, - ); + const emojis: ExtraCustomEmojiMap = {}; + const emojiArray = isList(extraEmojis) ? extraEmojis.toJS() : extraEmojis; + for (const emoji of emojiArray) { + emojis[emoji.shortcode] = emoji; } - return extraEmojis; + + return emojis; } /** diff --git a/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx b/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx index cb44e1d075c..2e4cc0214c4 100644 --- a/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx +++ b/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx @@ -7,6 +7,7 @@ import elephantUIPlane from '@/images/elephant_ui_plane.svg'; import type { RenderSlideFn } from '@/mastodon/components/carousel'; import { Carousel } from '@/mastodon/components/carousel'; import { CustomEmojiProvider } from '@/mastodon/components/emoji/context'; +import { useCustomEmojis } from '@/mastodon/hooks/useCustomEmojis'; import { mascot } from '@/mastodon/initial_state'; import { createAppSelector, useAppSelector } from '@/mastodon/store'; @@ -23,7 +24,7 @@ const announcementSelector = createAppSelector( export const Announcements: FC = () => { const announcements = useAppSelector(announcementSelector); - const emojis = useAppSelector((state) => state.custom_emojis); + const emojis = useCustomEmojis(); const renderSlide: RenderSlideFn<{ id: string; diff --git a/app/javascript/mastodon/hooks/useCustomEmojis.ts b/app/javascript/mastodon/hooks/useCustomEmojis.ts new file mode 100644 index 00000000000..df0eed61385 --- /dev/null +++ b/app/javascript/mastodon/hooks/useCustomEmojis.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react'; + +import type { ExtraCustomEmojiMap } from '../features/emoji/types'; + +let emojis: ExtraCustomEmojiMap | null = null; + +export function useCustomEmojis() { + const [, setLoaded] = useState(emojis !== null); + useEffect(() => { + if (!emojis) { + void loadEmojisIntoCache().then(() => { + setLoaded(true); + }); + } + }, []); + + return emojis; +} + +async function loadEmojisIntoCache() { + const { loadAllCustomEmoji } = await import('../features/emoji/database'); + const emojisRaw = await loadAllCustomEmoji(); + if (emojisRaw.length === 0) { + return; + } + + emojis = {}; + for (const emoji of emojisRaw) { + emojis[emoji.shortcode] = { + url: emoji.url, + shortcode: emoji.shortcode, + static_url: emoji.static_url, + }; + } +}