diff --git a/app/soapbox/features/compose/components/emoji_picker_dropdown.js b/app/soapbox/features/compose/components/emoji_picker_dropdown.js deleted file mode 100644 index bec0261aa..000000000 --- a/app/soapbox/features/compose/components/emoji_picker_dropdown.js +++ /dev/null @@ -1,270 +0,0 @@ -import classNames from 'classnames'; -import { supportsPassiveEvents } from 'detect-passive-events'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; - -import { IconButton } from 'soapbox/components/ui'; - -import { buildCustomEmojis } from '../../emoji/emoji'; -import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; - -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 emoji\'s found.' }, - 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; // load asynchronously - -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - -@injectIntl -class EmojiPickerMenu extends React.PureComponent { - - static propTypes = { - custom_emojis: ImmutablePropTypes.list, - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), - loading: PropTypes.bool, - onClose: PropTypes.func.isRequired, - onPick: PropTypes.func.isRequired, - style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, - intl: PropTypes.object.isRequired, - skinTone: PropTypes.number.isRequired, - onSkinTone: PropTypes.func.isRequired, - }; - - 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 => { - if (!emoji.native) { - emoji.native = emoji.shortcodes; - } - - this.props.onClose(); - this.props.onPick(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 theme = 'dark'; - - return ( -
- -
- ); - } - -} - -export default @injectIntl -class EmojiPickerDropdown extends React.PureComponent { - - static propTypes = { - custom_emojis: ImmutablePropTypes.list, - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), - intl: PropTypes.object.isRequired, - onPickEmoji: PropTypes.func.isRequired, - onSkinTone: PropTypes.func.isRequired, - skinTone: PropTypes.number.isRequired, - }; - - state = { - active: false, - loading: false, - }; - - setRef = (c) => { - this.dropdown = c; - } - - onShowDropdown = (e) => { - e.stopPropagation(); - - this.setState({ active: true }); - - if (!EmojiPicker) { - this.setState({ loading: true }); - - EmojiPickerAsync().then(EmojiMart => { - EmojiPicker = EmojiMart; - - this.setState({ loading: false }); - }).catch(() => { - this.setState({ loading: false }); - }); - } - - const { top } = e.target.getBoundingClientRect(); - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); - } - - onHideDropdown = () => { - this.setState({ active: false }); - } - - onToggle = (e) => { - if (!this.state.loading && (!e.key || e.key === 'Enter')) { - if (this.state.active) { - this.onHideDropdown(); - } else { - this.onShowDropdown(e); - } - } - } - - handleKeyDown = e => { - if (e.key === 'Escape') { - this.onHideDropdown(); - } - } - - setTargetRef = c => { - this.target = c; - } - - findTarget = () => { - return this.target; - } - - render() { - const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; - const title = intl.formatMessage(messages.emoji); - const { active, loading, placement } = this.state; - - return ( -
- - - - - -
- ); - } - -} diff --git a/app/soapbox/features/compose/components/emoji_picker_dropdown.tsx b/app/soapbox/features/compose/components/emoji_picker_dropdown.tsx new file mode 100644 index 000000000..5704f8f61 --- /dev/null +++ b/app/soapbox/features/compose/components/emoji_picker_dropdown.tsx @@ -0,0 +1,188 @@ +import classNames from 'classnames'; +import React, { useRef, useEffect, useState } from 'react'; +import { usePopper } from 'react-popper'; +import { defineMessages, useIntl } from 'react-intl'; +import { createPortal } from 'react-dom'; +import { supportsPassiveEvents } from 'detect-passive-events'; + +import { IconButton, Toggle } from 'soapbox/components/ui'; +import { useSettings, useSystemTheme } from 'soapbox/hooks'; +import type { List } from 'immutable'; + +import { buildCustomEmojis } from '../../emoji/emoji'; +import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; +// import EmojiPicker from '../../emoji/emoji_picker'; + +let EmojiPicker: any; // load asynchronously + +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 emoji\'s found.' }, + 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' }, +}); + +interface IEmojiPickerDropdown { + custom_emojis: any, + frequentlyUsedEmojis: string[], + intl: any, + onPickEmoji: (emoji: any) => void, + onSkinTone: () => void, + skinTone: () => void, +} + +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +const EmojiPickerDropdown: React.FC = ({ custom_emojis, frequentlyUsedEmojis, onPickEmoji, onSkinTone, skinTone }) => { + const intl = useIntl(); + const settings = useSettings(); + const title = intl.formatMessage(messages.emoji); + const userTheme = settings.get('themeMode'); + const theme = (userTheme === 'dark' || userTheme === 'light') ? userTheme : 'auto'; + + const [popperElement, setPopperElement] = useState(null); + const [referenceElement, setReferenceElement] = useState(null); + const [containerElement, setContainerElement] = useState(null); + + const [visible, setVisible] = useState(false); + const [loading, setLoading] = useState(false); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: 'top-start', + }); + + const handleToggle = () => { + setVisible(!visible); + }; + + const handleHide = () => { + setVisible(false); + }; + + const handleDocClick = (e: any) => { + if (!containerElement?.contains(e.target) && !popperElement?.contains(e.target)) { + setVisible(false); + } + } + + const handlePick = (emoji: any) => { + if (!emoji.native) { + emoji.native = emoji.shortcodes; + } + + setVisible(false); + onPickEmoji(emoji); + } + + const getI18n = () => { + 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), + }, + }; + }; + + useEffect(() => { + document.addEventListener('click', handleDocClick, false); + document.addEventListener('touchend', handleDocClick, listenerOptions); + + return function cleanup() { + document.removeEventListener('click', handleDocClick); + document.removeEventListener('touchend', handleDocClick); + } + }); + + useEffect(() => { + if (!EmojiPicker) { + setLoading(true); + + EmojiPickerAsync().then(EmojiMart => { + EmojiPicker = EmojiMart.Picker; + + setLoading(false); + }).catch(() => { + setLoading(false); + }); + } + }, [visible]); + + let Popup; + + if (loading) { + Popup = () =>
; + } else { + Popup = () =>
+ +
+ } + + return ( +
+ + + {createPortal( +
+ {visible && ()} +
, + document.body + )} +
+ ); +}; + +export default EmojiPickerDropdown; diff --git a/app/soapbox/features/emoji/emoji_picker.js b/app/soapbox/features/emoji/emoji_picker.tsx similarity index 63% rename from app/soapbox/features/emoji/emoji_picker.js rename to app/soapbox/features/emoji/emoji_picker.tsx index 4dda1de07..691af4a13 100644 --- a/app/soapbox/features/emoji/emoji_picker.js +++ b/app/soapbox/features/emoji/emoji_picker.tsx @@ -1,5 +1,5 @@ -import data from '@emoji-mart/data'; -import { Picker as EmojiPicker } from 'emoji-mart'; +import data from '@emoji-mart/data/sets/14/twitter.json' +import { Picker as EmojiPicker, PickerProps } from 'emoji-mart'; import React, { useRef, useEffect } from 'react'; // const categories = [ @@ -14,9 +14,10 @@ import React, { useRef, useEffect } from 'react'; // 'symbols', // 'flags', // ]; +// -export default function Picker(props) { - const ref = useRef(); +function Picker(props: PickerProps) { + const ref = useRef(null); useEffect(() => { const input = { ...props, data, ref }; @@ -26,3 +27,7 @@ export default function Picker(props) { return
; } + +export { + Picker +} diff --git a/app/styles/emoji_picker.scss b/app/styles/emoji_picker.scss index 7bef34d27..0b94fcfb7 100644 --- a/app/styles/emoji_picker.scss +++ b/app/styles/emoji_picker.scss @@ -1,4 +1,3 @@ -.emoji-mart, .emoji-mart * { box-sizing: border-box; line-height: 1.15; diff --git a/types/emoji-mart/index.d.ts b/types/emoji-mart/index.d.ts new file mode 100644 index 000000000..2f0daf1ab --- /dev/null +++ b/types/emoji-mart/index.d.ts @@ -0,0 +1,26 @@ +declare module 'emoji-mart' { + export type PickerProps = { + custom?: { emojis: any[] }[], + set?: string, + title?: string, + theme?: string, + onEmojiSelect?: any, + recent?: any, + skin?: any, + perLine?: number, + emojiSize?: number, + emojiButtonSize?: number, + navPosition?: string, + set?: string, + theme?: string, + autoFocus?: boolean, + i18n?: any, + } + + export class Picker { + + constructor(props: PickerProps); + + } + +}