From 87877a3f968182a173e2665df950f46be7057538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 11 Sep 2022 19:28:12 +0200 Subject: [PATCH] TS/functional: Emoji picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/interactions.ts | 1 - .../announcements/reactions-bar.tsx | 2 +- app/soapbox/components/polls/poll-footer.tsx | 1 - .../compose/components/compose-form.tsx | 2 +- .../components/emoji_picker_dropdown.js | 397 ------------------ .../compose/components/privacy_dropdown.tsx | 2 +- .../features/compose/components/upload.tsx | 49 +-- .../compose/components/upload_form.tsx | 8 +- .../emoji_picker_dropdown_container.js | 84 ---- .../compose/containers/upload_container.js | 37 -- .../containers/upload_progress_container.js | 10 - .../steps/suggested-accounts-step.tsx | 1 - app/soapbox/normalizers/announcement.ts | 2 +- app/soapbox/queries/suggestions.ts | 1 - app/soapbox/react-notification/index.d.ts | 2 +- app/soapbox/reducers/admin.ts | 2 +- app/soapbox/reducers/announcements.ts | 4 +- app/soapbox/reducers/index.ts | 1 + 18 files changed, 38 insertions(+), 568 deletions(-) delete mode 100644 app/soapbox/features/compose/components/emoji_picker_dropdown.js delete mode 100644 app/soapbox/features/compose/containers/emoji_picker_dropdown_container.js delete mode 100644 app/soapbox/features/compose/containers/upload_container.js delete mode 100644 app/soapbox/features/compose/containers/upload_progress_container.js diff --git a/app/soapbox/actions/interactions.ts b/app/soapbox/actions/interactions.ts index 1a90a259f..cb23f3dae 100644 --- a/app/soapbox/actions/interactions.ts +++ b/app/soapbox/actions/interactions.ts @@ -177,7 +177,6 @@ const toggleFavourite = (status: StatusEntity) => } }; - const favouriteRequest = (status: StatusEntity) => ({ type: FAVOURITE_REQUEST, status: status, diff --git a/app/soapbox/components/announcements/reactions-bar.tsx b/app/soapbox/components/announcements/reactions-bar.tsx index 5cec53974..130db2d99 100644 --- a/app/soapbox/components/announcements/reactions-bar.tsx +++ b/app/soapbox/components/announcements/reactions-bar.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { TransitionMotion, spring } from 'react-motion'; import { Icon } from 'soapbox/components/ui'; -import EmojiPickerDropdown from 'soapbox/features/compose/containers/emoji_picker_dropdown_container'; +import EmojiPickerDropdown from 'soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown'; import { useSettings } from 'soapbox/hooks'; import Reaction from './reaction'; diff --git a/app/soapbox/components/polls/poll-footer.tsx b/app/soapbox/components/polls/poll-footer.tsx index dfa91e663..693b28db1 100644 --- a/app/soapbox/components/polls/poll-footer.tsx +++ b/app/soapbox/components/polls/poll-footer.tsx @@ -45,7 +45,6 @@ const PollFooter: React.FC = ({ poll, showResults, selected }): JSX votesCount = ; } - return ( {(!showResults && poll?.multiple) && ( diff --git a/app/soapbox/features/compose/components/compose-form.tsx b/app/soapbox/features/compose/components/compose-form.tsx index fc93b1ab0..90eff3584 100644 --- a/app/soapbox/features/compose/components/compose-form.tsx +++ b/app/soapbox/features/compose/components/compose-form.tsx @@ -21,6 +21,7 @@ import { Button, Stack } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; import { isMobile } from 'soapbox/is_mobile'; +import EmojiPickerDropdown from '../components/emoji-picker/emoji-picker-dropdown'; import MarkdownButton from '../components/markdown_button'; import PollButton from '../components/poll_button'; import PollForm from '../components/polls/poll-form'; @@ -30,7 +31,6 @@ import ScheduleButton from '../components/schedule_button'; import SpoilerButton from '../components/spoiler_button'; import UploadForm from '../components/upload_form'; import Warning from '../components/warning'; -import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; import QuotedStatusContainer from '../containers/quoted_status_container'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import ScheduleFormContainer from '../containers/schedule_form_container'; 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 9deddd623..000000000 --- a/app/soapbox/features/compose/components/emoji_picker_dropdown.js +++ /dev/null @@ -1,397 +0,0 @@ -import classNames from 'clsx'; -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, Emoji; // load asynchronously - -const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png'); -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - -const categoriesSort = [ - 'recent', - 'custom', - 'people', - 'nature', - 'foods', - 'activity', - 'places', - 'objects', - 'symbols', - 'flags', -]; - -class ModifierPickerMenu extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - onSelect: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - }; - - handleClick = e => { - this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); - } - - componentDidUpdate(prevProps) { - if (this.props.active) { - this.attachListeners(); - } else { - this.removeListeners(); - } - } - - componentWillUnmount() { - this.removeListeners(); - } - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - attachListeners() { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - removeListeners() { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - render() { - const { active } = this.props; - - return ( -
- - - - - - -
- ); - } - -} - -class ModifierPicker extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - modifier: PropTypes.number, - onChange: PropTypes.func, - onClose: PropTypes.func, - onOpen: PropTypes.func, - }; - - handleClick = () => { - if (this.props.active) { - this.props.onClose(); - } else { - this.props.onOpen(); - } - } - - handleSelect = modifier => { - this.props.onChange(modifier); - this.props.onClose(); - } - - render() { - const { active, modifier } = this.props; - - return ( -
- - -
- ); - } - -} - -@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.colons; - } - - 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; - - 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, - button: PropTypes.node, - }; - - 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.Picker; - Emoji = EmojiMart.Emoji; - - 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, button } = this.props; - const title = intl.formatMessage(messages.emoji); - const { active, loading, placement } = this.state; - - return ( -
-
- {button || } -
- - - - -
- ); - } - -} diff --git a/app/soapbox/features/compose/components/privacy_dropdown.tsx b/app/soapbox/features/compose/components/privacy_dropdown.tsx index 193b0a7b8..da9fd57b3 100644 --- a/app/soapbox/features/compose/components/privacy_dropdown.tsx +++ b/app/soapbox/features/compose/components/privacy_dropdown.tsx @@ -10,8 +10,8 @@ import { changeComposeVisibility } from 'soapbox/actions/compose'; import { closeModal, openModal } from 'soapbox/actions/modals'; import Icon from 'soapbox/components/icon'; import { IconButton } from 'soapbox/components/ui'; -import { isUserTouching } from 'soapbox/is_mobile'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { isUserTouching } from 'soapbox/is_mobile'; import Motion from '../../ui/util/optional_motion'; diff --git a/app/soapbox/features/compose/components/upload.tsx b/app/soapbox/features/compose/components/upload.tsx index 77876c505..66fa25c4b 100644 --- a/app/soapbox/features/compose/components/upload.tsx +++ b/app/soapbox/features/compose/components/upload.tsx @@ -1,17 +1,19 @@ import classNames from 'clsx'; +import { List as ImmutableList } from 'immutable'; import React, { useState } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { spring } from 'react-motion'; import { useHistory } from 'react-router-dom'; +import { undoUploadCompose, changeUploadCompose, submitCompose } from 'soapbox/actions/compose'; +import { openModal } from 'soapbox/actions/modals'; import Blurhash from 'soapbox/components/blurhash'; import Icon from 'soapbox/components/icon'; import IconButton from 'soapbox/components/icon_button'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import Motion from '../../ui/util/optional_motion'; -import type { Map as ImmutableMap } from 'immutable'; - const bookIcon = require('@tabler/icons/book.svg'); const fileCodeIcon = require('@tabler/icons/file-code.svg'); const fileSpreadsheetIcon = require('@tabler/icons/file-spreadsheet.svg'); @@ -60,18 +62,17 @@ const messages = defineMessages({ }); interface IUpload { - media: ImmutableMap, - descriptionLimit: number, - onUndo: (attachmentId: string) => void, - onDescriptionChange: (attachmentId: string, description: string) => void, - onOpenFocalPoint: (attachmentId: string) => void, - onOpenModal: (attachments: ImmutableMap) => void, - onSubmit: (history: ReturnType) => void, + id: string, + composeId: string, } -const Upload: React.FC = (props) => { +const Upload: React.FC = ({ composeId, id }) => { const intl = useIntl(); const history = useHistory(); + const dispatch = useAppDispatch(); + + const media = useAppSelector((state) => state.compose.get(composeId)!.media_attachments.find(item => item.get('id') === id)!); + const descriptionLimit = useAppSelector((state) => state.instance.get('description_limit')); const [hovered, setHovered] = useState(false); const [focused, setFocused] = useState(false); @@ -85,12 +86,12 @@ const Upload: React.FC = (props) => { const handleSubmit = () => { handleInputBlur(); - props.onSubmit(history); + dispatch(submitCompose(composeId, history)); }; const handleUndoClick: React.MouseEventHandler = e => { e.stopPropagation(); - props.onUndo(props.media.get('id')); + dispatch(undoUploadCompose(media.id)); }; const handleInputChange: React.ChangeEventHandler = e => { @@ -118,22 +119,22 @@ const Upload: React.FC = (props) => { setDirtyDescription(null); if (dirtyDescription !== null) { - props.onDescriptionChange(props.media.get('id'), dirtyDescription); + dispatch(changeUploadCompose(media.id, { dirtyDescription })); } }; const handleOpenModal = () => { - props.onOpenModal(props.media); + dispatch(openModal('MEDIA', { media: ImmutableList.of(media), index: 0 })); }; const active = hovered || focused; - const description = dirtyDescription || (dirtyDescription !== '' && props.media.get('description')) || ''; - const focusX = props.media.getIn(['meta', 'focus', 'x']) as number | undefined; - const focusY = props.media.getIn(['meta', 'focus', 'y']) as number | undefined; + const description = dirtyDescription || (dirtyDescription !== '' && media.description) || ''; + const focusX = media.meta.getIn(['focus', 'x']) as number | undefined; + const focusY = media.meta.getIn(['focus', 'y']) as number | undefined; const x = focusX ? ((focusX / 2) + .5) * 100 : undefined; const y = focusY ? ((focusY / -2) + .5) * 100 : undefined; - const mediaType = props.media.get('type'); - const mimeType = props.media.getIn(['pleroma', 'mime_type']) as string | undefined; + const mediaType = media.type; + const mimeType = media.pleroma.get('mime_type') as string | undefined; const uploadIcon = mediaType === 'unknown' && ( = (props) => { return (
- + {({ scale }) => (
@@ -162,7 +163,7 @@ const Upload: React.FC = (props) => { /> {/* Only display the "Preview" button for a valid attachment with a URL */} - {(mediaType !== 'unknown' && Boolean(props.media.get('url'))) && ( + {(mediaType !== 'unknown' && Boolean(media.url)) && ( = (props) => {