diff --git a/app/soapbox/components/ui/text/text.tsx b/app/soapbox/components/ui/text/text.tsx index 072491109..f7513595f 100644 --- a/app/soapbox/components/ui/text/text.tsx +++ b/app/soapbox/components/ui/text/text.tsx @@ -9,6 +9,7 @@ type TrackingSizes = 'normal' | 'wide' type TransformProperties = 'uppercase' | 'normal' type Families = 'sans' | 'mono' type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label' +type Directions = 'ltr' | 'rtl' const themes = { default: 'text-gray-900 dark:text-gray-100', @@ -64,6 +65,8 @@ interface IText extends Pick, 'danger align?: Alignments, /** Extra class names for the outer element. */ className?: string, + /** Text direction. */ + direction?: Directions, /** Typeface of the text. */ family?: Families, /** The "for" attribute specifies which form element a label is bound to. */ @@ -90,6 +93,7 @@ const Text: React.FC = React.forwardRef( const { align, className, + direction, family = 'sans', size = 'md', tag = 'p', @@ -109,7 +113,10 @@ const Text: React.FC = React.forwardRef( { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - handleKeyDown = e => { - const { items } = this.props; - const value = e.currentTarget.getAttribute('data-index'); - const index = items.findIndex(item => { - return (item.value === value); - }); - let element = null; - - switch (e.key) { - case 'Escape': - this.props.onClose(); - break; - case 'Enter': - this.handleClick(e); - break; - case 'ArrowDown': - element = this.node.childNodes[index + 1] || this.node.firstChild; - break; - case 'ArrowUp': - element = this.node.childNodes[index - 1] || this.node.lastChild; - break; - case 'Tab': - if (e.shiftKey) { - element = this.node.childNodes[index - 1] || this.node.lastChild; - } else { - element = this.node.childNodes[index + 1] || this.node.firstChild; - } - break; - case 'Home': - element = this.node.firstChild; - break; - case 'End': - element = this.node.lastChild; - break; - } - - if (element) { - element.focus(); - this.props.onChange(element.getAttribute('data-index')); - e.preventDefault(); - e.stopPropagation(); - } - } - - handleClick = e => { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); - - this.props.onClose(); - this.props.onChange(value); - } - - componentDidMount() { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); - this.setState({ mounted: true }); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - setFocusRef = c => { - this.focusedItem = c; - } - - render() { - const { mounted } = this.state; - const { style, items, placement, value } = this.props; - - return ( - - {({ opacity, scaleX, scaleY }) => ( - // It should not be transformed when mounting because the resulting - // size will be used to determine the coordinate of the menu by - // react-overlays -
- {items.map(item => ( -
-
- -
- -
- {item.text} - {item.meta} -
-
- ))} -
- )} -
- ); - } - -} - -export default @injectIntl -class PrivacyDropdown extends React.PureComponent { - - static propTypes = { - isUserTouching: PropTypes.func, - isModalOpen: PropTypes.bool.isRequired, - onModalOpen: PropTypes.func, - onModalClose: PropTypes.func, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - open: false, - placement: 'bottom', - }; - - constructor(props) { - super(props); - const { intl: { formatMessage } } = props; - - this.options = [ - { icon: require('@tabler/icons/icons/world.svg'), value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, - { icon: require('@tabler/icons/icons/lock-open.svg'), value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, - { icon: require('@tabler/icons/icons/lock.svg'), value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, - { icon: require('@tabler/icons/icons/mail.svg'), value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, - ]; - } - - handleToggle = (e) => { - if (this.props.isUserTouching()) { - if (this.state.open) { - this.props.onModalClose(); - } else { - this.props.onModalOpen({ - actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })), - onClick: this.handleModalActionClick, - }); - } - } else { - const { top } = e.target.getBoundingClientRect(); - if (this.state.open && this.activeElement) { - this.activeElement.focus(); - } - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); - this.setState({ open: !this.state.open }); - } - e.stopPropagation(); - } - - handleModalActionClick = (e) => { - e.preventDefault(); - - const { value } = this.options[e.currentTarget.getAttribute('data-index')]; - - this.props.onModalClose(); - this.props.onChange(value); - } - - handleKeyDown = e => { - switch (e.key) { - case 'Escape': - this.handleClose(); - break; - } - } - - handleMouseDown = () => { - if (!this.state.open) { - this.activeElement = document.activeElement; - } - } - - handleButtonKeyDown = (e) => { - switch (e.key) { - case ' ': - case 'Enter': - this.handleMouseDown(); - break; - } - } - - handleClose = () => { - if (this.state.open && this.activeElement) { - this.activeElement.focus(); - } - this.setState({ open: false }); - } - - handleChange = value => { - this.props.onChange(value); - } - - render() { - const { value, intl, unavailable } = this.props; - const { open, placement } = this.state; - - if (unavailable) { - return null; - } - - const valueOption = this.options.find(item => item.value === value); - - return ( -
-
- -
- - - - -
- ); - } - -} diff --git a/app/soapbox/features/compose/components/privacy_dropdown.tsx b/app/soapbox/features/compose/components/privacy_dropdown.tsx new file mode 100644 index 000000000..94ebfcc09 --- /dev/null +++ b/app/soapbox/features/compose/components/privacy_dropdown.tsx @@ -0,0 +1,262 @@ +import classNames from 'classnames'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import React, { useState, useRef, useEffect } from 'react'; +import { useIntl, defineMessages } from 'react-intl'; +import { spring } from 'react-motion'; +// @ts-ignore +import Overlay from 'react-overlays/lib/Overlay'; + +import Icon from 'soapbox/components/icon'; +import { IconButton } from 'soapbox/components/ui'; + +import Motion from '../../ui/util/optional_motion'; + +const messages = defineMessages({ + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not post to public timelines' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' }, + change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust post privacy' }, +}); + +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +interface IPrivacyDropdownMenu { + style?: React.CSSProperties, + items: any[], + value: string, + placement: string, + onClose: () => void, + onChange: (value: string | null) => void, + unavailable?: boolean, +} + +const PrivacyDropdownMenu: React.FC = ({ style, items, placement, value, onClose, onChange }) => { + const node = useRef(null); + const focusedItem = useRef(null); + + const [mounted, setMounted] = useState(false); + + const handleDocumentClick = (e: MouseEvent | TouchEvent) => { + if (node.current && !node.current.contains(e.target as HTMLElement)) { + onClose(); + } + }; + + const handleKeyDown: React.KeyboardEventHandler = e => { + const value = e.currentTarget.getAttribute('data-index'); + const index = items.findIndex(item => item.value === value); + let element = null; + + switch (e.key) { + case 'Escape': + onClose(); + break; + case 'Enter': + handleClick(e); + break; + case 'ArrowDown': + element = node.current?.childNodes[index + 1] || node.current?.firstChild; + break; + case 'ArrowUp': + element = node.current?.childNodes[index - 1] || node.current?.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = node.current?.childNodes[index - 1] || node.current?.lastChild; + } else { + element = node.current?.childNodes[index + 1] || node.current?.firstChild; + } + break; + case 'Home': + element = node.current?.firstChild; + break; + case 'End': + element = node.current?.lastChild; + break; + } + + if (element) { + (element as HTMLElement).focus(); + onChange((element as HTMLElement).getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + }; + + const handleClick: React.EventHandler = (e: MouseEvent | KeyboardEvent) => { + const value = (e.currentTarget as HTMLElement)?.getAttribute('data-index'); + + e.preventDefault(); + + onClose(); + onChange(value); + }; + + useEffect(() => { + document.addEventListener('click', handleDocumentClick, false); + document.addEventListener('touchend', handleDocumentClick, listenerOptions); + + focusedItem.current?.focus({ preventScroll: true }); + setMounted(true); + + return () => { + document.removeEventListener('click', handleDocumentClick, false); + document.removeEventListener('touchend', handleDocumentClick); + }; + }, []); + + return ( + + {({ opacity, scaleX, scaleY }) => ( + // It should not be transformed when mounting because the resulting + // size will be used to determine the coordinate of the menu by + // react-overlays +
+ {items.map(item => ( +
+
+ +
+ +
+ {item.text} + {item.meta} +
+
+ ))} +
+ )} +
+ ); +}; + +interface IPrivacyDropdown { + isUserTouching: () => boolean, + isModalOpen: boolean, + onModalOpen: (opts: any) => void, + onModalClose: () => void, + value: string, + onChange: (value: string | null) => void, + unavailable: boolean, +} + +const PrivacyDropdown: React.FC = ({ + isUserTouching, + onChange, + onModalClose, + onModalOpen, + value, + unavailable, +}) => { + const intl = useIntl(); + const node = useRef(null); + const activeElement = useRef(null); + + const [open, setOpen] = useState(false); + const [placement, setPlacement] = useState('bottom'); + + const options = [ + { icon: require('@tabler/icons/icons/world.svg'), value: 'public', text: intl.formatMessage(messages.public_short), meta: intl.formatMessage(messages.public_long) }, + { icon: require('@tabler/icons/icons/lock-open.svg'), value: 'unlisted', text: intl.formatMessage(messages.unlisted_short), meta: intl.formatMessage(messages.unlisted_long) }, + { icon: require('@tabler/icons/icons/lock.svg'), value: 'private', text: intl.formatMessage(messages.private_short), meta: intl.formatMessage(messages.private_long) }, + { icon: require('@tabler/icons/icons/mail.svg'), value: 'direct', text: intl.formatMessage(messages.direct_short), meta: intl.formatMessage(messages.direct_long) }, + ]; + + const handleToggle: React.MouseEventHandler = (e) => { + if (isUserTouching()) { + if (open) { + onModalClose(); + } else { + onModalOpen({ + actions: options.map(option => ({ ...option, active: option.value === value })), + onClick: handleModalActionClick, + }); + } + } else { + const { top } = e.currentTarget.getBoundingClientRect(); + if (open) { + activeElement.current?.focus(); + } + setPlacement(top * 2 < innerHeight ? 'bottom' : 'top'); + setOpen(!open); + } + e.stopPropagation(); + }; + + const handleModalActionClick: React.MouseEventHandler = (e) => { + e.preventDefault(); + + const { value } = options[e.currentTarget.getAttribute('data-index') as any]; + + onModalClose(); + onChange(value); + }; + + const handleKeyDown: React.KeyboardEventHandler = e => { + switch (e.key) { + case 'Escape': + handleClose(); + break; + } + }; + + const handleMouseDown = () => { + if (!open) { + activeElement.current = document.activeElement as HTMLElement | null; + } + }; + + const handleButtonKeyDown: React.KeyboardEventHandler = (e) => { + switch (e.key) { + case ' ': + case 'Enter': + handleMouseDown(); + break; + } + }; + + const handleClose = () => { + if (open) { + activeElement.current?.focus(); + } + setOpen(false); + }; + + if (unavailable) { + return null; + } + + const valueOption = options.find(item => item.value === value); + + return ( +
+
+ +
+ + + + +
+ ); +}; + +export default PrivacyDropdown; diff --git a/app/soapbox/features/compose/components/reply_indicator.js b/app/soapbox/features/compose/components/reply_indicator.js deleted file mode 100644 index ffa3d6a11..000000000 --- a/app/soapbox/features/compose/components/reply_indicator.js +++ /dev/null @@ -1,69 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; -import { Stack, Text } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account_container'; -import { isRtl } from 'soapbox/rtl'; - -export default class ReplyIndicator extends ImmutablePureComponent { - - static propTypes = { - status: ImmutablePropTypes.record, - onCancel: PropTypes.func.isRequired, - hideActions: PropTypes.bool, - }; - - handleClick = () => { - this.props.onCancel(); - } - - render() { - const { status, hideActions } = this.props; - - if (!status) { - return null; - } - - const style = { - direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr', - }; - - let actions = {}; - if (!hideActions) { - actions = { - onActionClick: this.handleClick, - actionIcon: require('@tabler/icons/icons/x.svg'), - actionAlignment: 'top', - actionTitle: 'Dismiss', - }; - } - - return ( - - - - - - {status.get('media_attachments').size > 0 && ( - - )} - - ); - } - -} diff --git a/app/soapbox/features/compose/components/reply_indicator.tsx b/app/soapbox/features/compose/components/reply_indicator.tsx new file mode 100644 index 000000000..d135bfdeb --- /dev/null +++ b/app/soapbox/features/compose/components/reply_indicator.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; +import { Stack, Text } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { isRtl } from 'soapbox/rtl'; + +import type { Status } from 'soapbox/types/entities'; + +interface IReplyIndicator { + status?: Status, + onCancel: () => void, + hideActions: boolean, +} + +const ReplyIndicator: React.FC = ({ status, hideActions, onCancel }) => { + const handleClick = () => { + onCancel(); + }; + + if (!status) { + return null; + } + + let actions = {}; + if (!hideActions) { + actions = { + onActionClick: handleClick, + actionIcon: require('@tabler/icons/icons/x.svg'), + actionAlignment: 'top', + actionTitle: 'Dismiss', + }; + } + + return ( + + + + + + {status.media_attachments.size > 0 && ( + + )} + + ); +}; + +export default ReplyIndicator; diff --git a/app/soapbox/features/compose/components/schedule_button.js b/app/soapbox/features/compose/components/schedule_button.js deleted file mode 100644 index 484174355..000000000 --- a/app/soapbox/features/compose/components/schedule_button.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; - -import ComposeFormButton from './compose_form_button'; - -const messages = defineMessages({ - add_schedule: { id: 'schedule_button.add_schedule', defaultMessage: 'Schedule post for later' }, - remove_schedule: { id: 'schedule_button.remove_schedule', defaultMessage: 'Post immediately' }, -}); - -export default -@injectIntl -class ScheduleButton extends React.PureComponent { - - static propTypes = { - disabled: PropTypes.bool, - active: PropTypes.bool, - unavailable: PropTypes.bool, - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleClick = () => { - this.props.onClick(); - } - - render() { - const { intl, active, unavailable, disabled } = this.props; - - if (unavailable) { - return null; - } - - return ( - - ); - } - -} diff --git a/app/soapbox/features/compose/components/schedule_button.tsx b/app/soapbox/features/compose/components/schedule_button.tsx new file mode 100644 index 000000000..8d1509884 --- /dev/null +++ b/app/soapbox/features/compose/components/schedule_button.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import ComposeFormButton from './compose_form_button'; + +const messages = defineMessages({ + add_schedule: { id: 'schedule_button.add_schedule', defaultMessage: 'Schedule post for later' }, + remove_schedule: { id: 'schedule_button.remove_schedule', defaultMessage: 'Post immediately' }, +}); + +interface IScheduleButton { + disabled: boolean, + active: boolean, + unavailable: boolean, + onClick: () => void, +} + +const ScheduleButton: React.FC = ({ active, unavailable, disabled, onClick }) => { + const intl = useIntl(); + + const handleClick = () => { + onClick(); + }; + + if (unavailable) { + return null; + } + + return ( + + ); +}; + +export default ScheduleButton; diff --git a/app/soapbox/features/compose/components/text_icon_button.js b/app/soapbox/features/compose/components/text_icon_button.js deleted file mode 100644 index 1ca71fe6f..000000000 --- a/app/soapbox/features/compose/components/text_icon_button.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -export default class TextIconButton extends React.PureComponent { - - static propTypes = { - label: PropTypes.string.isRequired, - title: PropTypes.string, - active: PropTypes.bool, - onClick: PropTypes.func.isRequired, - ariaControls: PropTypes.string, - unavailable: PropTypes.bool, - }; - - handleClick = (e) => { - e.preventDefault(); - this.props.onClick(); - } - - render() { - const { label, title, active, ariaControls, unavailable } = this.props; - - if (unavailable) { - return null; - } - - return ( - - ); - } - -} diff --git a/app/soapbox/features/compose/components/text_icon_button.tsx b/app/soapbox/features/compose/components/text_icon_button.tsx new file mode 100644 index 000000000..fd49d4ed0 --- /dev/null +++ b/app/soapbox/features/compose/components/text_icon_button.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +interface ITextIconButton { + label: string, + title: string, + active: boolean, + onClick: () => void, + ariaControls: string, + unavailable: boolean, +} + +const TextIconButton: React.FC = ({ + label, + title, + active, + ariaControls, + unavailable, + onClick, +}) => { + const handleClick: React.MouseEventHandler = (e) => { + e.preventDefault(); + onClick(); + }; + + if (unavailable) { + return null; + } + + return ( + + ); +}; + +export default TextIconButton; diff --git a/app/soapbox/features/compose/components/upload.js b/app/soapbox/features/compose/components/upload.js deleted file mode 100644 index a293509f9..000000000 --- a/app/soapbox/features/compose/components/upload.js +++ /dev/null @@ -1,212 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import spring from 'react-motion/lib/spring'; -import { withRouter } from 'react-router-dom'; - -import Blurhash from 'soapbox/components/blurhash'; -import Icon from 'soapbox/components/icon'; -import IconButton from 'soapbox/components/icon_button'; - -import Motion from '../../ui/util/optional_motion'; - -const bookIcon = require('@tabler/icons/icons/book.svg'); -const fileAnalyticsIcon = require('@tabler/icons/icons/file-analytics.svg'); -const fileCodeIcon = require('@tabler/icons/icons/file-code.svg'); -const fileTextIcon = require('@tabler/icons/icons/file-text.svg'); -const fileZipIcon = require('@tabler/icons/icons/file-zip.svg'); -const presentationIcon = require('@tabler/icons/icons/presentation.svg'); - -export const MIMETYPE_ICONS = { - 'application/x-freearc': fileZipIcon, - 'application/x-bzip': fileZipIcon, - 'application/x-bzip2': fileZipIcon, - 'application/gzip': fileZipIcon, - 'application/vnd.rar': fileZipIcon, - 'application/x-tar': fileZipIcon, - 'application/zip': fileZipIcon, - 'application/x-7z-compressed': fileZipIcon, - 'application/x-csh': fileCodeIcon, - 'application/html': fileCodeIcon, - 'text/javascript': fileCodeIcon, - 'application/json': fileCodeIcon, - 'application/ld+json': fileCodeIcon, - 'application/x-httpd-php': fileCodeIcon, - 'application/x-sh': fileCodeIcon, - 'application/xhtml+xml': fileCodeIcon, - 'application/xml': fileCodeIcon, - 'application/epub+zip': bookIcon, - 'application/vnd.oasis.opendocument.spreadsheet': fileAnalyticsIcon, - 'application/vnd.ms-excel': fileAnalyticsIcon, - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': fileAnalyticsIcon, - 'application/pdf': fileTextIcon, - 'application/vnd.oasis.opendocument.presentation': presentationIcon, - 'application/vnd.ms-powerpoint': presentationIcon, - 'application/vnd.openxmlformats-officedocument.presentationml.presentation': presentationIcon, - 'text/plain': fileTextIcon, - 'application/rtf': fileTextIcon, - 'application/msword': fileTextIcon, - 'application/x-abiword': fileTextIcon, - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': fileTextIcon, - 'application/vnd.oasis.opendocument.text': fileTextIcon, -}; - -const messages = defineMessages({ - description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, - delete: { id: 'upload_form.undo', defaultMessage: 'Delete' }, -}); - -export default @injectIntl @withRouter -class Upload extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, - onUndo: PropTypes.func.isRequired, - onDescriptionChange: PropTypes.func.isRequired, - onOpenFocalPoint: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, - }; - - state = { - hovered: false, - focused: false, - dirtyDescription: null, - }; - - handleKeyDown = (e) => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.handleSubmit(); - } - } - - handleSubmit = () => { - this.handleInputBlur(); - this.props.onSubmit(this.props.history); - } - - handleUndoClick = e => { - e.stopPropagation(); - this.props.onUndo(this.props.media.get('id')); - } - - handleFocalPointClick = e => { - e.stopPropagation(); - this.props.onOpenFocalPoint(this.props.media.get('id')); - } - - handleInputChange = e => { - this.setState({ dirtyDescription: e.target.value }); - } - - handleMouseEnter = () => { - this.setState({ hovered: true }); - } - - handleMouseLeave = () => { - this.setState({ hovered: false }); - } - - handleInputFocus = () => { - this.setState({ focused: true }); - } - - handleClick = () => { - this.setState({ focused: true }); - } - - handleInputBlur = () => { - const { dirtyDescription } = this.state; - - this.setState({ focused: false, dirtyDescription: null }); - - if (dirtyDescription !== null) { - this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription); - } - } - - handleOpenModal = () => { - this.props.onOpenModal(this.props.media); - } - - render() { - const { intl, media, descriptionLimit } = this.props; - const active = this.state.hovered || this.state.focused; - const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''; - const focusX = media.getIn(['meta', 'focus', 'x']); - const focusY = media.getIn(['meta', 'focus', 'y']); - const x = ((focusX / 2) + .5) * 100; - const y = ((focusY / -2) + .5) * 100; - const mediaType = media.get('type'); - const uploadIcon = mediaType === 'unknown' && ( - - ); - - return ( -
- - - {({ scale }) => ( -
-
- } - /> - - {/* Only display the "Preview" button for a valid attachment with a URL */} - {(mediaType !== 'unknown' && Boolean(media.get('url'))) && ( - } - /> - )} -
- -
-