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} + +
+ ); + } + +}