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,