diff --git a/app/javascript/mastodon/actions/custom_emojis.js b/app/javascript/mastodon/actions/custom_emojis.js
deleted file mode 100644
index fb65f072dc..0000000000
--- a/app/javascript/mastodon/actions/custom_emojis.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import api from '../api';
-
-export const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST';
-export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS';
-export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
-
-export function fetchCustomEmojis() {
- return (dispatch) => {
- dispatch(fetchCustomEmojisRequest());
-
- api().get('/api/v1/custom_emojis').then(response => {
- dispatch(fetchCustomEmojisSuccess(response.data));
- }).catch(error => {
- dispatch(fetchCustomEmojisFail(error));
- });
- };
-}
-
-export function fetchCustomEmojisRequest() {
- return {
- type: CUSTOM_EMOJIS_FETCH_REQUEST,
- skipLoading: true,
- };
-}
-
-export function fetchCustomEmojisSuccess(custom_emojis) {
- return {
- type: CUSTOM_EMOJIS_FETCH_SUCCESS,
- custom_emojis,
- skipLoading: true,
- };
-}
-
-export function fetchCustomEmojisFail(error) {
- return {
- type: CUSTOM_EMOJIS_FETCH_FAIL,
- error,
- skipLoading: true,
- };
-}
diff --git a/app/javascript/mastodon/containers/compose_container.jsx b/app/javascript/mastodon/containers/compose_container.jsx
index 3e6d20c74c..33fc3a79a3 100644
--- a/app/javascript/mastodon/containers/compose_container.jsx
+++ b/app/javascript/mastodon/containers/compose_container.jsx
@@ -1,6 +1,5 @@
import { Provider } from 'react-redux';
-import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
import { fetchServer } from 'mastodon/actions/server';
import { hydrateStore } from 'mastodon/actions/store';
import { Router } from 'mastodon/components/router';
@@ -13,7 +12,6 @@ if (initialState) {
store.dispatch(hydrateStore(initialState));
}
-store.dispatch(fetchCustomEmojis());
store.dispatch(fetchServer());
const ComposeContainer = () => (
diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx
index ee861366a5..f697c5a8b0 100644
--- a/app/javascript/mastodon/containers/mastodon.jsx
+++ b/app/javascript/mastodon/containers/mastodon.jsx
@@ -5,8 +5,6 @@ import { Route } from 'react-router-dom';
import { Provider as ReduxProvider } from 'react-redux';
-
-import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
import { hydrateStore } from 'mastodon/actions/store';
import { connectUserStream } from 'mastodon/actions/streaming';
import ErrorBoundary from 'mastodon/components/error_boundary';
@@ -26,9 +24,6 @@ const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`;
const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction);
-if (initialState.meta.me) {
- store.dispatch(fetchCustomEmojis());
-}
export default class Mastodon extends PureComponent {
identity = createIdentityContext(initialState);
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx
index c7c5e88f8c..0013db281a 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx
@@ -5,8 +5,6 @@ import { defineMessages, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-
import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay';
@@ -14,7 +12,6 @@ import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
import { IconButton } from 'mastodon/components/icon_button';
import { injectIntl } from '@/mastodon/components/intl';
-import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
const messages = defineMessages({
@@ -156,7 +153,6 @@ class ModifierPicker extends PureComponent {
class EmojiPickerMenuImpl extends PureComponent {
static propTypes = {
- custom_emojis: ImmutablePropTypes.list,
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
loading: PropTypes.bool,
onClose: PropTypes.func.isRequired,
@@ -254,7 +250,7 @@ class EmojiPickerMenuImpl extends PureComponent {
};
render() {
- const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
+ const { loading, style, intl, skinTone, frequentlyUsedEmojis } = this.props;
if (loading) {
return
;
@@ -264,32 +260,16 @@ class EmojiPickerMenuImpl extends PureComponent {
const { modifierOpen } = this.state;
- const categoriesSort = [
- 'recent',
- 'people',
- 'nature',
- 'foods',
- 'activity',
- 'places',
- 'objects',
- 'symbols',
- 'flags',
- ];
-
- categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort());
-
return (
{
+ this.setState({ loading: false });
+ });
}).catch(() => {
this.setState({ loading: false, active: false });
});
@@ -408,7 +388,6 @@ class EmojiPickerDropdown extends PureComponent {
state.get('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();
-
- if (aShort < bShort) {
- return -1;
- } else if (aShort > bShort ) {
- return 1;
- } else {
- return 0;
- }
-}));
-
const mapStateToProps = state => ({
- custom_emojis: getCustomEmojis(state),
skinTone: state.getIn(['settings', 'skinTone']),
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index 859665a531..8f45c76ac7 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -168,29 +168,3 @@ const emojify = (str, customEmojis = {}) => {
};
export default emojify;
-
-export const buildCustomEmojis = (customEmojis) => {
- const emojis = [];
-
- customEmojis.forEach(emoji => {
- const shortcode = emoji.get('shortcode');
- const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url');
- const name = shortcode.replace(':', '');
-
- emojis.push({
- id: name,
- name,
- short_names: [name],
- text: '',
- emoticons: [],
- keywords: [name],
- imageUrl: url,
- custom: true,
- customCategory: emoji.get('category'),
- });
- });
-
- return emojis;
-};
-
-export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set(['custom']));
diff --git a/app/javascript/mastodon/features/emoji/emoji_picker.tsx b/app/javascript/mastodon/features/emoji/emoji_picker.tsx
index 37fc94dde7..a9a93aa87d 100644
--- a/app/javascript/mastodon/features/emoji/emoji_picker.tsx
+++ b/app/javascript/mastodon/features/emoji/emoji_picker.tsx
@@ -1,49 +1,130 @@
-import type { EmojiProps, PickerProps } from 'emoji-mart';
+import type { FC } from 'react';
+import { useEffect, useState } from 'react';
+
+import type {
+ CategoryName,
+ CustomEmoji,
+ EmojiProps,
+ PickerProps,
+} from 'emoji-mart';
import EmojiRaw from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
import PickerRaw from 'emoji-mart/dist-es/components/picker/nimble-picker';
+import { autoPlayGif } from '@/mastodon/initial_state';
import { assetHost } from 'mastodon/utils/config';
import { EMOJI_MODE_NATIVE } from './constants';
import EmojiData from './emoji_data.json';
import { useEmojiAppState } from './mode';
+import { emojiLogger } from './utils';
const backgroundImageFnDefault = () => `${assetHost}/emoji/sheet_16_0.png`;
-const Emoji = ({
+let customEmojis: CustomEmoji[] | null = null;
+let customCategories = [
+ 'recent',
+ 'people',
+ 'nature',
+ 'foods',
+ 'activity',
+ 'places',
+ 'objects',
+ 'symbols',
+ 'flags',
+] as CategoryName[];
+
+const log = emojiLogger('picker');
+
+export const Picker: FC = ({
set = 'twitter',
sheetSize = 32,
sheetColumns = 62,
sheetRows = 62,
backgroundImageFn = backgroundImageFnDefault,
...props
-}: EmojiProps) => {
+}) => {
+ const { mode } = useEmojiAppState();
+ const [isLoaded, setLoaded] = useState(customEmojis !== null);
+
+ useEffect(() => {
+ if (customEmojis === null) {
+ void loadCustomEmojiData().then(() => {
+ setLoaded(true);
+ });
+ }
+ }, []);
+
+ if (!isLoaded) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+export async function loadCustomEmojiData() {
+ const { loadAllCustomEmoji } = await import('./database');
+ const emojisRaw = await loadAllCustomEmoji();
+ if (emojisRaw.length === 0) {
+ return;
+ }
+
+ const categories = new Set(['custom']);
+ const emojis = [];
+ for (const emoji of emojisRaw) {
+ const name = emoji.shortcode.replaceAll(':', '');
+ emojis.push({
+ name,
+ id: name,
+ custom: true,
+ short_names: [name],
+ imageUrl: autoPlayGif ? emoji.url : emoji.static_url,
+ customCategory: emoji.category,
+ });
+
+ if (emoji.category) {
+ categories.add(`custom-${emoji.category}`);
+ }
+ }
+
+ customEmojis = emojis.toSorted((a, b) => {
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+ });
+ customCategories = customCategories.toSpliced(
+ 1,
+ 0,
+ ...(Array.from(categories).toSorted() as CategoryName[]),
+ );
+ log(
+ 'loaded %d custom emojis in %d categories',
+ customEmojis.length,
+ categories.size,
+ );
+}
+
+export const Emoji: FC = ({
+ set = 'twitter',
+ sheetSize = 32,
+ sheetColumns = 62,
+ sheetRows = 62,
+ backgroundImageFn = backgroundImageFnDefault,
+ ...props
+}) => {
const { mode } = useEmojiAppState();
return (
- );
-};
-
-const Picker = ({
- set = 'twitter',
- sheetSize = 32,
- sheetColumns = 62,
- sheetRows = 62,
- backgroundImageFn = backgroundImageFnDefault,
- ...props
-}: PickerProps) => {
- const { mode } = useEmojiAppState();
- return (
-
);
};
-
-export { Picker, Emoji };
diff --git a/app/javascript/mastodon/reducers/custom_emojis.js b/app/javascript/mastodon/reducers/custom_emojis.js
deleted file mode 100644
index 47aa3edbbb..0000000000
--- a/app/javascript/mastodon/reducers/custom_emojis.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { List as ImmutableList, fromJS as ConvertToImmutable } from 'immutable';
-
-import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom_emojis';
-import { buildCustomEmojis } from '../features/emoji/emoji';
-import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
-
-/** @type {ImmutableList} */
-const initialState = ImmutableList([]);
-
-export default function custom_emojis(state = initialState, action) {
- if(action.type === CUSTOM_EMOJIS_FETCH_SUCCESS) {
- state = ConvertToImmutable(action.custom_emojis);
- emojiSearch('', { custom: buildCustomEmojis(state) });
- }
-
- return state;
-}
diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts
index 7343f5e164..c3bc5792ac 100644
--- a/app/javascript/mastodon/reducers/index.ts
+++ b/app/javascript/mastodon/reducers/index.ts
@@ -11,7 +11,6 @@ import announcements from './announcements';
import { composeReducer } from './compose';
import { contextsReducer } from './contexts';
import conversations from './conversations';
-import custom_emojis from './custom_emojis';
import { dropdownMenuReducer } from './dropdown_menu';
import filters from './filters';
import height_cache from './height_cache';
@@ -67,7 +66,6 @@ const reducers = {
notifications,
notificationGroups: notificationGroupsReducer,
height_cache,
- custom_emojis,
lists: listsReducer,
followedTags: followedTagsReducer,
filters,