diff --git a/app/javascript/mastodon/features/ui/components/reaction_modal.js b/app/javascript/mastodon/features/ui/components/reaction_modal.js
new file mode 100644
index 000000000..b23d7b6aa
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/reaction_modal.js
@@ -0,0 +1,268 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import StatusContent from '../../../components/status_content';
+import Avatar from '../../../components/avatar';
+import RelativeTimestamp from '../../../components/relative_timestamp';
+import DisplayName from '../../../components/display_name';
+import IconButton from '../../../components/icon_button';
+import classNames from 'classnames';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { supportsPassiveEvents } from 'detect-passive-events';
+import { EmojiPicker as EmojiPickerAsync } from '../util/async-components';
+import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
+import { assetHost } from 'mastodon/utils/config';
+
+const messages = defineMessages({
+ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
+ emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
+ emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' },
+ custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
+ recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
+ search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
+ people: { id: 'emoji_button.people', defaultMessage: 'People' },
+ nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
+ food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
+ activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
+ travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
+ objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
+ symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
+ flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
+});
+
+let EmojiPicker, Emoji; // load asynchronously
+
+const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
+
+const notFoundFn = () => (
+
+);
+
+@injectIntl
+class ReactionPicker extends React.PureComponent {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ custom_emojis: ImmutablePropTypes.list,
+ onPickEmoji: PropTypes.func.isRequired,
+ onSkinTone: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+ skinTone: PropTypes.number.isRequired,
+ frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
+ loading: PropTypes.bool,
+ style: PropTypes.object,
+ placement: PropTypes.string,
+ arrowOffsetLeft: PropTypes.string,
+ arrowOffsetTop: PropTypes.string,
+ };
+
+ static defaultProps = {
+ style: {},
+ loading: true,
+ frequentlyUsedEmojis: [],
+ };
+
+ state = {
+ modifierOpen: false,
+ placement: null,
+ };
+
+ handleDocumentClick = e => {
+ if (this.node && !this.node.contains(e.target)) {
+ this.props.onClose();
+ }
+ }
+
+ componentDidMount () {
+ document.addEventListener('click', this.handleDocumentClick, false);
+ document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
+ }
+
+ componentWillUnmount () {
+ document.removeEventListener('click', this.handleDocumentClick, false);
+ document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
+ }
+
+ setRef = c => {
+ this.node = c;
+ }
+
+ getI18n = () => {
+ const { intl } = this.props;
+
+ return {
+ search: intl.formatMessage(messages.emoji_search),
+ notfound: intl.formatMessage(messages.emoji_not_found),
+ categories: {
+ search: intl.formatMessage(messages.search_results),
+ recent: intl.formatMessage(messages.recent),
+ people: intl.formatMessage(messages.people),
+ nature: intl.formatMessage(messages.nature),
+ foods: intl.formatMessage(messages.food),
+ activity: intl.formatMessage(messages.activity),
+ places: intl.formatMessage(messages.travel),
+ objects: intl.formatMessage(messages.objects),
+ symbols: intl.formatMessage(messages.symbols),
+ flags: intl.formatMessage(messages.flags),
+ custom: intl.formatMessage(messages.custom),
+ },
+ };
+ }
+
+ handleClick = (emoji, event) => {
+ if (!emoji.native) {
+ emoji.native = emoji.colons;
+ }
+ if (!(event.ctrlKey || event.metaKey)) {
+ this.props.onClose();
+ }
+ this.props.onPickEmoji(emoji);
+ }
+
+ handleModifierOpen = () => {
+ this.setState({ modifierOpen: true });
+ }
+
+ handleModifierClose = () => {
+ this.setState({ modifierOpen: false });
+ }
+
+ handleModifierChange = modifier => {
+ this.props.onSkinTone(modifier);
+ }
+
+ render () {
+ const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
+
+ if (loading) {
+ return ;
+ }
+
+ const title = intl.formatMessage(messages.emoji);
+
+ 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 (
+
+ );
+ }
+}
+
+export default class ReactionModal extends ImmutablePureComponent {
+
+ static propTypes = {
+ status: ImmutablePropTypes.map,
+ custom_emojis: ImmutablePropTypes.list,
+ onPickEmoji: PropTypes.func.isRequired,
+ onSkinTone: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+ skinTone: PropTypes.number.isRequired,
+ frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
+ };
+
+ state = {
+ loading: true,
+ };
+
+
+ componentDidMount() {
+ this.setState({ loading: true });
+ EmojiPickerAsync().then(EmojiMart => {
+ EmojiPicker = EmojiMart.Picker;
+ Emoji = EmojiMart.Emoji;
+ this.setState({ loading: false });
+ }).catch(() => {
+ this.setState({ loading: false });
+ });
+ }
+
+ render () {
+ const { custom_emojis, onPickEmoji, onSkinTone, onClose, skinTone, frequentlyUsedEmojis } = this.props;
+ const loading = this.state.loading;
+ const status = this.props.status && (
+
+ );
+
+ return (
+
+ {status}
+
+
+ );
+ }
+
+}