diff --git a/src/actions/compose.ts b/src/actions/compose.ts index b0733cda9..600cf869c 100644 --- a/src/actions/compose.ts +++ b/src/actions/compose.ts @@ -24,7 +24,7 @@ import { createStatus } from './statuses.ts'; import type { EditorState } from 'lexical'; import type { AutoSuggestion } from 'soapbox/components/autosuggest-input.tsx'; import type { Emoji } from 'soapbox/features/emoji/index.ts'; -import type { Account, Group } from 'soapbox/schemas/index.ts'; +import type { Account, CustomEmoji, Group } from 'soapbox/schemas/index.ts'; import type { AppDispatch, RootState } from 'soapbox/store.ts'; import type { APIEntity, Status, Tag } from 'soapbox/types/entities.ts'; import type { History } from 'soapbox/types/history.ts'; @@ -512,9 +512,8 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, }); }, 200, { leading: true, trailing: true }); -const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => { - const state = getState(); - const results = emojiSearch(token.replace(':', ''), { maxResults: 10 }, state.custom_emojis); +const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, composeId: string, token: string, customEmojis: CustomEmoji[]) => { + const results = emojiSearch(token.replace(':', ''), { maxResults: 10 }, customEmojis); dispatch(readyComposeSuggestionsEmojis(composeId, token, results)); }; @@ -553,11 +552,11 @@ const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => Root }); }; -const fetchComposeSuggestions = (composeId: string, token: string) => +const fetchComposeSuggestions = (composeId: string, token: string, customEmojis: CustomEmoji[]) => (dispatch: AppDispatch, getState: () => RootState) => { switch (token[0]) { case ':': - fetchComposeSuggestionsEmojis(dispatch, getState, composeId, token); + fetchComposeSuggestionsEmojis(dispatch, composeId, token, customEmojis); break; case '#': fetchComposeSuggestionsTags(dispatch, getState, composeId, token); diff --git a/src/actions/custom-emojis.ts b/src/actions/custom-emojis.ts deleted file mode 100644 index ad71b5c79..000000000 --- a/src/actions/custom-emojis.ts +++ /dev/null @@ -1,49 +0,0 @@ -import api from '../api/index.ts'; - -import type { AppDispatch, RootState } from 'soapbox/store.ts'; -import type { APIEntity } from 'soapbox/types/entities.ts'; - -const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST'; -const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS'; -const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL'; - -const fetchCustomEmojis = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const me = getState().me; - if (!me) return; - - dispatch(fetchCustomEmojisRequest()); - - api(getState).get('/api/v1/custom_emojis').then(response => { - dispatch(fetchCustomEmojisSuccess(response.data)); - }).catch(error => { - dispatch(fetchCustomEmojisFail(error)); - }); - }; - -const fetchCustomEmojisRequest = () => ({ - type: CUSTOM_EMOJIS_FETCH_REQUEST, - skipLoading: true, -}); - -const fetchCustomEmojisSuccess = (custom_emojis: APIEntity[]) => ({ - type: CUSTOM_EMOJIS_FETCH_SUCCESS, - custom_emojis, - skipLoading: true, -}); - -const fetchCustomEmojisFail = (error: unknown) => ({ - type: CUSTOM_EMOJIS_FETCH_FAIL, - error, - skipLoading: true, -}); - -export { - CUSTOM_EMOJIS_FETCH_REQUEST, - CUSTOM_EMOJIS_FETCH_SUCCESS, - CUSTOM_EMOJIS_FETCH_FAIL, - fetchCustomEmojis, - fetchCustomEmojisRequest, - fetchCustomEmojisSuccess, - fetchCustomEmojisFail, -}; diff --git a/src/api/hooks/useCustomEmojis.ts b/src/api/hooks/useCustomEmojis.ts new file mode 100644 index 000000000..0104466ce --- /dev/null +++ b/src/api/hooks/useCustomEmojis.ts @@ -0,0 +1,29 @@ +import { useQuery } from '@tanstack/react-query'; + +import { autosuggestPopulate } from 'soapbox/features/emoji/search.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { CustomEmoji, customEmojiSchema } from 'soapbox/schemas/custom-emoji.ts'; +import { filteredArray } from 'soapbox/schemas/utils.ts'; + +/** Get the Instance for the current backend. */ +export function useCustomEmojis() { + const api = useApi(); + + const { data: customEmojis = [], ...rest } = useQuery({ + queryKey: ['customEmojis', api.baseUrl], + queryFn: async () => { + const response = await api.get('/api/v1/custom_emojis'); + const data = await response.json(); + const customEmojis = filteredArray(customEmojiSchema).parse(data); + + // Add custom emojis to the search index. + autosuggestPopulate(customEmojis); + + return customEmojis; + }, + placeholderData: [], + retryOnMount: false, + }); + + return { customEmojis, ...rest }; +} diff --git a/src/components/announcements/announcement.tsx b/src/components/announcements/announcement.tsx index 35aa9f4f1..934052972 100644 --- a/src/components/announcements/announcement.tsx +++ b/src/components/announcements/announcement.tsx @@ -8,15 +8,13 @@ import { getTextDirection } from 'soapbox/utils/rtl.ts'; import AnnouncementContent from './announcement-content.tsx'; import ReactionsBar from './reactions-bar.tsx'; -import type { Map as ImmutableMap } from 'immutable'; import type { Announcement as AnnouncementEntity } from 'soapbox/schemas/index.ts'; interface IAnnouncement { announcement: AnnouncementEntity; - emojiMap: ImmutableMap>; } -const Announcement: React.FC = ({ announcement, emojiMap }) => { +const Announcement: React.FC = ({ announcement }) => { const features = useFeatures(); const startsAt = announcement.starts_at && new Date(announcement.starts_at); @@ -64,7 +62,6 @@ const Announcement: React.FC = ({ announcement, emojiMap }) => { )} diff --git a/src/components/announcements/announcements-panel.tsx b/src/components/announcements/announcements-panel.tsx index 5d6cb66e2..84540954c 100644 --- a/src/components/announcements/announcements-panel.tsx +++ b/src/components/announcements/announcements-panel.tsx @@ -1,26 +1,17 @@ import clsx from 'clsx'; -import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import ReactSwipeableViews from 'react-swipeable-views'; -import { createSelector } from 'reselect'; import { useAnnouncements } from 'soapbox/api/hooks/announcements/index.ts'; import { Card } from 'soapbox/components/ui/card.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; import Widget from 'soapbox/components/ui/widget.tsx'; -import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import Announcement from './announcement.tsx'; -import type { RootState } from 'soapbox/store.ts'; - -const customEmojiMap = createSelector([(state: RootState) => state.custom_emojis], items => (items as ImmutableList>).reduce((map, emoji) => map.set(emoji.get('shortcode')!, emoji), ImmutableMap>())); - const AnnouncementsPanel = () => { - const emojiMap = useAppSelector(state => customEmojiMap(state)); const [index, setIndex] = useState(0); - const { data: announcements } = useAnnouncements(); if (!announcements || announcements.length === 0) return null; @@ -37,7 +28,6 @@ const AnnouncementsPanel = () => { )).reverse()} diff --git a/src/components/announcements/emoji.tsx b/src/components/announcements/emoji.tsx index f9994ec47..3380ddb9e 100644 --- a/src/components/announcements/emoji.tsx +++ b/src/components/announcements/emoji.tsx @@ -1,34 +1,20 @@ -import unicodeMapping from 'soapbox/features/emoji/mapping.ts'; +import { useCustomEmojis } from 'soapbox/api/hooks/useCustomEmojis.ts'; +import NativeEmoji from 'soapbox/components/ui/emoji.tsx'; import { useSettings } from 'soapbox/hooks/useSettings.ts'; -import type { Map as ImmutableMap } from 'immutable'; - interface IEmoji { emoji: string; - emojiMap: ImmutableMap>; hovered: boolean; } -const Emoji: React.FC = ({ emoji, emojiMap, hovered }) => { +const Emoji: React.FC = ({ emoji, hovered }) => { const { autoPlayGif } = useSettings(); + const { customEmojis } = useCustomEmojis(); - // @ts-ignore - if (unicodeMapping[emoji]) { - // @ts-ignore - const { filename, shortCode } = unicodeMapping[emoji]; - const title = shortCode ? `:${shortCode}:` : ''; + const custom = customEmojis.find((x) => x.shortcode === emoji); - return ( - {emoji} - ); - } else if (emojiMap.get(emoji as any)) { - const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']); + if (custom) { + const filename = (autoPlayGif || hovered) ? custom.url : custom.static_url; const shortCode = `:${emoji}:`; return ( @@ -40,9 +26,9 @@ const Emoji: React.FC = ({ emoji, emojiMap, hovered }) => { src={filename as string} /> ); - } else { - return null; } + + return ; }; export default Emoji; diff --git a/src/components/announcements/reaction.tsx b/src/components/announcements/reaction.tsx index ad9c0704e..7fbccb736 100644 --- a/src/components/announcements/reaction.tsx +++ b/src/components/announcements/reaction.tsx @@ -7,17 +7,15 @@ import unicodeMapping from 'soapbox/features/emoji/mapping.ts'; import Emoji from './emoji.tsx'; -import type { Map as ImmutableMap } from 'immutable'; import type { AnnouncementReaction } from 'soapbox/schemas/index.ts'; interface IReaction { announcementId: string; reaction: AnnouncementReaction; - emojiMap: ImmutableMap>; style: React.CSSProperties; } -const Reaction: React.FC = ({ announcementId, reaction, emojiMap, style }) => { +const Reaction: React.FC = ({ announcementId, reaction, style }) => { const [hovered, setHovered] = useState(false); const { addReaction, removeReaction } = useAnnouncements(); @@ -55,7 +53,7 @@ const Reaction: React.FC = ({ announcementId, reaction, emojiMap, sty style={style} > - + diff --git a/src/components/announcements/reactions-bar.tsx b/src/components/announcements/reactions-bar.tsx index 18f7243d4..56f52ffb7 100644 --- a/src/components/announcements/reactions-bar.tsx +++ b/src/components/announcements/reactions-bar.tsx @@ -7,17 +7,15 @@ import { useSettings } from 'soapbox/hooks/useSettings.ts'; import Reaction from './reaction.tsx'; -import type { Map as ImmutableMap } from 'immutable'; import type { Emoji, NativeEmoji } from 'soapbox/features/emoji/index.ts'; import type { AnnouncementReaction } from 'soapbox/schemas/index.ts'; interface IReactionsBar { announcementId: string; reactions: AnnouncementReaction[]; - emojiMap: ImmutableMap>; } -const ReactionsBar: React.FC = ({ announcementId, reactions, emojiMap }) => { +const ReactionsBar: React.FC = ({ announcementId, reactions }) => { const { reduceMotion } = useSettings(); const { addReaction } = useAnnouncements(); @@ -47,7 +45,6 @@ const ReactionsBar: React.FC = ({ announcementId, reactions, emoj reaction={data} style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }} announcementId={announcementId} - emojiMap={emojiMap} /> ))} diff --git a/src/features/compose/components/compose-form.tsx b/src/features/compose/components/compose-form.tsx index d68626efc..1e4b4da68 100644 --- a/src/features/compose/components/compose-form.tsx +++ b/src/features/compose/components/compose-form.tsx @@ -15,6 +15,7 @@ import { selectComposeSuggestion, uploadCompose, } from 'soapbox/actions/compose.ts'; +import { useCustomEmojis } from 'soapbox/api/hooks/useCustomEmojis.ts'; import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input.tsx'; import Button from 'soapbox/components/ui/button.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; @@ -109,6 +110,7 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab const spoilerTextRef = useRef(null); const editorRef = useRef(null); const { isDraggedOver } = useDraggedFiles(formRef); + const { customEmojis } = useCustomEmojis(); const text = editorRef.current?.getEditorState().read(() => $getRoot().getTextContent()) ?? ''; const fulltext = [spoilerText, countableText(text)].join(''); @@ -161,8 +163,8 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab dispatch(clearComposeSuggestions(id)); }; - const onSuggestionsFetchRequested = (token: string | number) => { - dispatch(fetchComposeSuggestions(id, token as string)); + const onSuggestionsFetchRequested = (token: string) => { + dispatch(fetchComposeSuggestions(id, token, customEmojis)); }; const onSpoilerSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => { diff --git a/src/features/compose/components/polls/poll-form.tsx b/src/features/compose/components/polls/poll-form.tsx index 4ef1043aa..98313325e 100644 --- a/src/features/compose/components/polls/poll-form.tsx +++ b/src/features/compose/components/polls/poll-form.tsx @@ -1,6 +1,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { addPollOption, changePollOption, changePollSettings, clearComposeSuggestions, fetchComposeSuggestions, removePoll, removePollOption, selectComposeSuggestion } from 'soapbox/actions/compose.ts'; +import { useCustomEmojis } from 'soapbox/api/hooks/useCustomEmojis.ts'; import AutosuggestInput from 'soapbox/components/autosuggest-input.tsx'; import Button from 'soapbox/components/ui/button.tsx'; import Divider from 'soapbox/components/ui/divider.tsx'; @@ -55,6 +56,7 @@ const Option: React.FC = ({ const intl = useIntl(); const suggestions = useCompose(composeId).suggestions; + const { customEmojis } = useCustomEmojis(); const handleOptionTitleChange = (event: React.ChangeEvent) => onChange(index, event.target.value); @@ -68,7 +70,7 @@ const Option: React.FC = ({ const onSuggestionsClearRequested = () => dispatch(clearComposeSuggestions(composeId)); - const onSuggestionsFetchRequested = (token: string) => dispatch(fetchComposeSuggestions(composeId, token)); + const onSuggestionsFetchRequested = (token: string) => dispatch(fetchComposeSuggestions(composeId, token, customEmojis)); const onSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => { if (token && typeof token === 'string') { diff --git a/src/features/compose/editor/plugins/autosuggest-plugin.tsx b/src/features/compose/editor/plugins/autosuggest-plugin.tsx index af5eb3e66..984dea262 100644 --- a/src/features/compose/editor/plugins/autosuggest-plugin.tsx +++ b/src/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -35,6 +35,7 @@ import ReactDOM from 'react-dom'; import { clearComposeSuggestions, fetchComposeSuggestions } from 'soapbox/actions/compose.ts'; import { chooseEmoji } from 'soapbox/actions/emojis.ts'; +import { useCustomEmojis } from 'soapbox/api/hooks/useCustomEmojis.ts'; import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji.tsx'; import { isNativeEmoji } from 'soapbox/features/emoji/index.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; @@ -278,6 +279,7 @@ const AutosuggestPlugin = ({ setSuggestionsHidden, }: AutosuggestPluginProps): JSX.Element | null => { const { suggestions } = useCompose(composeId); + const { customEmojis } = useCustomEmojis(); const dispatch = useAppDispatch(); const [editor] = useLexicalComposerContext(); @@ -410,7 +412,7 @@ const AutosuggestPlugin = ({ return; } - dispatch(fetchComposeSuggestions(composeId, match.matchingString.trim())); + dispatch(fetchComposeSuggestions(composeId, match.matchingString.trim(), customEmojis)); if (!isSelectionOnEntityBoundary(editor, match.leadOffset)) { const isRangePositioned = tryToPositionRange(match.leadOffset, range); diff --git a/src/features/emoji/components/emoji-picker-dropdown.tsx b/src/features/emoji/components/emoji-picker-dropdown.tsx index 3a528c267..2f8445cb2 100644 --- a/src/features/emoji/components/emoji-picker-dropdown.tsx +++ b/src/features/emoji/components/emoji-picker-dropdown.tsx @@ -5,6 +5,7 @@ import { createSelector } from 'reselect'; import { chooseEmoji } from 'soapbox/actions/emojis.ts'; import { changeSetting } from 'soapbox/actions/settings.ts'; +import { useCustomEmojis } from 'soapbox/api/hooks/useCustomEmojis.ts'; import { buildCustomEmojis } from 'soapbox/features/emoji/index.ts'; import { EmojiPicker } from 'soapbox/features/ui/util/async-components.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; @@ -13,6 +14,7 @@ import { useTheme } from 'soapbox/hooks/useTheme.ts'; import { RootState } from 'soapbox/store.ts'; import type { Emoji, CustomEmoji, NativeEmoji } from 'soapbox/features/emoji/index.ts'; +import type { CustomEmoji as MastodonCustomEmoji } from 'soapbox/schemas/custom-emoji.ts'; export const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -90,20 +92,21 @@ export const getFrequentlyUsedEmojis = createSelector([ return emojis; }); -const getCustomEmojis = createSelector([ - (state: RootState) => state.custom_emojis, -], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => { - const aShort = a.get('shortcode')!.toLowerCase(); - const bShort = b.get('shortcode')!.toLowerCase(); +/** Filter custom emojis to only ones visible in the picker, and sort them alphabetically. */ +function filterCustomEmojis(customEmojis: MastodonCustomEmoji[]) { + return customEmojis.filter(e => e.visible_in_picker).sort((a, b) => { + const aShort = a.shortcode.toLowerCase(); + const bShort = b.shortcode.toLowerCase(); - if (aShort < bShort) { - return -1; - } else if (aShort > bShort) { - return 1; - } else { - return 0; - } -})); + if (aShort < bShort) { + return -1; + } else if (aShort > bShort) { + return 1; + } else { + return 0; + } + }); +} interface IRenderAfter { children: React.ReactNode; @@ -137,7 +140,7 @@ const EmojiPickerDropdown: React.FC = ({ const title = intl.formatMessage(messages.emoji); const theme = useTheme(); - const customEmojis = useAppSelector((state) => getCustomEmojis(state)); + const { customEmojis } = useCustomEmojis(); const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state)); const handlePick = (emoji: any) => { @@ -226,7 +229,7 @@ const EmojiPickerDropdown: React.FC = ({ {})}> { export default emojify; -export const buildCustomEmojis = (customEmojis: any) => { +export function buildCustomEmojis(customEmojis: MastodonCustomEmoji[]): EmojiMart[] { const emojis: EmojiMart[] = []; - customEmojis.forEach((emoji: any) => { - const shortcode = emoji.get('shortcode'); - const url = emoji.get('static_url'); - const name = shortcode.replace(':', ''); + customEmojis.forEach((emoji) => { + const shortcode = emoji.shortcode; + const url = emoji.url; + const name = shortcode.replace(':', ''); emojis.push({ id: name, @@ -223,4 +224,4 @@ export const buildCustomEmojis = (customEmojis: any) => { }); return emojis; -}; +} diff --git a/src/features/emoji/search.test.ts b/src/features/emoji/search.test.ts index fb64cbbc4..4d2982e5a 100644 --- a/src/features/emoji/search.test.ts +++ b/src/features/emoji/search.test.ts @@ -1,4 +1,3 @@ -import { List, Map } from 'immutable'; import pick from 'lodash/pick'; import { describe, expect, it } from 'vitest'; @@ -35,13 +34,19 @@ describe('emoji_index', () => { id: 'mastodon', name: 'mastodon', keywords: ['mastodon'], - skins: { src: 'http://example.com' }, + skins: [{ src: 'http://example.com' }], }, ]; - const custom_emojis = List([ - Map({ static_url: 'http://example.com', shortcode: 'mastodon' }), - ]); + const customEmojis = [ + { + category: '', + url: 'http://example.com/mastodon.png', + static_url: 'http://example.com/mastodon.png', + shortcode: 'mastodon', + visible_in_picker: true, + }, + ]; const lightExpected = [ { @@ -51,7 +56,7 @@ describe('emoji_index', () => { ]; addCustomToPool(custom); - expect(search('masto', {}, custom_emojis).map(trimEmojis)).toEqual(lightExpected); + expect(search('masto', {}, customEmojis).map(trimEmojis)).toEqual(lightExpected); }); it('updates custom emoji if another is passed', () => { @@ -60,7 +65,7 @@ describe('emoji_index', () => { id: 'mastodon', name: 'mastodon', keywords: ['mastodon'], - skins: { src: 'http://example.com' }, + skins: [{ src: 'http://example.com' }], }, ]; @@ -71,18 +76,24 @@ describe('emoji_index', () => { id: 'pleroma', name: 'pleroma', keywords: ['pleroma'], - skins: { src: 'http://example.com' }, + skins: [{ src: 'http://example.com' }], }, ]; addCustomToPool(custom2); - const custom_emojis = List([ - Map({ static_url: 'http://example.com', shortcode: 'pleroma' }), - ]); + const customEmojis = [ + { + category: '', + url: 'http://example.com/pleroma.png', + static_url: 'http://example.com/pleroma.png', + shortcode: 'pleroma', + visible_in_picker: true, + }, + ]; const expected: any = []; - expect(search('masto', {}, custom_emojis).map(trimEmojis)).toEqual(expected); + expect(search('masto', {}, customEmojis).map(trimEmojis)).toEqual(expected); }); it('does an emoji whose unified name is irregular', () => { diff --git a/src/features/emoji/search.ts b/src/features/emoji/search.ts index 695a2ae37..8151ee48e 100644 --- a/src/features/emoji/search.ts +++ b/src/features/emoji/search.ts @@ -1,10 +1,10 @@ // @ts-ignore import Index from '@akryum/flexsearch-es'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import data from './data.ts'; +import data, { Emoji as EmojiMart, CustomEmoji as EmojiMartCustom } from 'soapbox/features/emoji/data.ts'; +import { CustomEmoji } from 'soapbox/schemas/custom-emoji.ts'; -import type { Emoji } from './index.ts'; +import { buildCustomEmojis, type Emoji } from './index.ts'; // @ts-ignore Wrong default export. const index: Index.Index = new Index({ @@ -23,8 +23,7 @@ export interface searchOptions { custom?: any; } -export const addCustomToPool = (customEmojis: any[]) => { - // @ts-ignore +export function addCustomToPool(customEmojis: EmojiMart[]): void { for (const key in index.register) { if (key[0] === 'c') { index.remove(key); // remove old custom emojis @@ -36,27 +35,27 @@ export const addCustomToPool = (customEmojis: any[]) => { for (const emoji of customEmojis) { index.add('c' + i++, emoji.id); } -}; +} // we can share an index by prefixing custom emojis with 'c' and native with 'n' const search = ( str: string, { maxResults = 5 }: searchOptions = {}, - custom_emojis?: ImmutableList>, + customEmojis?: CustomEmoji[], ): Emoji[] => { return index.search(str, maxResults) .flatMap((id: any) => { if (typeof id !== 'string') return; - if (id[0] === 'c' && custom_emojis) { + if (id[0] === 'c' && customEmojis) { const index = Number(id.slice(1)); - const custom = custom_emojis.get(index); + const custom = customEmojis[index]; if (custom) { return { - id: custom.get('shortcode', ''), - colons: ':' + custom.get('shortcode', '') + ':', + id: custom.shortcode, + colons: ':' + custom.shortcode + ':', custom: true, - imageUrl: custom.get('static_url', ''), + imageUrl: custom.url, }; } } @@ -74,4 +73,9 @@ const search = ( }).filter(Boolean) as Emoji[]; }; +/** Import Mastodon custom emojis as emoji mart custom emojis. */ +export function autosuggestPopulate(emojis: CustomEmoji[]) { + addCustomToPool(buildCustomEmojis(emojis)); +} + export default search; diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index 6dbe3412d..59fdea8d9 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -4,7 +4,6 @@ import { Switch, useHistory, useLocation, Redirect } from 'react-router-dom'; import { fetchFollowRequests } from 'soapbox/actions/accounts.ts'; import { fetchReports, fetchUsers, fetchConfig } from 'soapbox/actions/admin.ts'; -import { fetchCustomEmojis } from 'soapbox/actions/custom-emojis.ts'; import { fetchFilters } from 'soapbox/actions/filters.ts'; import { fetchMarker } from 'soapbox/actions/markers.ts'; import { expandNotifications } from 'soapbox/actions/notifications.ts'; @@ -13,6 +12,7 @@ import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses.ts'; import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions.ts'; import { expandHomeTimeline } from 'soapbox/actions/timelines.ts'; import { useUserStream } from 'soapbox/api/hooks/index.ts'; +import { useCustomEmojis } from 'soapbox/api/hooks/useCustomEmojis.ts'; import SidebarNavigation from 'soapbox/components/sidebar-navigation.tsx'; import ThumbNavigation from 'soapbox/components/thumb-navigation.tsx'; import Layout from 'soapbox/components/ui/layout.tsx'; @@ -472,11 +472,11 @@ const UI: React.FC = ({ children }) => { }, []); useUserStream(); + useCustomEmojis(); // The user has logged in useEffect(() => { loadAccountData(); - dispatch(fetchCustomEmojis()); }, [!!account]); useEffect(() => { diff --git a/src/reducers/custom-emojis.test.ts b/src/reducers/custom-emojis.test.ts deleted file mode 100644 index 090c265fb..000000000 --- a/src/reducers/custom-emojis.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { List as ImmutableList } from 'immutable'; -import { describe, expect, it } from 'vitest'; - -import reducer from './custom-emojis.ts'; - -describe('custom_emojis reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableList()); - }); -}); diff --git a/src/reducers/custom-emojis.ts b/src/reducers/custom-emojis.ts deleted file mode 100644 index a3a57a7e3..000000000 --- a/src/reducers/custom-emojis.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable'; - -import emojiData from 'soapbox/features/emoji/data.ts'; -import { buildCustomEmojis } from 'soapbox/features/emoji/index.ts'; -import { addCustomToPool } from 'soapbox/features/emoji/search.ts'; - -import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom-emojis.ts'; - -import type { AnyAction } from 'redux'; -import type { APIEntity } from 'soapbox/types/entities.ts'; - -const initialState = ImmutableList>(); - -// Populate custom emojis for composer autosuggest -const autosuggestPopulate = (emojis: ImmutableList>) => { - addCustomToPool(buildCustomEmojis(emojis)); -}; - -const importEmojis = (customEmojis: APIEntity[]) => { - const emojis = (fromJS(customEmojis) as ImmutableList>).filter((emoji) => { - // If a custom emoji has the shortcode of a Unicode emoji, skip it. - // Otherwise it breaks EmojiMart. - // https://gitlab.com/soapbox-pub/soapbox/-/issues/610 - const shortcode = emoji.get('shortcode', '').toLowerCase(); - return !emojiData.emojis[shortcode]; - }); - - autosuggestPopulate(emojis); - return emojis; -}; - -export default function custom_emojis(state = initialState, action: AnyAction) { - if (action.type === CUSTOM_EMOJIS_FETCH_SUCCESS) { - return importEmojis(action.custom_emojis); - } - - return state; -} diff --git a/src/reducers/index.ts b/src/reducers/index.ts index e39c98910..21fae33b3 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -14,7 +14,6 @@ import compose_event from './compose-event.ts'; import compose from './compose.ts'; import contexts from './contexts.ts'; import conversations from './conversations.ts'; -import custom_emojis from './custom-emojis.ts'; import domain_lists from './domain-lists.ts'; import dropdown_menu from './dropdown-menu.ts'; import filters from './filters.ts'; @@ -69,7 +68,6 @@ export default combineReducers({ compose_event, contexts, conversations, - custom_emojis, domain_lists, dropdown_menu, entities,