Fetch custom_emojis with a hook
This commit is contained in:
parent
eebdb46b7d
commit
62ca4c7a2a
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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<CustomEmoji[]>({
|
||||
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 };
|
||||
}
|
|
@ -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<string, ImmutableMap<string, string>>;
|
||||
}
|
||||
|
||||
const Announcement: React.FC<IAnnouncement> = ({ announcement, emojiMap }) => {
|
||||
const Announcement: React.FC<IAnnouncement> = ({ announcement }) => {
|
||||
const features = useFeatures();
|
||||
|
||||
const startsAt = announcement.starts_at && new Date(announcement.starts_at);
|
||||
|
@ -64,7 +62,6 @@ const Announcement: React.FC<IAnnouncement> = ({ announcement, emojiMap }) => {
|
|||
<ReactionsBar
|
||||
reactions={announcement.reactions}
|
||||
announcementId={announcement.id}
|
||||
emojiMap={emojiMap}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
@ -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<ImmutableMap<string, string>>).reduce((map, emoji) => map.set(emoji.get('shortcode')!, emoji), ImmutableMap<string, ImmutableMap<string, string>>()));
|
||||
|
||||
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 = () => {
|
|||
<Announcement
|
||||
key={announcement.id}
|
||||
announcement={announcement}
|
||||
emojiMap={emojiMap}
|
||||
/>
|
||||
)).reverse()}
|
||||
</ReactSwipeableViews>
|
||||
|
|
|
@ -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<string, ImmutableMap<string, string>>;
|
||||
hovered: boolean;
|
||||
}
|
||||
|
||||
const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
||||
const Emoji: React.FC<IEmoji> = ({ 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 (
|
||||
<img
|
||||
draggable='false'
|
||||
className='emojione m-0 block'
|
||||
alt={emoji}
|
||||
title={title}
|
||||
src={`/packs/emoji/${filename}.svg`}
|
||||
/>
|
||||
);
|
||||
} 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<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
|||
src={filename as string}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <NativeEmoji emoji={emoji} />;
|
||||
};
|
||||
|
||||
export default Emoji;
|
||||
|
|
|
@ -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<string, ImmutableMap<string, string>>;
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, style }) => {
|
||||
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, style }) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const { addReaction, removeReaction } = useAnnouncements();
|
||||
|
@ -55,7 +53,7 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, sty
|
|||
style={style}
|
||||
>
|
||||
<span className='block size-4'>
|
||||
<Emoji hovered={hovered} emoji={reaction.name} emojiMap={emojiMap} />
|
||||
<Emoji hovered={hovered} emoji={reaction.name} />
|
||||
</span>
|
||||
<span className='block min-w-[9px] text-center text-xs font-medium text-primary-600 dark:text-white'>
|
||||
<AnimatedNumber value={reaction.count} />
|
||||
|
|
|
@ -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<string, ImmutableMap<string, string>>;
|
||||
}
|
||||
|
||||
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, emojiMap }) => {
|
||||
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions }) => {
|
||||
const { reduceMotion } = useSettings();
|
||||
const { addReaction } = useAnnouncements();
|
||||
|
||||
|
@ -47,7 +45,6 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, emoj
|
|||
reaction={data}
|
||||
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
|
||||
announcementId={announcementId}
|
||||
emojiMap={emojiMap}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
|
|
@ -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 extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
const spoilerTextRef = useRef<AutosuggestInput>(null);
|
||||
const editorRef = useRef<LexicalEditor>(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 extends string>({ 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) => {
|
||||
|
|
|
@ -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<IOption> = ({
|
|||
const intl = useIntl();
|
||||
|
||||
const suggestions = useCompose(composeId).suggestions;
|
||||
const { customEmojis } = useCustomEmojis();
|
||||
|
||||
const handleOptionTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => onChange(index, event.target.value);
|
||||
|
||||
|
@ -68,7 +70,7 @@ const Option: React.FC<IOption> = ({
|
|||
|
||||
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') {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,11 +92,11 @@ 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;
|
||||
|
@ -103,7 +105,8 @@ const getCustomEmojis = createSelector([
|
|||
} else {
|
||||
return 0;
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
interface IRenderAfter {
|
||||
children: React.ReactNode;
|
||||
|
@ -137,7 +140,7 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
|||
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<IEmojiPickerDropdown> = ({
|
|||
<Suspense>
|
||||
<RenderAfter update={update ?? (() => {})}>
|
||||
<EmojiPicker
|
||||
custom={withCustom ? [{ emojis: buildCustomEmojis(customEmojis) }] : undefined}
|
||||
custom={withCustom ? [{ emojis: buildCustomEmojis(filterCustomEmojis(customEmojis)) }] : undefined}
|
||||
title={title}
|
||||
onEmojiSelect={handlePick}
|
||||
recent={frequentlyUsedEmojis}
|
||||
|
|
|
@ -3,6 +3,7 @@ import split from 'graphemesplit';
|
|||
import unicodeMapping from './mapping.ts';
|
||||
|
||||
import type { Emoji as EmojiMart, CustomEmoji as EmojiMartCustom } from 'soapbox/features/emoji/data.ts';
|
||||
import type { CustomEmoji as MastodonCustomEmoji } from 'soapbox/schemas/custom-emoji.ts';
|
||||
|
||||
/*
|
||||
* TODO: Consolate emoji object types
|
||||
|
@ -206,12 +207,12 @@ const emojify = (str: string, customEmojis = {}) => {
|
|||
|
||||
export default emojify;
|
||||
|
||||
export const buildCustomEmojis = (customEmojis: any) => {
|
||||
export function buildCustomEmojis(customEmojis: MastodonCustomEmoji[]): EmojiMart<EmojiMartCustom>[] {
|
||||
const emojis: EmojiMart<EmojiMartCustom>[] = [];
|
||||
|
||||
customEmojis.forEach((emoji: any) => {
|
||||
const shortcode = emoji.get('shortcode');
|
||||
const url = emoji.get('static_url');
|
||||
customEmojis.forEach((emoji) => {
|
||||
const shortcode = emoji.shortcode;
|
||||
const url = emoji.url;
|
||||
const name = shortcode.replace(':', '');
|
||||
|
||||
emojis.push({
|
||||
|
@ -223,4 +224,4 @@ export const buildCustomEmojis = (customEmojis: any) => {
|
|||
});
|
||||
|
||||
return emojis;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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<EmojiMartCustom>[]): 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<ImmutableMap<string, string>>,
|
||||
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;
|
||||
|
|
|
@ -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<IUI> = ({ children }) => {
|
|||
}, []);
|
||||
|
||||
useUserStream();
|
||||
useCustomEmojis();
|
||||
|
||||
// The user has logged in
|
||||
useEffect(() => {
|
||||
loadAccountData();
|
||||
dispatch(fetchCustomEmojis());
|
||||
}, [!!account]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
});
|
|
@ -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<ImmutableMap<string, string>>();
|
||||
|
||||
// Populate custom emojis for composer autosuggest
|
||||
const autosuggestPopulate = (emojis: ImmutableList<ImmutableMap<string, string>>) => {
|
||||
addCustomToPool(buildCustomEmojis(emojis));
|
||||
};
|
||||
|
||||
const importEmojis = (customEmojis: APIEntity[]) => {
|
||||
const emojis = (fromJS(customEmojis) as ImmutableList<ImmutableMap<string, string>>).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;
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue